Compare commits

..

1 Commits

Author SHA1 Message Date
Ankit Nayan
0db4073e94 chore: different ticker interval for active user
(cherry picked from commit 215ea8d819)
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-01-08 21:25:47 +00:00
343 changed files with 18153 additions and 7930 deletions

2
.github/config.yml vendored
View File

@@ -17,7 +17,7 @@ newPRWelcomeComment: >
# Comment to be posted to on pull requests merged by a first time user
firstPRMergeComment: >
Congrats on merging your first pull request!
![minion-party](https://i.imgur.com/Xlg59lP.gif)
We here at SigNoz are proud of you! 🥳

View File

@@ -32,10 +32,6 @@ 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: |

View File

@@ -57,7 +57,7 @@ jobs:
--set frontend.service.type=LoadBalancer \
--set queryService.image.tag=$DOCKER_TAG \
--set frontend.image.tag=$DOCKER_TAG
# get pods, services and the container images
kubectl get pods -n platform
kubectl get svc -n platform

View File

@@ -17,3 +17,4 @@ jobs:
uses: hattan/verify-linked-issue-action@v1.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -11,6 +11,6 @@ jobs:
- name: Remove label
uses: buildsville/add-remove-label@v1
with:
label: ok-to-test,testing-deploy
label: ok-to-test
type: remove
token: ${{ secrets.GITHUB_TOKEN }}

25
.github/workflows/repo-stats.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
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 }}

View File

@@ -24,3 +24,4 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

View File

@@ -1,38 +0,0 @@
name: staging-deployment
# Trigger deployment only on push to develop branch
on:
push:
branches:
- develop
jobs:
deploy:
name: Deploy latest develop branch to staging
runs-on: ubuntu-latest
environment: staging
steps:
- name: Executing remote ssh commands using ssh key
uses: appleboy/ssh-action@v0.1.6
env:
GITHUB_BRANCH: develop
GITHUB_SHA: ${{ github.sha }}
with:
host: ${{ secrets.HOST_DNS }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.EC2_SSH_KEY }}
envs: GITHUB_BRANCH,GITHUB_SHA
command_timeout: 60m
script: |
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
echo "GITHUB_SHA: ${GITHUB_SHA}"
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
docker system prune --force
cd ~/signoz
git status
git add .
git stash push -m "stashed on $(date --iso-8601=seconds)"
git fetch origin
git checkout ${GITHUB_BRANCH}
git pull
make build-ee-query-service-amd64
make build-frontend-amd64
make run-signoz

View File

@@ -1,39 +0,0 @@
name: testing-deployment
# Trigger deployment only on testing-deploy label on pull request
on:
pull_request:
types: [labeled]
jobs:
deploy:
name: Deploy PR branch to testing
runs-on: ubuntu-latest
environment: testing
if: ${{ github.event.label.name == 'testing-deploy' }}
steps:
- name: Executing remote ssh commands using ssh key
uses: appleboy/ssh-action@v0.1.6
env:
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
GITHUB_SHA: ${{ github.sha }}
with:
host: ${{ secrets.HOST_DNS }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.EC2_SSH_KEY }}
envs: GITHUB_BRANCH,GITHUB_SHA
command_timeout: 60m
script: |
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
echo "GITHUB_SHA: ${GITHUB_SHA}"
export DOCKER_TAG="${GITHUB_SHA:0:7}" # needed for child process to access it
export DEV_BUILD="1"
docker system prune --force
cd ~/signoz
git status
git add .
git stash push -m "stashed on $(date --iso-8601=seconds)"
git fetch origin
git checkout ${GITHUB_BRANCH}
git pull
make build-ee-query-service-amd64
make build-frontend-amd64
make run-signoz

View File

@@ -215,26 +215,9 @@ Please ping us in the [`#contributing`](https://signoz-community.slack.com/archi
# 4. Contribute to Backend (Query-Service) 🌑
**Need to Update: [https://github.com/SigNoz/signoz/tree/develop/pkg/query-service](https://github.com/SigNoz/signoz/tree/develop/pkg/query-service)**
[**https://github.com/SigNoz/signoz/tree/develop/pkg/query-service**](https://github.com/SigNoz/signoz/tree/develop/pkg/query-service)
## 4.1 Prerequisites
### 4.1.1 Install SQLite3
- Run `sqlite3` command to check if you already have SQLite3 installed on your machine.
- If not installed already, Install using below command
- on Linux
- on Debian / Ubuntu
```
sudo apt install sqlite3
```
- on CentOS / Fedora / RedHat
```
sudo yum install sqlite3
```
## 4.2 To run ClickHouse setup (recommended for local development)
## 4.1 To run ClickHouse setup (recommended for local development)
- Clone the SigNoz repository and cd into signoz directory,
```

View File

@@ -45,7 +45,7 @@ build-frontend-amd64:
@echo "--> Building frontend docker image for amd64"
@echo "------------------"
@cd $(FRONTEND_DIRECTORY) && \
docker build --file Dockerfile -t $(REPONAME)/$(FRONTEND_DOCKER_IMAGE):$(DOCKER_TAG) \
docker build --file Dockerfile --no-cache -t $(REPONAME)/$(FRONTEND_DOCKER_IMAGE):$(DOCKER_TAG) \
--build-arg TARGETPLATFORM="linux/amd64" .
# Step to build and push docker image of frontend(used in push pipeline)
@@ -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/arm64,linux/amd64 \
docker buildx build --file Dockerfile --progress plane --no-cache --push --platform linux/amd64 \
--tag $(REPONAME)/$(FRONTEND_DOCKER_IMAGE):$(DOCKER_TAG) .
# Steps to build and push docker image of query service
@@ -65,7 +65,7 @@ build-query-service-amd64:
@echo "--> Building query-service docker image for amd64"
@echo "------------------"
@docker build --file $(QUERY_SERVICE_DIRECTORY)/Dockerfile \
-t $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) \
--no-cache -t $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) \
--build-arg TARGETPLATFORM="linux/amd64" --build-arg LD_FLAGS="$(LD_FLAGS)" .
# Step to build and push docker image of query in amd64 and arm64 (used in push pipeline)
@@ -73,7 +73,7 @@ build-push-query-service:
@echo "------------------"
@echo "--> Building and pushing query-service docker image"
@echo "------------------"
@docker buildx build --file $(QUERY_SERVICE_DIRECTORY)/Dockerfile --progress plane \
@docker buildx build --file $(QUERY_SERVICE_DIRECTORY)/Dockerfile --progress plane --no-cache \
--push --platform linux/arm64,linux/amd64 --build-arg LD_FLAGS="$(LD_FLAGS)" \
--tag $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) .
@@ -84,11 +84,11 @@ build-ee-query-service-amd64:
@echo "------------------"
@if [ $(DEV_BUILD) != "" ]; then \
docker build --file $(EE_QUERY_SERVICE_DIRECTORY)/Dockerfile \
-t $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) \
--no-cache -t $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) \
--build-arg TARGETPLATFORM="linux/amd64" --build-arg LD_FLAGS="${LD_FLAGS} ${DEV_LD_FLAGS}" .; \
else \
docker build --file $(EE_QUERY_SERVICE_DIRECTORY)/Dockerfile \
-t $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) \
--no-cache -t $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) \
--build-arg TARGETPLATFORM="linux/amd64" --build-arg LD_FLAGS="$(LD_FLAGS)" .; \
fi
@@ -98,7 +98,7 @@ build-push-ee-query-service:
@echo "--> Building and pushing query-service docker image"
@echo "------------------"
@docker buildx build --file $(EE_QUERY_SERVICE_DIRECTORY)/Dockerfile \
--progress plane --push --platform linux/arm64,linux/amd64 \
--progress plane --no-cache --push --platform linux/arm64,linux/amd64 \
--build-arg LD_FLAGS="$(LD_FLAGS)" --tag $(REPONAME)/$(QUERY_SERVICE_DOCKER_IMAGE):$(DOCKER_TAG) .
dev-setup:
@@ -119,22 +119,16 @@ down-local:
$(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-local.yaml \
down -v
pull-signoz:
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml pull
run-signoz:
run-x86:
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml up --build -d
down-signoz:
down-x86:
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml down -v
clear-standalone-data:
@docker run --rm -v "$(PWD)/$(STANDALONE_DIRECTORY)/data:/pwd" busybox \
sh -c "cd /pwd && rm -rf alertmanager/* clickhouse*/* signoz/* zookeeper-*/*"
sh -c "cd /pwd && rm -rf alertmanager/* clickhous*/* signoz/* zookeeper-*/*"
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/...
sh -c "cd /pwd && rm -rf alertmanager/* clickhous*/* signoz/* zookeeper-*/*"

View File

@@ -23,7 +23,7 @@
##
SigNoz helps developers monitor applications and troubleshoot problems in their deployed applications. With SigNoz, you can:
SigNoz helps developers monitor applications and troubleshoot problems in their deployed applications. SigNoz uses distributed tracing to gain visibility into your software stack.
👉 Visualise Metrics, Traces and Logs in a single pane of glass
@@ -144,8 +144,6 @@ 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>&nbsp </p>
### SigNoz vs Loki
@@ -154,8 +152,6 @@ We have published benchmarks comparing Elastic with SigNoz. Check it out [here](
- 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" />

View File

@@ -27,6 +27,12 @@ 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.

View File

@@ -137,7 +137,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.16.1
image: signoz/query-service:0.13.0
command: ["-config=/root/config/prometheus.yml"]
# ports:
# - "6060:6060" # pprof port
@@ -166,7 +166,7 @@ services:
<<: *clickhouse-depend
frontend:
image: signoz/frontend:0.16.1
image: signoz/frontend:0.13.0
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.4
image: signoz/signoz-otel-collector:0.66.1
command: ["--config=/etc/otel-collector-config.yaml"]
user: root # required for reading docker container logs
volumes:
@@ -188,7 +188,6 @@ 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
@@ -208,7 +207,7 @@ services:
<<: *clickhouse-depend
otel-collector-metrics:
image: signoz/signoz-otel-collector:0.66.4
image: signoz/signoz-otel-collector:0.66.1
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@@ -64,9 +64,7 @@ receivers:
- job_name: otel-collector
static_configs:
- targets:
- localhost:8888
labels:
job_name: otel-collector
- localhost:8888
processors:
batch:
@@ -86,10 +84,6 @@ 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
@@ -110,13 +104,11 @@ 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:
enabled: true
clickhousemetricswrite/prometheus:
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
prometheus:
endpoint: 0.0.0.0:8889
# logging: {}
@@ -155,13 +147,9 @@ service:
processors: [batch]
exporters: [clickhousemetricswrite]
metrics/generic:
receivers: [hostmetrics]
receivers: [hostmetrics, prometheus]
processors: [resourcedetection, batch]
exporters: [clickhousemetricswrite]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [clickhousemetricswrite/prometheus]
metrics/spanmetrics:
receivers: [otlp/spanmetrics]
exporters: [prometheus]

View File

@@ -7,9 +7,7 @@ receivers:
scrape_interval: 60s
static_configs:
- targets:
- localhost:8888
labels:
job_name: otel-collector-metrics
- localhost:8888
# SigNoz span metrics
- job_name: signozspanmetrics-collector
scrape_interval: 60s

View File

@@ -905,8 +905,7 @@
<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_scripts_path>/var/lib/clickhouse/user_scripts/</user_scripts_path>
<user_defined_executable_functions_config>*_function.xml</user_defined_executable_functions_config>
<!-- Uncomment if you want data to be compressed 30-100% better.
Don't do that if you just started using ClickHouse.

View File

@@ -1,21 +0,0 @@
<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>

View File

@@ -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.4
image: signoz/signoz-otel-collector:0.66.1
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.4
image: signoz/signoz-otel-collector:0.66.1
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@@ -97,11 +97,9 @@ 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
@@ -114,12 +112,9 @@ 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
@@ -132,14 +127,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-3/:/var/lib/clickhouse/
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
alertmanager:
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.0-0.2}
image: signoz/alertmanager:0.23.0-0.2
volumes:
- ./data/alertmanager:/data
depends_on:
@@ -153,7 +146,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.16.1}
image: signoz/query-service:0.13.0
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
# ports:
@@ -181,7 +174,7 @@ services:
<<: *clickhouse-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.16.1}
image: signoz/frontend:0.13.0
container_name: frontend
restart: on-failure
depends_on:
@@ -193,7 +186,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.4}
image: signoz/signoz-otel-collector:0.66.1
command: ["--config=/etc/otel-collector-config.yaml"]
user: root # required for reading docker container logs
volumes:
@@ -202,7 +195,6 @@ 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
@@ -219,7 +211,7 @@ services:
<<: *clickhouse-depend
otel-collector-metrics:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.4}
image: signoz/signoz-otel-collector:0.66.1
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
@@ -232,15 +224,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"

View File

@@ -64,10 +64,7 @@ receivers:
- job_name: otel-collector
static_configs:
- targets:
- localhost:8888
labels:
job_name: otel-collector
- localhost:8888
processors:
batch:
@@ -83,10 +80,6 @@ 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
@@ -119,13 +112,11 @@ 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:
enabled: true
clickhousemetricswrite/prometheus:
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
prometheus:
endpoint: 0.0.0.0:8889
# logging: {}
@@ -160,13 +151,9 @@ service:
processors: [batch]
exporters: [clickhousemetricswrite]
metrics/generic:
receivers: [hostmetrics]
receivers: [hostmetrics, prometheus]
processors: [resourcedetection, batch]
exporters: [clickhousemetricswrite]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [clickhousemetricswrite/prometheus]
metrics/spanmetrics:
receivers: [otlp/spanmetrics]
exporters: [prometheus]

View File

@@ -11,9 +11,7 @@ receivers:
scrape_interval: 60s
static_configs:
- targets:
- localhost:8888
labels:
job_name: otel-collector-metrics
- localhost:8888
# SigNoz span metrics
- job_name: signozspanmetrics-collector
scrape_interval: 60s

View File

@@ -1,237 +0,0 @@
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))
}
}

View File

@@ -81,11 +81,6 @@ check_os() {
os="centos"
package_manager="yum"
;;
Rocky*)
desired_os=1
os="centos"
package_manager="yum"
;;
SLES*)
desired_os=1
os="sles"
@@ -228,7 +223,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/health?live=1" || true)"
status_code="$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3301/api/v1/services/list || true)"
if [[ status_code -eq 200 ]]; then
break
else
@@ -516,15 +511,13 @@ 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 in Getting Started?"
echo "👉 Need help 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!"

View File

@@ -1,11 +1,8 @@
package app
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
_ "net/http/pprof" // http profiler
@@ -30,7 +27,6 @@ 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"
@@ -270,82 +266,15 @@ func (lrw *loggingResponseWriter) Flush() {
lrw.ResponseWriter.(http.Flusher).Flush()
}
func extractDashboardMetaData(path string, r *http.Request) (map[string]interface{}, bool) {
pathToExtractBodyFrom := "/api/v2/metrics/query_range"
data := map[string]interface{}{}
var postData *model.QueryRangeParamsV2
if path == pathToExtractBodyFrom && (r.Method == "POST") {
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)
} else {
return nil, false
}
} else {
return nil, false
}
signozMetricNotFound := false
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
}
if signozMetricNotFound {
telemetry.GetInstance().AddActiveMetricsUser()
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_DASHBOARDS_METADATA, data, true)
}
return data, true
}
func getActiveLogs(path string, r *http.Request) {
// if path == "/api/v1/dashboards/{uuid}" {
// telemetry.GetInstance().AddActiveMetricsUser()
// }
if path == "/api/v1/logs" {
hasFilters := len(r.URL.Query().Get("q"))
if hasFilters > 0 {
telemetry.GetInstance().AddActiveLogsUser()
}
}
}
func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
route := mux.CurrentRoute(r)
path, _ := route.GetPathTemplate()
dashboardMetadata, metadataExists := extractDashboardMetaData(path, r)
getActiveLogs(path, r)
lrw := NewLoggingResponseWriter(w)
next.ServeHTTP(lrw, r)
data := map[string]interface{}{"path": path, "statusCode": lrw.statusCode}
if metadataExists {
for key, value := range dashboardMetadata {
data[key] = value
}
}
if _, ok := telemetry.IgnoredPaths()[path]; !ok {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data)

View File

@@ -102,10 +102,9 @@ 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',

View File

@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
cd frontend && yarn run commitlint --edit $1
cd frontend && npm run commitlint

View File

@@ -1 +0,0 @@
network-timeout 600000

View File

@@ -9,9 +9,8 @@ ARG TARGETARCH
WORKDIR /frontend
# Copy the package.json and .yarnrc files prior to install dependencies
# Copy the package.json to install dependencies
COPY package.json ./
COPY .yarnrc ./
# Install the dependencies and make the folder
RUN CI=1 yarn install

View File

@@ -25,7 +25,6 @@ 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'],

View File

@@ -27,12 +27,16 @@
"author": "",
"license": "ISC",
"dependencies": {
"@ant-design/colors": "6.0.0",
"@ant-design/icons": "4.8.0",
"@ant-design/colors": "^6.0.0",
"@ant-design/icons": "^4.6.2",
"@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": "5.0.5",
"antd": "4.19.2",
"axios": "^0.21.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.0",
@@ -40,7 +44,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.9.1",
"chart.js": "^3.4.0",
"chartjs-adapter-date-fns": "^2.0.0",
"chartjs-plugin-annotation": "^1.4.0",
"color": "^4.2.1",
@@ -66,18 +70,17 @@
"less-loader": "^10.2.0",
"lodash-es": "^4.17.21",
"mini-css-extract-plugin": "2.4.5",
"react": "18.2.0",
"react-dom": "18.2.0",
"react": "17.0.0",
"react-dom": "17.0.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-virtuoso": "4.0.3",
"react-vis": "^1.11.7",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"stream": "^0.0.2",
@@ -117,9 +120,7 @@
"@commitlint/config-conventional": "^16.2.4",
"@jest/globals": "^27.5.1",
"@playwright/test": "^1.22.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
"@testing-library/user-event": "14.4.3",
"@testing-library/react-hooks": "^7.0.2",
"@types/color": "^3.0.3",
"@types/compression-webpack-plugin": "^9.0.0",
"@types/copy-webpack-plugin": "^8.0.1",
@@ -131,11 +132,10 @@
"@types/lodash-es": "^4.17.4",
"@types/mini-css-extract-plugin": "^2.5.1",
"@types/node": "^16.10.3",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"@types/react": "^17.0.0",
"@types/react-dom": "^16.9.9",
"@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,7 +145,6 @@
"@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",
@@ -174,9 +173,7 @@
"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",
@@ -189,7 +186,7 @@
]
},
"resolutions": {
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10"
"@types/react": "17.0.0",
"@types/react-dom": "17.0.0"
}
}

10
frontend/public/css/antd.dark.min.css vendored Normal file

File diff suppressed because one or more lines are too long

10
frontend/public/css/antd.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -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,8 +47,6 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const dispatch = useDispatch<Dispatch<AppActions>>();
const { notifications } = useNotifications();
const currentRoute = mapRoutes.get('current');
const navigateToLoginIfNotLoggedIn = (isLoggedIn = isLoggedInState): void => {
@@ -108,7 +106,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
} else {
Logout();
notifications.error({
notification.error({
message: response.error || t('something_went_wrong'),
});
}

View File

@@ -1,9 +1,6 @@
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';
@@ -12,33 +9,29 @@ import PrivateRoute from './Private';
import routes from './routes';
function App(): JSX.Element {
const themeConfig = useThemeConfig();
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}
/>
))}
<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}
/>
);
})}
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</PrivateRoute>
</Router>
</NotificationProvider>
</ConfigProvider>
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppLayout>
</PrivateRoute>
</Router>
);
}

View File

@@ -1,4 +1,4 @@
import { ApiV2Instance as axios } from 'api';
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
@@ -8,7 +8,9 @@ const query = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post(`/variables/query`, props);
const response = await axios.get(
`/variables/query?query=${encodeURIComponent(props.query)}`,
);
return {
statusCode: 200,

View File

@@ -10,11 +10,9 @@ const getSpans = async (
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const updatedSelectedTags = props.selectedTags.map((e) => ({
Key: e.Key,
Key: e.Key[0],
Operator: e.Operator,
StringValues: e.StringValues,
NumberValues: e.NumberValues,
BoolValues: e.BoolValues,
Values: e.Values,
}));
const exclude: string[] = [];
@@ -37,7 +35,7 @@ const getSpans = async (
start: String(props.start),
end: String(props.end),
function: props.function,
groupBy: props.groupBy === 'none' ? '' : props.groupBy,
groupBy: props.groupBy,
step: props.step,
tags: updatedSelectedTags,
...nonDuration,

View File

@@ -28,11 +28,9 @@ const getSpanAggregate = async (
});
const updatedSelectedTags = props.selectedTags.map((e) => ({
Key: e.Key,
Key: e.Key[0],
Operator: e.Operator,
StringValues: e.StringValues,
NumberValues: e.NumberValues,
BoolValues: e.BoolValues,
Values: e.Values,
}));
const other = Object.fromEntries(props.selectedFilter);

View File

@@ -11,11 +11,9 @@ const getTagValue = async (
const response = await axios.post<PayloadProps>(`/getTagValues`, {
start: props.start.toString(),
end: props.end.toString(),
tagKey: {
Key: props.tagKey.Key,
Type: props.tagKey.Type,
},
tagKey: props.tagKey,
});
return {
statusCode: 200,
error: null,

View File

@@ -0,0 +1,9 @@
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;

View File

@@ -1,6 +1,8 @@
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,
@@ -10,7 +12,7 @@ function Editor({
height,
options,
}: MEditorProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
return (
<MEditor
theme={isDarkMode ? 'vs-dark' : 'vs-light'}

View File

@@ -1,321 +0,0 @@
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;
};

View File

@@ -11,7 +11,7 @@ export const emptyGraph = {
ctx.textBaseline = 'middle';
ctx.font = '1.5rem sans-serif';
ctx.fillStyle = `${grey.primary}`;
ctx.fillText('No data', width / 2, height / 2);
ctx.fillText('No data to display', width / 2, height / 2);
ctx.restore();
},
};

View File

@@ -1,164 +0,0 @@
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;
};

View File

@@ -22,86 +22,87 @@ 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> => ({
id: 'htmlLegend',
afterUpdate(chart): void {
const ul = getOrCreateLegendList(chart, id || 'legend', isLonger);
export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => {
return {
id: 'htmlLegend',
afterUpdate(chart): void {
const ul = getOrCreateLegendList(chart, id || 'legend', isLonger);
// 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;
// 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 = '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);
// 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;
// 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);
}
});
},
};
};

View File

@@ -1,20 +0,0 @@
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,
};
}

View File

@@ -1,8 +0,0 @@
import { themeColors } from 'constants/theme';
export const getAxisLabelColor = (currentTheme: string): string => {
if (currentTheme === 'light') {
return themeColors.black;
}
return themeColors.whiteCream;
};

View File

@@ -23,27 +23,14 @@ import {
} from 'chart.js';
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
import annotationPlugin from 'chartjs-plugin-annotation';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import isEqual from 'lodash-es/isEqual';
import React, { memo, useCallback, useEffect, useRef } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
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';
@@ -79,13 +66,9 @@ function Graph({
forceReRender,
staticLine,
containerHeight,
onDragSelect,
dragSelectColor,
}: GraphProps): JSX.Element {
const nearestDatasetIndex = useRef<null | number>(null);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
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
@@ -109,7 +92,7 @@ function Graph({
}
if (chartRef.current !== null) {
const options: CustomChartOptions = {
const options: ChartOptions = {
animation: {
duration: animate ? 200 : 0,
},
@@ -153,10 +136,6 @@ 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 || '';
@@ -166,27 +145,10 @@ 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,
@@ -216,7 +178,6 @@ function Graph({
},
},
type: 'time',
ticks: { color: getAxisLabelColor(currentTheme) },
},
y: {
display: true,
@@ -225,7 +186,6 @@ function Graph({
color: getGridColor(),
},
ticks: {
color: getAxisLabelColor(currentTheme),
// Include a dollar sign in the ticks
callback(value) {
return getYAxisFormattedValue(value.toString(), yAxisUnit);
@@ -241,50 +201,18 @@ 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(createIntersectionCursorPlugin());
chartPlugins.push(createDragSelectPlugin());
} else {
chartPlugins.push(emptyGraph);
}
if (!chartHasData) chartPlugins.push(emptyGraph);
chartPlugins.push(legend(name, data.datasets.length > 3));
lineChartRef.current = new Chart(chartRef.current, {
@@ -307,9 +235,6 @@ function Graph({
yAxisUnit,
onClickHandler,
staticLine,
onDragSelect,
dragSelectColor,
currentTheme,
]);
useEffect(() => {
@@ -324,13 +249,6 @@ function Graph({
);
}
type CustomChartOptions = ChartOptions & {
plugins: {
[dragSelectPluginId]: DragSelectPluginOptions | false;
[intersectionCursorPluginId]: IntersectionCursorPluginOptions | false;
};
};
interface GraphProps {
animate?: boolean;
type: ChartType;
@@ -343,8 +261,6 @@ interface GraphProps {
forceReRender?: boolean | null | number;
staticLine?: StaticLineProps | undefined;
containerHeight?: string | number;
onDragSelect?: (start: number, end: number) => void;
dragSelectColor?: string;
}
export interface StaticLineProps {
@@ -371,11 +287,6 @@ Graph.defaultProps = {
yAxisUnit: undefined,
forceReRender: undefined,
staticLine: undefined,
containerHeight: '90%',
onDragSelect: undefined,
dragSelectColor: undefined,
containerHeight: '85%',
};
export default memo(Graph, (prevProps, nextProps) =>
isEqual(prevProps.data, nextProps.data),
);
export default Graph;

View File

@@ -1,22 +1,14 @@
import { themeColors } from 'constants/theme';
import styled from 'styled-components';
export const LegendsContainer = styled.div`
height: 10%;
height: 15%;
* {
::-webkit-scrollbar {
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};
display: none !important;
}
-ms-overflow-style: none !important; /* IE and Edge */
scrollbar-width: none !important; /* Firefox */
}
`;

View File

@@ -2,9 +2,7 @@ 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';
@@ -16,7 +14,7 @@ function AddToQueryHOC({
const {
searchFilter: { queryString },
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const dispatch = useDispatch<Dispatch<AppActions>>();
const dispatch = useDispatch();
const generatedQuery = useMemo(
() => generateFilterQuery({ fieldKey, fieldValue, type: 'IN' }),
@@ -33,9 +31,7 @@ function AddToQueryHOC({
}
dispatch({
type: SET_SEARCH_QUERY_STRING,
payload: {
searchQueryString: updatedQueryString,
},
payload: updatedQueryString,
});
}, [dispatch, generatedQuery, queryString]);

View File

@@ -1,28 +1,29 @@
import { Popover } from 'antd';
import { useNotifications } from 'hooks/useNotifications';
import React, { useCallback, useEffect } from 'react';
import React from 'react';
import { useCopyToClipboard } from 'react-use';
interface CopyClipboardHOCProps {
textToCopy: string;
children: React.ReactNode;
}
function CopyClipboardHOC({
textToCopy,
children,
}: CopyClipboardHOCProps): JSX.Element {
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]);
const [, setCopy] = useCopyToClipboard();
return (
<span onClick={onClick} onKeyDown={onClick} role="button" tabIndex={0}>
<span
style={{
margin: 0,
padding: 0,
cursor: 'pointer',
}}
onClick={(): void => setCopy(textToCopy)}
onKeyDown={(): void => setCopy(textToCopy)}
role="button"
tabIndex={0}
>
<Popover
placement="top"
content={<span style={{ fontSize: '0.9rem' }}>Copy to clipboard</span>}
@@ -33,9 +34,4 @@ function CopyClipboardHOC({
);
}
interface CopyClipboardHOCProps {
textToCopy: string;
children: React.ReactNode;
}
export default CopyClipboardHOC;

View File

@@ -3,7 +3,6 @@ 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';
@@ -15,7 +14,7 @@ import { ILogsReducer } from 'types/reducer/logs';
import AddToQueryHOC from '../AddToQueryHOC';
import CopyClipboardHOC from '../CopyClipboardHOC';
import { Container, LogContainer, Text, TextContainer } from './styles';
import { Container } from './styles';
import { isValidLogField } from './util';
interface LogFieldProps {
@@ -24,17 +23,21 @@ interface LogFieldProps {
}
function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
return (
<TextContainer>
<Text ellipsis type="secondary">
{fieldKey}
</Text>
<div
style={{
display: 'flex',
overflow: 'hidden',
width: '100%',
}}
>
<Typography.Text type="secondary">{fieldKey}</Typography.Text>
<CopyClipboardHOC textToCopy={fieldValue}>
<Typography.Text ellipsis>
{': '}
{fieldValue}
</Typography.Text>
</CopyClipboardHOC>
</TextContainer>
</div>
);
}
function LogSelectedField({
@@ -80,7 +83,6 @@ 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({
@@ -91,40 +93,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>
<div style={{ maxWidth: '100%' }}>
<div>
{'{'}
<LogContainer>
<>
<LogGeneralField fieldKey="log" fieldValue={flattenLogData.body} />
{flattenLogData.stream && (
<LogGeneralField fieldKey="stream" fieldValue={flattenLogData.stream} />
)}
<div style={{ marginLeft: '0.5rem' }}>
<LogGeneralField
fieldKey="log"
fieldValue={flattenLogData.body as never}
/>
{flattenLogData.stream && (
<LogGeneralField
fieldKey="timestamp"
fieldValue={dayjs((flattenLogData.timestamp as never) / 1e6).format()}
fieldKey="stream"
fieldValue={flattenLogData.stream as never}
/>
</>
</LogContainer>
)}
<LogGeneralField
fieldKey="timestamp"
fieldValue={dayjs((flattenLogData.timestamp as never) / 1e6).format()}
/>
</div>
{'}'}
</div>
<div>
{map(selected, (field) =>
isValidLogField(flattenLogData[field.name] as never) ? (
{map(selected, (field) => {
return 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 }} />

View File

@@ -1,4 +1,4 @@
import { Card, Typography } from 'antd';
import { Card } from 'antd';
import styled, { keyframes } from 'styled-components';
const fadeInAnimation = keyframes`
@@ -16,20 +16,3 @@ 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;
`;

View File

@@ -11,7 +11,7 @@ function CustomModal({
return (
<Modal
title={title}
open={isModalVisible}
visible={isModalVisible}
footer={footer}
closable={closable}
>

View File

@@ -1,3 +1,7 @@
/**
* @jest-environment jsdom
*/
import { render } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';

View File

@@ -48,9 +48,9 @@ function ReleaseNote({ path }: ReleaseNoteProps): JSX.Element | null {
(state) => state.app,
);
const c = allComponentMap.find((item) =>
item.match(path, currentVersion, userFlags),
);
const c = allComponentMap.find((item) => {
return item.match(path, currentVersion, userFlags);
});
if (!c) {
return null;

View File

@@ -1,51 +0,0 @@
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;

View File

@@ -1,51 +0,0 @@
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;

View File

@@ -1 +0,0 @@
export const enableUserSelectHack = { enableUserSelectHack: false };

View File

@@ -1,4 +0,0 @@
import ResizableHeader from './ResizableHeader';
import ResizeTable from './ResizeTable';
export { ResizableHeader, ResizeTable };

View File

@@ -1,11 +0,0 @@
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;
`;

View File

@@ -4,9 +4,9 @@ import React from 'react';
import { SpinerStyle } from './styles';
function Spinner({ size, tip, height, style }: SpinnerProps): JSX.Element {
function Spinner({ size, tip, height }: SpinnerProps): JSX.Element {
return (
<SpinerStyle height={height} style={style}>
<SpinerStyle height={height}>
<Spin spinning size={size} tip={tip} indicator={<LoadingOutlined spin />} />
</SpinerStyle>
);
@@ -16,13 +16,11 @@ 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;

View File

@@ -6,16 +6,18 @@ import React from 'react';
function TextToolTip({ text, url }: TextToolTipProps): JSX.Element {
return (
<Tooltip
overlay={(): JSX.Element => (
<div>
{`${text} `}
{url && (
<a href={url} rel="noopener noreferrer" target="_blank">
here
</a>
)}
</div>
)}
overlay={(): JSX.Element => {
return (
<div>
{`${text} `}
{url && (
<a href={url} rel="noopener noreferrer" target="_blank">
here
</a>
)}
</div>
);
}}
>
<QuestionCircleFilled style={{ fontSize: '1.3125rem' }} />
</Tooltip>

View File

@@ -8,6 +8,7 @@ export const TextContainer = styled.div<TextContainerProps>`
display: flex;
> button {
margin-left: ${({ noButtonMargin }): string =>
noButtonMargin ? '0' : '0.5rem'}
margin-left: ${({ noButtonMargin }): string => {
return noButtonMargin ? '0' : '0.5rem';
}}
`;

View File

@@ -3,5 +3,4 @@ export enum LOCALSTORAGE {
IS_LOGGED_IN = 'IS_LOGGED_IN',
AUTH_TOKEN = 'AUTH_TOKEN',
REFRESH_AUTH_TOKEN = 'REFRESH_AUTH_TOKEN',
THEME = 'THEME',
}

View File

@@ -8,11 +8,11 @@ export const OperatorConversions: Array<{
{
label: 'IN',
metricValue: '=~',
traceValue: 'In',
traceValue: 'in',
},
{
label: 'Not IN',
metricValue: '!~',
traceValue: 'NotIn',
traceValue: 'not in',
},
];

View File

@@ -1,43 +0,0 @@
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 };

View File

@@ -1,10 +1,8 @@
/* eslint-disable react/display-name */
import { Button } from 'antd';
import { Button, notification, Table } 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';
@@ -18,7 +16,7 @@ import Delete from './Delete';
function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
const { t } = useTranslation(['channels']);
const { notifications } = useNotifications();
const [notifications, Element] = notification.useNotification();
const [channels, setChannels] = useState<Channels[]>(allChannels);
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [action] = useComponentPermission(['new_alert_action'], role);
@@ -36,13 +34,11 @@ 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,
},
];
@@ -52,7 +48,6 @@ 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">
@@ -64,7 +59,13 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
});
}
return <ResizeTable columns={columns} dataSource={channels} rowKey="id" />;
return (
<>
{Element}
<Table rowKey="id" dataSource={channels} columns={columns} />
</>
);
}
interface AlertChannelsProps {

View File

@@ -1,5 +1,5 @@
import { Button } from 'antd';
import { NotificationInstance } from 'antd/es/notification/interface';
import { NotificationInstance } from 'antd/lib/notification';
import deleteChannel from 'api/channels/delete';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';

View File

@@ -3,21 +3,20 @@ import {
Button,
Card,
Input,
notification,
Space,
Table,
TableProps,
Tooltip,
Typography,
} from 'antd';
import { ColumnType, TablePaginationConfig } from 'antd/es/table';
import { FilterValue, SorterResult } from 'antd/es/table/interface';
import { ColumnType } from 'antd/es/table';
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';
@@ -31,7 +30,6 @@ 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,
@@ -110,32 +108,22 @@ function AllErrors(): JSX.Element {
enabled: !loading,
},
{
queryKey: [
'getErrorCounts',
maxTime,
minTime,
getUpdatedExceptionType,
getUpdatedServiceName,
],
queryKey: ['getErrorCounts', maxTime, minTime],
queryFn: (): Promise<ErrorResponse | SuccessResponse<number>> =>
getErrorCounts({
end: maxTime,
start: minTime,
exceptionType: getUpdatedExceptionType,
serviceName: getUpdatedServiceName,
}),
enabled: !loading,
},
]);
const { notifications } = useNotifications();
useEffect(() => {
if (data?.error) {
notifications.error({
notification.error({
message: data.error || t('something_went_wrong'),
});
}
}, [data?.error, data?.payload, t, notifications]);
}, [data?.error, data?.payload, t]);
const getDateValue = (value: string): JSX.Element => (
<Typography>{dayjs(value).format('DD/MM/YYYY HH:mm:ss A')}</Typography>
@@ -179,45 +167,41 @@ function AllErrors(): JSX.Element {
);
const filterDropdownWrapper = useCallback(
({
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>
),
({ 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>
);
},
[getUpdatedExceptionType, getUpdatedServiceName, handleSearch],
);
const onExceptionTypeFilter: ColumnType<Exception>['onFilter'] = useCallback(
(value: unknown, record: Exception): boolean => {
const onExceptionTypeFilter = useCallback(
(value, record: Exception): boolean => {
if (record.exceptionType && typeof value === 'string') {
return record.exceptionType.toLowerCase().includes(value.toLowerCase());
}
@@ -227,7 +211,7 @@ function AllErrors(): JSX.Element {
);
const onApplicationTypeFilter = useCallback(
(value: unknown, record: Exception): boolean => {
(value, record: Exception): boolean => {
if (record.serviceName && typeof value === 'string') {
return record.serviceName.toLowerCase().includes(value.toLowerCase());
}
@@ -259,7 +243,6 @@ function AllErrors(): JSX.Element {
const columns: ColumnsType<Exception> = [
{
title: 'Exception Type',
width: 100,
dataIndex: 'exceptionType',
key: 'exceptionType',
...getFilter(onExceptionTypeFilter, 'Search By Exception', 'exceptionType'),
@@ -285,7 +268,6 @@ function AllErrors(): JSX.Element {
title: 'Error Message',
dataIndex: 'exceptionMessage',
key: 'exceptionMessage',
width: 100,
render: (value): JSX.Element => (
<Tooltip overlay={(): JSX.Element => value}>
<Typography.Paragraph
@@ -300,7 +282,6 @@ function AllErrors(): JSX.Element {
},
{
title: 'Count',
width: 50,
dataIndex: 'exceptionCount',
key: 'exceptionCount',
sorter: true,
@@ -313,7 +294,6 @@ function AllErrors(): JSX.Element {
{
title: 'Last Seen',
dataIndex: 'lastSeen',
width: 80,
key: 'lastSeen',
render: getDateValue,
sorter: true,
@@ -326,7 +306,6 @@ function AllErrors(): JSX.Element {
{
title: 'First Seen',
dataIndex: 'firstSeen',
width: 80,
key: 'firstSeen',
render: getDateValue,
sorter: true,
@@ -339,7 +318,6 @@ function AllErrors(): JSX.Element {
{
title: 'Application',
dataIndex: 'serviceName',
width: 100,
key: 'serviceName',
sorter: true,
defaultSortOrder: getDefaultOrder(
@@ -356,11 +334,7 @@ function AllErrors(): JSX.Element {
];
const onChangeHandler: TableProps<Exception>['onChange'] = useCallback(
(
paginations: TablePaginationConfig,
filters: Record<string, FilterValue | null>,
sorter: SorterResult<Exception>[] | SorterResult<Exception>,
) => {
(paginations, filters, sorter) => {
if (!Array.isArray(sorter)) {
const { pageSize = 0, current = 0 } = paginations;
const { columnKey = '', order } = sorter;
@@ -386,10 +360,10 @@ function AllErrors(): JSX.Element {
);
return (
<ResizeTable
columns={columns}
<Table
tableLayout="fixed"
dataSource={data?.payload as Exception[]}
columns={columns}
rowKey="firstSeen"
loading={isLoading || false || errorCountResponse.status === 'loading'}
pagination={{

View File

@@ -1,9 +0,0 @@
import { FilterDropdownProps } from 'antd/es/table/interface';
export interface FilterDropdownExtendsProps {
placeholder: string;
filterKey: string;
confirm: FilterDropdownProps['confirm'];
setSelectedKeys: FilterDropdownProps['setSelectedKeys'];
selectedKeys: FilterDropdownProps['selectedKeys'];
}

View File

@@ -20,14 +20,15 @@ export const urlKey = {
serviceName: 'serviceName',
};
export const isOrderParams = (orderBy: string | null): orderBy is OrderBy =>
!!(
export const isOrderParams = (orderBy: string | null): orderBy is OrderBy => {
return !!(
orderBy === 'serviceName' ||
orderBy === 'exceptionCount' ||
orderBy === 'lastSeen' ||
orderBy === 'firstSeen' ||
orderBy === 'exceptionType'
);
};
export const getOrder = (order: string | null): Order => {
if (isOrder(order)) {
@@ -81,9 +82,12 @@ export const getDefaultOrder = (
return undefined;
};
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 getNanoSeconds = (date: string): string => {
return (
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) {

View File

@@ -1,3 +1,4 @@
import { notification } from 'antd';
import getDynamicConfigs from 'api/dynamicConfigs/getDynamicConfigs';
import getFeaturesFlags from 'api/features/getFeatureFlags';
import getUserLatestVersion from 'api/user/getLatestVersion';
@@ -5,7 +6,6 @@ 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,8 +91,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const latestVersionCounter = useRef(0);
const latestConfigCounter = useRef(0);
const { notifications } = useNotifications();
useEffect(() => {
if (
getUserLatestVersionResponse.isFetched &&
@@ -107,7 +105,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isError: true,
},
});
notifications.error({
notification.error({
message: t('oops_something_went_wrong_version'),
});
}
@@ -125,7 +123,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isError: true,
},
});
notifications.error({
notification.error({
message: t('oops_something_went_wrong_version'),
});
}
@@ -221,7 +219,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
getDynamicConfigsResponse.data,
getDynamicConfigsResponse.isFetched,
getDynamicConfigsResponse.isSuccess,
notifications,
]);
const isToDisplayLayout = isLoggedIn;

View File

@@ -3,10 +3,9 @@ import styled from 'styled-components';
export const Layout = styled(LayoutComponent)`
&&& {
min-height: 92vh;
display: flex;
position: relative;
min-height: calc(100vh - 4rem);
overflow: hidden;
}
`;

View File

@@ -1,12 +1,13 @@
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(
@@ -14,10 +15,10 @@ function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
[config.components],
);
const isDarkMode = useIsDarkMode();
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
return (
<Menu>
<Menu.ItemGroup>
{sortedConfig.map((item) => {
const iconName = `${isDarkMode ? item.darkIcon : item.lightIcon}`;
@@ -27,19 +28,19 @@ function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
return (
<ErrorLink key={item.text + item.href}>
<Suspense fallback={<Spinner height="5vh" />}>
<MenuItem>
<Menu.Item>
<LinkContainer href={item.href}>
<Space size="small" align="start">
<Component />
{item.text}
</Space>
</LinkContainer>
</MenuItem>
</Menu.Item>
</Suspense>
</ErrorLink>
);
})}
</Menu>
</Menu.ItemGroup>
);
}

View File

@@ -1,10 +0,0 @@
import { Menu } from 'antd';
import styled from 'styled-components';
export const MenuItem = styled(Menu.Item)`
&&& {
height: 1.75rem;
display: flex;
align-items: center;
}
`;

View File

@@ -5,7 +5,6 @@ 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';
@@ -16,8 +15,9 @@ import HelpToolTip from './Config';
function DynamicConfigDropdown({
frontendId,
}: DynamicConfigDropdownProps): JSX.Element {
const { configs } = useSelector<AppState, AppReducer>((state) => state.app);
const isDarkMode = useIsDarkMode();
const { configs, isDarkMode } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
const [isHelpDropDownOpen, setIsHelpDropDownOpen] = useState<boolean>(false);
const config = useMemo(

View File

@@ -1,4 +1,4 @@
import { Form } from 'antd';
import { Form, notification } from 'antd';
import createPagerApi from 'api/channels/createPager';
import createSlackApi from 'api/channels/createSlack';
import createWebhookApi from 'api/channels/createWebhook';
@@ -7,7 +7,6 @@ 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';
@@ -59,7 +58,7 @@ function CreateAlertChannels({
});
const [savingState, setSavingState] = useState<boolean>(false);
const [testingState, setTestingState] = useState<boolean>(false);
const { notifications } = useNotifications();
const [notifications, NotificationElement] = notification.useNotification();
const [type, setType] = useState<ChannelType>(preType);
const onTypeChangeHandler = useCallback(
@@ -79,17 +78,16 @@ function CreateAlertChannels({
[type, selectedConfig],
);
const prepareSlackRequest = useCallback(
() => ({
const prepareSlackRequest = useCallback(() => {
return {
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);
@@ -337,6 +335,7 @@ function CreateAlertChannels({
onSaveHandler,
savingState,
testingState,
NotificationElement,
title: t('page_title_create'),
initialValue: {
type,

View File

@@ -6,16 +6,6 @@ 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: {
@@ -48,7 +38,9 @@ export const alertDefaults: AlertDef = {
labels: {
severity: 'warning',
},
annotations: defaultAnnotations,
annotations: {
description: 'A new alert',
},
evalWindow: defaultEvalWindow,
};
@@ -93,7 +85,9 @@ export const logAlertDefaults: AlertDef = {
severity: 'warning',
details: `${window.location.protocol}//${window.location.host}/logs`,
},
annotations: defaultAnnotations,
annotations: {
description: 'A new log-based alert',
},
evalWindow: defaultEvalWindow,
};
@@ -138,7 +132,9 @@ export const traceAlertDefaults: AlertDef = {
severity: 'warning',
details: `${window.location.protocol}//${window.location.host}/traces`,
},
annotations: defaultAnnotations,
annotations: {
description: 'A new trace-based alert',
},
evalWindow: defaultEvalWindow,
};
@@ -183,6 +179,8 @@ export const exceptionAlertDefaults: AlertDef = {
severity: 'warning',
details: `${window.location.protocol}//${window.location.host}/exceptions`,
},
annotations: defaultAnnotations,
annotations: {
description: 'A new exceptions-based alert',
},
evalWindow: defaultEvalWindow,
};

View File

@@ -1,4 +1,4 @@
import { Form } from 'antd';
import { Form, notification } from 'antd';
import editPagerApi from 'api/channels/editPager';
import editSlackApi from 'api/channels/editSlack';
import editWebhookApi from 'api/channels/editWebhook';
@@ -17,7 +17,6 @@ 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';
@@ -37,7 +36,7 @@ function EditAlertChannels({
});
const [savingState, setSavingState] = useState<boolean>(false);
const [testingState, setTestingState] = useState<boolean>(false);
const { notifications } = useNotifications();
const [notifications, NotificationElement] = notification.useNotification();
const { id } = useParams<{ id: string }>();
const [type, setType] = useState<ChannelType>(
@@ -48,8 +47,8 @@ function EditAlertChannels({
setType(value as ChannelType);
}, []);
const prepareSlackRequest = useCallback(
() => ({
const prepareSlackRequest = useCallback(() => {
return {
api_url: selectedConfig?.api_url || '',
channel: selectedConfig?.channel || '',
name: selectedConfig?.name || '',
@@ -57,9 +56,8 @@ function EditAlertChannels({
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
id,
}),
[id, selectedConfig],
);
};
}, [id, selectedConfig]);
const onSlackEditHandler = useCallback(async () => {
setSavingState(true);
@@ -145,8 +143,8 @@ function EditAlertChannels({
setSavingState(false);
}, [prepareWebhookRequest, t, notifications, selectedConfig]);
const preparePagerRequest = useCallback(
() => ({
const preparePagerRequest = useCallback(() => {
return {
name: selectedConfig.name || '',
routing_key: selectedConfig.routing_key,
client: selectedConfig.client,
@@ -159,9 +157,8 @@ function EditAlertChannels({
details: selectedConfig.details,
detailsArray: JSON.parse(selectedConfig.details || '{}'),
id,
}),
[id, selectedConfig],
);
};
}, [id, selectedConfig]);
const onPagerEditHandler = useCallback(async () => {
setSavingState(true);
@@ -282,6 +279,7 @@ function EditAlertChannels({
onSaveHandler,
testingState,
savingState,
NotificationElement,
title: t('page_title_edit'),
initialValue,
editing: true,

View File

@@ -1,10 +1,8 @@
import { Button, Divider, Space, Typography } from 'antd';
import { Button, Divider, notification, Space, Table, 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';
@@ -55,14 +53,12 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
() => [
{
title: 'Key',
width: 100,
dataIndex: 'key',
key: 'key',
},
{
title: 'Value',
dataIndex: 'value',
width: 100,
key: 'value',
},
],
@@ -81,15 +77,13 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
[],
);
const { notifications } = useNotifications();
const onClickErrorIdHandler = async (
id: string,
timestamp: string,
): Promise<void> => {
try {
if (id.length === 0) {
notifications.error({
notification.error({
message: 'Error Id cannot be empty',
});
return;
@@ -101,7 +95,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
}&timestamp=${getNanoSeconds(timestamp)}&errorId=${id}`,
);
} catch (error) {
notifications.error({
notification.error({
message: t('something_went_wrong'),
});
}
@@ -173,7 +167,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
<EditorContainer>
<Space direction="vertical">
<ResizeTable columns={columns} tableLayout="fixed" dataSource={data} />
<Table tableLayout="fixed" columns={columns} dataSource={data} />
</Space>
</EditorContainer>
</>

View File

@@ -1,4 +1,5 @@
import { Form, Input, Select } from 'antd';
import { Input, Select } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import { LabelFilterStatement } from 'container/CreateAlertChannels/config';
import React from 'react';
@@ -9,7 +10,7 @@ const { Option } = Select;
// point
function LabelFilterForm({ setFilter }: LabelFilterProps): JSX.Element {
return (
<Form.Item name="label_filter" label="Notify When (Optional)">
<FormItem name="label_filter" label="Notify When (Optional)">
<Input.Group compact>
<Select
defaultValue="Severity"
@@ -50,7 +51,7 @@ function LabelFilterForm({ setFilter }: LabelFilterProps): JSX.Element {
}}
/>
</Input.Group>
</Form.Item>
</FormItem>
);
}

View File

@@ -1,4 +1,5 @@
import { Form, Input } from 'antd';
import { Input } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import React from 'react';
import { useTranslation } from 'react-i18next';
@@ -10,7 +11,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
const { t } = useTranslation('channels');
return (
<>
<Form.Item name="routing_key" label={t('field_pager_routing_key')} required>
<FormItem name="routing_key" label={t('field_pager_routing_key')} required>
<Input
onChange={(event): void => {
setSelectedConfig((value) => ({
@@ -19,9 +20,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}));
}}
/>
</Form.Item>
</FormItem>
<Form.Item
<FormItem
name="description"
help={t('help_pager_description')}
label={t('field_pager_description')}
@@ -37,9 +38,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}
placeholder={t('placeholder_pager_description')}
/>
</Form.Item>
</FormItem>
<Form.Item
<FormItem
name="severity"
help={t('help_pager_severity')}
label={t('field_pager_severity')}
@@ -52,9 +53,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}))
}
/>
</Form.Item>
</FormItem>
<Form.Item
<FormItem
name="details"
help={t('help_pager_details')}
label={t('field_pager_details')}
@@ -68,9 +69,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}))
}
/>
</Form.Item>
</FormItem>
<Form.Item
<FormItem
name="component"
help={t('help_pager_component')}
label={t('field_pager_component')}
@@ -83,9 +84,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}))
}
/>
</Form.Item>
</FormItem>
<Form.Item
<FormItem
name="group"
help={t('help_pager_group')}
label={t('field_pager_group')}
@@ -98,9 +99,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}))
}
/>
</Form.Item>
</FormItem>
<Form.Item
<FormItem
name="class"
help={t('help_pager_class')}
label={t('field_pager_class')}
@@ -113,8 +114,8 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}))
}
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
name="client"
help={t('help_pager_client')}
label={t('field_pager_client')}
@@ -127,9 +128,9 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}))
}
/>
</Form.Item>
</FormItem>
<Form.Item
<FormItem
name="client_url"
help={t('help_pager_client_url')}
label={t('field_pager_client_url')}
@@ -142,7 +143,7 @@ function PagerForm({ setSelectedConfig }: PagerFormProps): JSX.Element {
}))
}
/>
</Form.Item>
</FormItem>
</>
);
}

View File

@@ -1,4 +1,5 @@
import { Form, Input } from 'antd';
import { Input } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import React from 'react';
import { useTranslation } from 'react-i18next';
@@ -11,7 +12,7 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
return (
<>
<Form.Item name="api_url" label={t('field_webhook_url')}>
<FormItem name="api_url" label={t('field_webhook_url')}>
<Input
onChange={(event): void => {
setSelectedConfig((value) => ({
@@ -20,9 +21,9 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
}));
}}
/>
</Form.Item>
</FormItem>
<Form.Item
<FormItem
name="channel"
help={t('slack_channel_help')}
label={t('field_slack_recipient')}
@@ -35,9 +36,9 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
}))
}
/>
</Form.Item>
</FormItem>
<Form.Item name="title" label={t('field_slack_title')}>
<FormItem 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 }}`}
@@ -48,9 +49,9 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
}))
}
/>
</Form.Item>
</FormItem>
<Form.Item name="text" label={t('field_slack_description')}>
<FormItem name="text" label={t('field_slack_description')}>
<TextArea
onChange={(event): void =>
setSelectedConfig((value) => ({
@@ -60,7 +61,7 @@ function Slack({ setSelectedConfig }: SlackProps): JSX.Element {
}
placeholder={t('placeholder_slack_description')}
/>
</Form.Item>
</FormItem>
</>
);
}

View File

@@ -1,4 +1,5 @@
import { Form, Input } from 'antd';
import { Input } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import React from 'react';
import { useTranslation } from 'react-i18next';
@@ -9,7 +10,7 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
return (
<>
<Form.Item name="api_url" label={t('field_webhook_url')}>
<FormItem name="api_url" label={t('field_webhook_url')}>
<Input
onChange={(event): void => {
setSelectedConfig((value) => ({
@@ -18,8 +19,8 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
}));
}}
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
name="username"
label={t('field_webhook_username')}
help={t('help_webhook_username')}
@@ -32,8 +33,8 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
}));
}}
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
name="password"
label="Password (optional)"
help={t('help_webhook_password')}
@@ -47,7 +48,7 @@ function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
}));
}}
/>
</Form.Item>
</FormItem>
</>
);
}

View File

@@ -1,4 +1,5 @@
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 {
@@ -31,6 +32,7 @@ function FormAlertChannels({
onSaveHandler,
savingState,
testingState,
NotificationElement,
title,
initialValue,
editing = false,
@@ -52,10 +54,12 @@ function FormAlertChannels({
};
return (
<>
{NotificationElement}
<Title level={3}>{title}</Title>
<Form initialValues={initialValue} layout="vertical" form={formInstance}>
<Form.Item label={t('field_channel_name')} labelAlign="left" name="name">
<FormItem label={t('field_channel_name')} labelAlign="left" name="name">
<Input
disabled={editing}
onChange={(event): void => {
@@ -65,9 +69,9 @@ function FormAlertChannels({
}));
}}
/>
</Form.Item>
</FormItem>
<Form.Item label={t('field_channel_type')} labelAlign="left" name="type">
<FormItem label={t('field_channel_type')} labelAlign="left" name="type">
<Select disabled={editing} onChange={onTypeChangeHandler} value={type}>
<Option value="slack" key="slack">
Slack
@@ -79,11 +83,11 @@ function FormAlertChannels({
Pagerduty
</Option>
</Select>
</Form.Item>
</FormItem>
<Form.Item>{renderSettings()}</Form.Item>
<FormItem>{renderSettings()}</FormItem>
<Form.Item>
<FormItem>
<Button
disabled={savingState}
loading={savingState}
@@ -106,7 +110,7 @@ function FormAlertChannels({
>
{t('button_return')}
</Button>
</Form.Item>
</FormItem>
</Form>
</>
);
@@ -123,6 +127,10 @@ 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

View File

@@ -1,4 +1,5 @@
import { Form, Select } from 'antd';
import { Select } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { AlertDef, Labels } from 'types/api/alerts/def';
@@ -30,7 +31,7 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
<>
<StepHeading> {t('alert_form_step3')} </StepHeading>
<FormContainer>
<Form.Item
<FormItem
label={t('field_severity')}
labelAlign="left"
name={['labels', 'severity']}
@@ -53,9 +54,9 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
<Option value="warning">{t('option_warning')}</Option>
<Option value="info">{t('option_info')}</Option>
</SeveritySelect>
</Form.Item>
</FormItem>
<Form.Item label={t('field_alert_name')} labelAlign="left" name="alert">
<FormItem label={t('field_alert_name')} labelAlign="left" name="alert">
<InputSmall
onChange={(e): void => {
setAlertDef({
@@ -64,8 +65,8 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
});
}}
/>
</Form.Item>
<Form.Item
</FormItem>
<FormItem
label={t('field_alert_desc')}
labelAlign="left"
name={['annotations', 'description']}
@@ -81,7 +82,7 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
});
}}
/>
</Form.Item>
</FormItem>
<FormItemMedium label={t('field_labels')}>
<LabelSelect
onSetLabels={(l: Labels): void => {

View File

@@ -32,8 +32,6 @@ 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 => ({
...src,
name: 'A',
rawQuery: src.query,
});
export const toIClickHouseQuery = (src: IChQuery): IClickHouseQuery => {
return { ...src, name: 'A', rawQuery: src.query };
};

View File

@@ -1,7 +1,6 @@
import { Select } from 'antd';
import { notification, 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';
@@ -21,14 +20,12 @@ function ChannelSelect({
const { loading, payload, error, errorMessage } = useFetch(getChannels);
const { notifications } = useNotifications();
const handleChange = (value: string[]): void => {
onSelectChannels(value);
};
if (error && errorMessage !== '') {
notifications.error({
notification.error({
message: 'Error',
description: errorMessage,
});

View File

@@ -1,12 +1,11 @@
import { PlusOutlined } from '@ant-design/icons';
import { Button, Tabs } from 'antd';
import { Button, notification, Tabs } from 'antd';
import MetricsBuilderFormula from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/formula';
import MetricsBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/query';
import {
IQueryBuilderFormulaHandleChange,
IQueryBuilderQueryHandleChange,
} from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/types';
import { useNotifications } from 'hooks/useNotifications';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { AlertTypes } from 'types/api/alerts/alertTypes';
@@ -164,11 +163,10 @@ function QuerySection({
...allQueries,
});
};
const { notifications } = useNotifications();
const addMetricQuery = useCallback(() => {
if (Object.keys(metricQueries).length > 5) {
notifications.error({
notification.error({
message: t('metric_query_max_limit'),
});
return;
@@ -193,7 +191,7 @@ function QuerySection({
expression: queryLabel,
};
setMetricQueries({ ...queries });
}, [t, getNextQueryLabel, metricQueries, setMetricQueries, notifications]);
}, [t, getNextQueryLabel, metricQueries, setMetricQueries]);
const addFormula = useCallback(() => {
// defaulting to F1 as only one formula is supported
@@ -213,70 +211,78 @@ function QuerySection({
setFormulaQueries({ ...formulas });
}, [formulaQueries, setFormulaQueries]);
const renderPromqlUI = (): JSX.Element => (
<PromqlSection promQueries={promQueries} setPromQueries={setPromQueries} />
);
const renderPromqlUI = (): JSX.Element => {
return (
<PromqlSection promQueries={promQueries} setPromQueries={setPromQueries} />
);
};
const renderChQueryUI = (): JSX.Element => (
<ChQuerySection chQueries={chQueries} setChQueries={setChQueries} />
);
const renderChQueryUI = (): JSX.Element => {
return <ChQuerySection chQueries={chQueries} setChQueries={setChQueries} />;
};
const renderFormulaButton = (): JSX.Element => (
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
{t('button_formula')}
</QueryButton>
);
const renderFormulaButton = (): JSX.Element => {
return (
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
{t('button_formula')}
</QueryButton>
);
};
const renderQueryButton = (): JSX.Element => (
<QueryButton onClick={addMetricQuery} icon={<PlusOutlined />}>
{t('button_query')}
</QueryButton>
);
const renderQueryButton = (): JSX.Element => {
return (
<QueryButton onClick={addMetricQuery} icon={<PlusOutlined />}>
{t('button_query')}
</QueryButton>
);
};
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) => {
const renderMetricUI = (): JSX.Element => {
return (
<div>
{metricQueries &&
Object.keys(metricQueries).map((key: string) => {
// todo(amol): need to handle this in fetch
const current = formulaQueries[key];
const current = metricQueries[key];
current.name = key;
return (
<MetricsBuilderFormula
<MetricsBuilder
key={key}
formulaIndex={key}
formulaData={current}
handleFormulaChange={handleFormulaChange}
queryIndex={key}
queryData={toIMetricsBuilderQuery(current)}
selectedGraph="TIME_SERIES"
handleQueryChange={handleMetricQueryChange}
/>
);
})}
{queryCategory === EQueryType.QUERY_BUILDER &&
(!formulaQueries || Object.keys(formulaQueries).length === 0) &&
metricQueries &&
Object.keys(metricQueries).length > 0 &&
renderFormulaButton()}
{queryCategory !== EQueryType.PROM && renderQueryButton()}
<div style={{ marginTop: '1rem' }}>
{formulaQueries &&
Object.keys(formulaQueries).map((key: string) => {
// todo(amol): need to handle this in fetch
const current = formulaQueries[key];
current.name = key;
return (
<MetricsBuilderFormula
key={key}
formulaIndex={key}
formulaData={current}
handleFormulaChange={handleFormulaChange}
/>
);
})}
{queryCategory === EQueryType.QUERY_BUILDER &&
(!formulaQueries || Object.keys(formulaQueries).length === 0) &&
metricQueries &&
Object.keys(metricQueries).length > 0 &&
renderFormulaButton()}
</div>
</div>
</div>
);
);
};
const handleRunQuery = (): void => {
runQuery();

View File

@@ -1,4 +1,5 @@
import { Form, Select, Typography } from 'antd';
import { Select, Typography } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -17,7 +18,6 @@ import {
} from './styles';
const { Option } = Select;
const FormItem = Form.Item;
function RuleOptions({
alertDef,
@@ -38,94 +38,106 @@ function RuleOptions({
});
};
const renderCompareOps = (): JSX.Element => (
<InlineSelect
defaultValue={defaultCompareOp}
value={alertDef.condition?.op}
style={{ minWidth: '120px' }}
onChange={(value: string | unknown): void => {
const newOp = (value as string) || '';
const renderCompareOps = (): JSX.Element => {
return (
<InlineSelect
defaultValue={defaultCompareOp}
value={alertDef.condition?.op}
style={{ minWidth: '120px' }}
onChange={(value: string | unknown): void => {
const newOp = (value as string) || '';
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
op: newOp,
},
});
}}
>
<Option value="1">{t('option_above')}</Option>
<Option value="2">{t('option_below')}</Option>
<Option value="3">{t('option_equal')}</Option>
<Option value="4">{t('option_notequal')}</Option>
</InlineSelect>
);
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 => (
<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 => {
return (
<InlineSelect
defaultValue={defaultMatchType}
style={{ minWidth: '130px' }}
value={alertDef.condition?.matchType}
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
>
<Option value="1">{t('option_atleastonce')}</Option>
<Option value="2">{t('option_allthetimes')}</Option>
<Option value="3">{t('option_onaverage')}</Option>
<Option value="4">{t('option_intotal')}</Option>
</InlineSelect>
);
};
const renderPromMatchOpts = (): JSX.Element => (
<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 => {
return (
<InlineSelect
defaultValue={defaultMatchType}
style={{ minWidth: '130px' }}
value={alertDef.condition?.matchType}
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
>
<Option value="1">{t('option_atleastonce')}</Option>
</InlineSelect>
);
};
const renderEvalWindows = (): JSX.Element => (
<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 => {
return (
<InlineSelect
defaultValue={defaultEvalWindow}
style={{ minWidth: '120px' }}
value={alertDef.evalWindow}
onChange={(value: string | unknown): void => {
const ew = (value as string) || alertDef.evalWindow;
setAlertDef({
...alertDef,
evalWindow: ew,
});
}}
>
{' '}
<Option value="5m0s">{t('option_5min')}</Option>
<Option value="10m0s">{t('option_10min')}</Option>
<Option value="15m0s">{t('option_15min')}</Option>
<Option value="60m0s">{t('option_60min')}</Option>
<Option value="4h0m0s">{t('option_4hours')}</Option>
<Option value="24h0m0s">{t('option_24hours')}</Option>
</InlineSelect>
);
};
const renderThresholdRuleOpts = (): JSX.Element => (
<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>
);
const renderThresholdRuleOpts = (): JSX.Element => {
return (
<FormItem>
<Typography.Text>
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
{renderThresholdMatchOpts()} {t('text_condition3')} {renderEvalWindows()}
</Typography.Text>
</FormItem>
);
};
const renderPromRuleOptions = (): JSX.Element => {
return (
<FormItem>
<Typography.Text>
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
{renderPromMatchOpts()}
</Typography.Text>
</FormItem>
);
};
return (
<>

View File

@@ -15,130 +15,154 @@ function UserGuide({ queryType }: UserGuideProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('alerts');
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 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 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 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 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 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 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 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 renderGuideForPQL = (): JSX.Element => (
<>
{renderStep1PQL()}
{renderStep2PQL()}
{renderStep3PQL()}
</>
);
const renderGuideForPQL = (): JSX.Element => {
return (
<>
{renderStep1PQL()}
{renderStep2PQL()}
{renderStep3PQL()}
</>
);
};
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 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 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 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 renderGuideForCH = (): JSX.Element => (
<>
{renderStep1CH()}
{renderStep2CH()}
{renderStep3CH()}
</>
);
const renderGuideForCH = (): JSX.Element => {
return (
<>
{renderStep1CH()}
{renderStep2CH()}
{renderStep3CH()}
</>
);
};
return (
<StyledMainContainer>
<Row>

View File

@@ -1,11 +1,10 @@
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
import { Col, FormInstance, Modal, Typography } from 'antd';
import { FormInstance, Modal, notification, 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';
@@ -35,6 +34,7 @@ import {
MainFormContainer,
PanelContainer,
StyledLeftContainer,
StyledRightContainer,
} from './styles';
import useDebounce from './useDebounce';
import UserGuide from './UserGuide';
@@ -191,14 +191,12 @@ 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) {
notifications.error({
notification.error({
message: 'Error',
description: t('promql_required'),
});
@@ -207,7 +205,7 @@ function FormAlertRules({
Object.keys(promQueries).forEach((key) => {
if (promQueries[key].query === '') {
notifications.error({
notification.error({
message: 'Error',
description: t('promql_required'),
});
@@ -216,14 +214,14 @@ function FormAlertRules({
});
return retval;
}, [t, promQueries, queryCategory, notifications]);
}, [t, promQueries, queryCategory]);
const validateChQueryParams = useCallback((): boolean => {
let retval = true;
if (queryCategory !== EQueryType.CLICKHOUSE) return retval;
if (!chQueries || Object.keys(chQueries).length === 0) {
notifications.error({
notification.error({
message: 'Error',
description: t('chquery_required'),
});
@@ -232,7 +230,7 @@ function FormAlertRules({
Object.keys(chQueries).forEach((key) => {
if (chQueries[key].rawQuery === '') {
notifications.error({
notification.error({
message: 'Error',
description: t('chquery_required'),
});
@@ -241,14 +239,14 @@ function FormAlertRules({
});
return retval;
}, [t, chQueries, queryCategory, notifications]);
}, [t, chQueries, queryCategory]);
const validateQBParams = useCallback((): boolean => {
let retval = true;
if (queryCategory !== EQueryType.QUERY_BUILDER) return true;
if (!metricQueries || Object.keys(metricQueries).length === 0) {
notifications.error({
notification.error({
message: 'Error',
description: t('condition_required'),
});
@@ -256,7 +254,7 @@ function FormAlertRules({
}
if (!alertDef.condition?.target) {
notifications.error({
notification.error({
message: 'Error',
description: t('target_missing'),
});
@@ -265,7 +263,7 @@ function FormAlertRules({
Object.keys(metricQueries).forEach((key) => {
if (metricQueries[key].metricName === '') {
notifications.error({
notification.error({
message: 'Error',
description: t('metricname_missing', { where: metricQueries[key].name }),
});
@@ -275,7 +273,7 @@ function FormAlertRules({
Object.keys(formulaQueries).forEach((key) => {
if (formulaQueries[key].expression === '') {
notifications.error({
notification.error({
message: 'Error',
description: t('expression_missing', formulaQueries[key].name),
});
@@ -283,11 +281,11 @@ function FormAlertRules({
}
});
return retval;
}, [t, alertDef, queryCategory, metricQueries, formulaQueries, notifications]);
}, [t, alertDef, queryCategory, metricQueries, formulaQueries]);
const isFormValid = useCallback((): boolean => {
if (!alertDef.alert || alertDef.alert === '') {
notifications.error({
notification.error({
message: 'Error',
description: t('alertname_required'),
});
@@ -303,14 +301,7 @@ function FormAlertRules({
}
return validateQBParams();
}, [
t,
validateQBParams,
validateChQueryParams,
alertDef,
validatePromParams,
notifications,
]);
}, [t, validateQBParams, validateChQueryParams, alertDef, validatePromParams]);
const preparePostData = (): AlertDef => {
const postableAlert: AlertDef = {
@@ -358,7 +349,7 @@ function FormAlertRules({
const response = await saveAlertApi(apiReq);
if (response.statusCode === 200) {
notifications.success({
notification.success({
message: 'Success',
description:
!ruleId || ruleId === 0 ? t('rule_created') : t('rule_edited'),
@@ -371,26 +362,19 @@ function FormAlertRules({
history.replace(ROUTES.LIST_ALL_ALERT);
}, 2000);
} else {
notifications.error({
notification.error({
message: 'Error',
description: response.error || t('unexpected_error'),
});
}
} catch (e) {
notifications.error({
notification.error({
message: 'Error',
description: t('unexpected_error'),
});
}
setLoading(false);
}, [
t,
isFormValid,
ruleId,
ruleCache,
memoizedPreparePostData,
notifications,
]);
}, [t, isFormValid, ruleId, ruleCache, memoizedPreparePostData]);
const onSaveHandler = useCallback(async () => {
const content = (
@@ -424,64 +408,70 @@ function FormAlertRules({
if (response.statusCode === 200) {
const { payload } = response;
if (payload?.alertCount === 0) {
notifications.error({
notification.error({
message: 'Error',
description: t('no_alerts_found'),
});
} else {
notifications.success({
notification.success({
message: 'Success',
description: t('rule_test_fired'),
});
}
} else {
notifications.error({
notification.error({
message: 'Error',
description: response.error || t('unexpected_error'),
});
}
} catch (e) {
notifications.error({
notification.error({
message: 'Error',
description: t('unexpected_error'),
});
}
setLoading(false);
}, [t, isFormValid, memoizedPreparePostData, notifications]);
}, [t, isFormValid, memoizedPreparePostData]);
const renderBasicInfo = (): JSX.Element => (
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
);
const renderQBChartPreview = (): JSX.Element => (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name=""
threshold={alertDef.condition?.target}
query={debouncedStagedQuery}
selectedInterval={toChartInterval(alertDef.evalWindow)}
/>
);
const renderQBChartPreview = (): JSX.Element => {
return (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name=""
threshold={alertDef.condition?.target}
query={debouncedStagedQuery}
selectedInterval={toChartInterval(alertDef.evalWindow)}
/>
);
};
const renderPromChartPreview = (): JSX.Element => (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name="Chart Preview"
threshold={alertDef.condition?.target}
query={debouncedStagedQuery}
/>
);
const renderPromChartPreview = (): JSX.Element => {
return (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name="Chart Preview"
threshold={alertDef.condition?.target}
query={debouncedStagedQuery}
/>
);
};
const renderChQueryChartPreview = (): JSX.Element => (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name="Chart Preview"
threshold={alertDef.condition?.target}
query={manualStagedQuery}
userQueryKey={runQueryId}
selectedInterval={toChartInterval(alertDef.evalWindow)}
/>
);
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)}
/>
);
};
return (
<>
{Element}
@@ -545,9 +535,9 @@ function FormAlertRules({
</ButtonContainer>
</MainFormContainer>
</StyledLeftContainer>
<Col flex="1 1 300px">
<StyledRightContainer flex="1 1 300px">
<UserGuide queryType={queryCategory} />
</Col>
</StyledRightContainer>
</PanelContainer>
</>
);

View File

@@ -4,11 +4,13 @@ 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';
@@ -27,8 +29,7 @@ function LabelSelect({
initialValues,
}: LabelSelectProps): JSX.Element | null {
const { t } = useTranslation('alerts');
const isDarkMode = useIsDarkMode();
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const [currentVal, setCurrentVal] = useState('');
const [staging, setStaging] = useState<string[]>([]);
const [queries, setQueries] = useState<ILabelRecord[]>(
@@ -119,15 +120,17 @@ function LabelSelect({
{queries.length > 0 &&
map(
queries,
(query): JSX.Element => (
<QueryChip key={query.key} queryData={query} onRemove={handleClose} />
),
(query): JSX.Element => {
return (
<QueryChip key={query.key} queryData={query} onRemove={handleClose} />
);
},
)}
</div>
<div>
{map(staging, (item) => (
<QueryChipItem key={uuid()}>{item}</QueryChipItem>
))}
{map(staging, (item) => {
return <QueryChipItem key={uuid()}>{item}</QueryChipItem>;
})}
</div>
<div style={{ display: 'flex', width: '100%' }}>

View File

@@ -9,15 +9,19 @@ 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;
@@ -95,8 +99,6 @@ export const ThresholdInput = styled(InputNumber)`
align-items: center;
& > .ant-input-number-group-addon {
width: 130px;
border: 0;
background: transparent;
}
& > .ant-input-number {
width: 50%;
@@ -109,7 +111,7 @@ export const TextareaMedium = styled(TextArea)`
width: 70%;
`;
export const FormItemMedium = styled(Item)`
export const FormItemMedium = styled(FormItem)`
width: 70%;
`;

View File

@@ -42,15 +42,17 @@ export const toMetricQueries = (b: IBuilderQueries): IMetricQueries => {
export const toIMetricsBuilderQuery = (
q: IMetricQuery,
): IMetricsBuilderQuery => ({
name: q.name,
metricName: q.metricName,
tagFilters: q.tagFilters,
groupBy: q.groupBy,
aggregateOperator: q.aggregateOperator,
disabled: q.disabled,
legend: q.legend,
});
): IMetricsBuilderQuery => {
return {
name: q.name,
metricName: q.metricName,
tagFilters: q.tagFilters,
groupBy: q.groupBy,
aggregateOperator: q.aggregateOperator,
disabled: q.disabled,
legend: q.legend,
};
};
export const prepareBuilderQueries = (
m: IMetricQueries,

View File

@@ -1,5 +1,5 @@
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useThemeMode from 'hooks/useThemeMode';
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 = useIsDarkMode();
const { isDarkMode } = useThemeMode();
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
return (
<SpanWrapper>

View File

@@ -1,10 +1,10 @@
import { CaretDownFilled, CaretRightFilled } from '@ant-design/icons';
import { Col, Typography } from 'antd';
import { Col } from 'antd';
import { StyledCol, StyledRow } from 'components/Styled';
import { IIntervalUnit } from 'container/TraceDetail/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useThemeMode from 'hooks/useThemeMode';
import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { ITraceTree } from 'types/api/trace/getTraceItem';
import { ITraceMetaData } from '..';
@@ -19,7 +19,6 @@ import {
styles,
Wrapper,
} from './styles';
import { getIconStyles } from './utils';
function Trace(props: TraceProps): JSX.Element {
const {
@@ -43,8 +42,7 @@ function Trace(props: TraceProps): JSX.Element {
isMissing,
} = props;
const isDarkMode = useIsDarkMode();
const { isDarkMode } = useThemeMode();
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.
@@ -113,18 +111,6 @@ 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}
@@ -150,8 +136,10 @@ function Trace(props: TraceProps): JSX.Element {
isDarkMode={isDarkMode}
onClick={onClickTreeExpansion}
>
<Typography>{totalSpans}</Typography>
<CaretContainer>{icon}</CaretContainer>
{totalSpans}
<CaretContainer>
{isOpen ? <CaretDownFilled /> : <CaretRightFilled />}
</CaretContainer>
</CardComponent>
)}
</Col>

View File

@@ -1,3 +0,0 @@
export const getIconStyles = (isDarkMode: boolean): Record<string, string> => ({
color: isDarkMode ? 'white' : 'black',
});

Some files were not shown because too many files have changed in this diff Show More