Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
622e1765cf | ||
|
|
faaf0a6e73 | ||
|
|
4542a51531 | ||
|
|
191a538430 | ||
|
|
e6ce80213b | ||
|
|
3115b32dcd | ||
|
|
af272a368b | ||
|
|
b336a6cb45 | ||
|
|
b72815ca2f | ||
|
|
ed4a01dea6 | ||
|
|
1914c3b4a0 | ||
|
|
3811e96e23 | ||
|
|
8d16493432 | ||
|
|
db2bfbb887 | ||
|
|
213838a021 | ||
|
|
fd6f9a90e1 | ||
|
|
13f9922c53 | ||
|
|
a654baaa5b | ||
|
|
f766435acc | ||
|
|
d7a65ba689 | ||
|
|
05ce03e67d | ||
|
|
ba6818f487 | ||
|
|
ca53136cbf | ||
|
|
c46bef321c | ||
|
|
ba8f804b26 | ||
|
|
6cc7025e37 | ||
|
|
e62e541fc4 | ||
|
|
2f1ca93eda | ||
|
|
f1c7d72fc5 | ||
|
|
a405307c96 | ||
|
|
c85d48d7fa | ||
|
|
75470f6bb9 | ||
|
|
f75e688b32 | ||
|
|
5f3ca045df | ||
|
|
186632af69 | ||
|
|
fa652be926 | ||
|
|
1e39131c38 | ||
|
|
153e859ac3 | ||
|
|
d1cc29e118 | ||
|
|
972bf94dd0 | ||
|
|
3632208d45 | ||
|
|
cd9768c738 | ||
|
|
f01b9605db | ||
|
|
eec236af50 | ||
|
|
bbff2b459e | ||
|
|
d9535e7a8d | ||
|
|
a82bbe1a72 | ||
|
|
6812f55152 | ||
|
|
83163c17cd | ||
|
|
5ed7c9a46e | ||
|
|
2f323056d0 | ||
|
|
51b583480b | ||
|
|
7b1e2c8b98 | ||
|
|
b87f3bdb50 | ||
|
|
2f5908a3dd | ||
|
|
ca77820e9d | ||
|
|
a4346a2d93 | ||
|
|
44360ecacf | ||
|
|
b675c3cfec | ||
|
|
b23d8da96c | ||
|
|
215ea8d819 | ||
|
|
0c27d5acbc | ||
|
|
435d74c37e |
4
.github/workflows/build.yaml
vendored
4
.github/workflows/build.yaml
vendored
@@ -32,6 +32,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
- name: Run tests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
make test
|
||||||
- name: Build query-service image
|
- name: Build query-service image
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
25
.github/workflows/repo-stats.yml
vendored
25
.github/workflows/repo-stats.yml
vendored
@@ -1,25 +0,0 @@
|
|||||||
on:
|
|
||||||
schedule:
|
|
||||||
# Run this once per day, towards the end of the day for keeping the most
|
|
||||||
# recent data point most meaningful (hours are interpreted in UTC).
|
|
||||||
- cron: "0 8 * * *"
|
|
||||||
workflow_dispatch: # Allow for running this manually.
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
j1:
|
|
||||||
name: repostats
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: run-ghrs
|
|
||||||
uses: jgehrcke/github-repo-stats@v1.1.0
|
|
||||||
with:
|
|
||||||
# Define the stats repository (the repo to fetch
|
|
||||||
# stats for and to generate the report for).
|
|
||||||
# Remove the parameter when the stats repository
|
|
||||||
# and the data repository are the same.
|
|
||||||
repository: signoz/signoz
|
|
||||||
# Set a GitHub API token that can read the stats
|
|
||||||
# repository, and that can push to the data
|
|
||||||
# repository (which this workflow file lives in),
|
|
||||||
# to store data and the report files.
|
|
||||||
ghtoken: ${{ github.token }}
|
|
||||||
5
Makefile
5
Makefile
@@ -54,7 +54,7 @@ build-push-frontend:
|
|||||||
@echo "--> Building and pushing frontend docker image"
|
@echo "--> Building and pushing frontend docker image"
|
||||||
@echo "------------------"
|
@echo "------------------"
|
||||||
@cd $(FRONTEND_DIRECTORY) && \
|
@cd $(FRONTEND_DIRECTORY) && \
|
||||||
docker buildx build --file Dockerfile --progress plane --push --platform linux/amd64 \
|
docker buildx build --file Dockerfile --progress plane --push --platform linux/arm64,linux/amd64 \
|
||||||
--tag $(REPONAME)/$(FRONTEND_DOCKER_IMAGE):$(DOCKER_TAG) .
|
--tag $(REPONAME)/$(FRONTEND_DOCKER_IMAGE):$(DOCKER_TAG) .
|
||||||
|
|
||||||
# Steps to build and push docker image of query service
|
# Steps to build and push docker image of query service
|
||||||
@@ -135,3 +135,6 @@ clear-standalone-data:
|
|||||||
clear-swarm-data:
|
clear-swarm-data:
|
||||||
@docker run --rm -v "$(PWD)/$(SWARM_DIRECTORY)/data:/pwd" busybox \
|
@docker run --rm -v "$(PWD)/$(SWARM_DIRECTORY)/data:/pwd" busybox \
|
||||||
sh -c "cd /pwd && rm -rf alertmanager/* clickhouse*/* signoz/* zookeeper-*/*"
|
sh -c "cd /pwd && rm -rf alertmanager/* clickhouse*/* signoz/* zookeeper-*/*"
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test ./pkg/query-service/app/metrics/...
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
##
|
##
|
||||||
|
|
||||||
SigNoz helps developers monitor applications and troubleshoot problems in their deployed applications. SigNoz uses distributed tracing to gain visibility into your software stack.
|
SigNoz helps developers monitor applications and troubleshoot problems in their deployed applications. With SigNoz, you can:
|
||||||
|
|
||||||
👉 Visualise Metrics, Traces and Logs in a single pane of glass
|
👉 Visualise Metrics, Traces and Logs in a single pane of glass
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.13.1
|
image: signoz/query-service:0.15.0
|
||||||
command: ["-config=/root/config/prometheus.yml"]
|
command: ["-config=/root/config/prometheus.yml"]
|
||||||
# ports:
|
# ports:
|
||||||
# - "6060:6060" # pprof port
|
# - "6060:6060" # pprof port
|
||||||
@@ -166,7 +166,7 @@ services:
|
|||||||
<<: *clickhouse-depend
|
<<: *clickhouse-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:0.13.1
|
image: signoz/frontend:0.15.0
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
@@ -179,7 +179,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:0.66.1
|
image: signoz/signoz-otel-collector:0.66.3
|
||||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||||
user: root # required for reading docker container logs
|
user: root # required for reading docker container logs
|
||||||
volumes:
|
volumes:
|
||||||
@@ -207,7 +207,7 @@ services:
|
|||||||
<<: *clickhouse-depend
|
<<: *clickhouse-depend
|
||||||
|
|
||||||
otel-collector-metrics:
|
otel-collector-metrics:
|
||||||
image: signoz/signoz-otel-collector:0.66.1
|
image: signoz/signoz-otel-collector:0.66.3
|
||||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ processors:
|
|||||||
default: default
|
default: default
|
||||||
- name: deployment.environment
|
- name: deployment.environment
|
||||||
default: default
|
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:
|
# memory_limiter:
|
||||||
# # 80% of maximum memory up to 2G
|
# # 80% of maximum memory up to 2G
|
||||||
# limit_mib: 1500
|
# limit_mib: 1500
|
||||||
|
|||||||
@@ -905,7 +905,8 @@
|
|||||||
<dictionaries_config>*_dictionary.xml</dictionaries_config>
|
<dictionaries_config>*_dictionary.xml</dictionaries_config>
|
||||||
|
|
||||||
<!-- Configuration of user defined executable functions -->
|
<!-- Configuration of user defined executable functions -->
|
||||||
<user_defined_executable_functions_config>*_function.xml</user_defined_executable_functions_config>
|
<user_defined_executable_functions_config>*function.xml</user_defined_executable_functions_config>
|
||||||
|
<user_scripts_path>/var/lib/clickhouse/user_scripts/</user_scripts_path>
|
||||||
|
|
||||||
<!-- Uncomment if you want data to be compressed 30-100% better.
|
<!-- Uncomment if you want data to be compressed 30-100% better.
|
||||||
Don't do that if you just started using ClickHouse.
|
Don't do that if you just started using ClickHouse.
|
||||||
|
|||||||
21
deploy/docker/clickhouse-setup/custom-function.xml
Normal file
21
deploy/docker/clickhouse-setup/custom-function.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<functions>
|
||||||
|
<function>
|
||||||
|
<type>executable</type>
|
||||||
|
<name>histogramQuantile</name>
|
||||||
|
<return_type>Float64</return_type>
|
||||||
|
<argument>
|
||||||
|
<type>Array(Float64)</type>
|
||||||
|
<name>buckets</name>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<type>Array(Float64)</type>
|
||||||
|
<name>counts</name>
|
||||||
|
</argument>
|
||||||
|
<argument>
|
||||||
|
<type>Float64</type>
|
||||||
|
<name>quantile</name>
|
||||||
|
</argument>
|
||||||
|
<format>CSV</format>
|
||||||
|
<command>./histogramQuantile</command>
|
||||||
|
</function>
|
||||||
|
</functions>
|
||||||
@@ -41,7 +41,7 @@ services:
|
|||||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||||
otel-collector:
|
otel-collector:
|
||||||
container_name: otel-collector
|
container_name: otel-collector
|
||||||
image: signoz/signoz-otel-collector:0.66.1
|
image: signoz/signoz-otel-collector:0.66.3
|
||||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||||
# user: root # required for reading docker container logs
|
# user: root # required for reading docker container logs
|
||||||
volumes:
|
volumes:
|
||||||
@@ -67,7 +67,7 @@ services:
|
|||||||
|
|
||||||
otel-collector-metrics:
|
otel-collector-metrics:
|
||||||
container_name: otel-collector-metrics
|
container_name: otel-collector-metrics
|
||||||
image: signoz/signoz-otel-collector:0.66.1
|
image: signoz/signoz-otel-collector:0.66.3
|
||||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||||
|
|||||||
@@ -97,9 +97,11 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
|
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
|
||||||
- ./clickhouse-users.xml:/etc/clickhouse-server/users.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-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
|
||||||
# - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
- ./data/clickhouse/:/var/lib/clickhouse/
|
- ./data/clickhouse/:/var/lib/clickhouse/
|
||||||
|
- ./user_scripts:/var/lib/clickhouse/user_scripts/
|
||||||
|
|
||||||
# clickhouse-2:
|
# clickhouse-2:
|
||||||
# <<: *clickhouse-defaults
|
# <<: *clickhouse-defaults
|
||||||
@@ -112,9 +114,12 @@ services:
|
|||||||
# volumes:
|
# volumes:
|
||||||
# - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
|
# - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
|
||||||
# - ./clickhouse-users.xml:/etc/clickhouse-server/users.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-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
|
||||||
# # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
# - ./data/clickhouse-2/:/var/lib/clickhouse/
|
# - ./data/clickhouse-2/:/var/lib/clickhouse/
|
||||||
|
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
|
||||||
|
|
||||||
|
|
||||||
# clickhouse-3:
|
# clickhouse-3:
|
||||||
# <<: *clickhouse-defaults
|
# <<: *clickhouse-defaults
|
||||||
@@ -127,9 +132,11 @@ services:
|
|||||||
# volumes:
|
# volumes:
|
||||||
# - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
|
# - ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
|
||||||
# - ./clickhouse-users.xml:/etc/clickhouse-server/users.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-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
|
||||||
# # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
# # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||||
# - ./data/clickhouse-3/:/var/lib/clickhouse/
|
# - ./data/clickhouse-3/:/var/lib/clickhouse/
|
||||||
|
# - ./user_scripts:/var/lib/clickhouse/user_scripts/
|
||||||
|
|
||||||
alertmanager:
|
alertmanager:
|
||||||
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.0-0.2}
|
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.0-0.2}
|
||||||
@@ -146,7 +153,7 @@ services:
|
|||||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:${DOCKER_TAG:-0.13.1}
|
image: signoz/query-service:${DOCKER_TAG:-0.15.0}
|
||||||
container_name: query-service
|
container_name: query-service
|
||||||
command: ["-config=/root/config/prometheus.yml"]
|
command: ["-config=/root/config/prometheus.yml"]
|
||||||
# ports:
|
# ports:
|
||||||
@@ -174,7 +181,7 @@ services:
|
|||||||
<<: *clickhouse-depend
|
<<: *clickhouse-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.13.1}
|
image: signoz/frontend:${DOCKER_TAG:-0.15.0}
|
||||||
container_name: frontend
|
container_name: frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -186,7 +193,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.1}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.3}
|
||||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||||
user: root # required for reading docker container logs
|
user: root # required for reading docker container logs
|
||||||
volumes:
|
volumes:
|
||||||
@@ -211,7 +218,7 @@ services:
|
|||||||
<<: *clickhouse-depend
|
<<: *clickhouse-depend
|
||||||
|
|
||||||
otel-collector-metrics:
|
otel-collector-metrics:
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.1}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.66.3}
|
||||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||||
|
|||||||
@@ -83,6 +83,10 @@ processors:
|
|||||||
default: default
|
default: default
|
||||||
- name: deployment.environment
|
- name: deployment.environment
|
||||||
default: default
|
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:
|
# memory_limiter:
|
||||||
# # 80% of maximum memory up to 2G
|
# # 80% of maximum memory up to 2G
|
||||||
# limit_mib: 1500
|
# limit_mib: 1500
|
||||||
|
|||||||
BIN
deploy/docker/clickhouse-setup/user_scripts/histogramQuantile
Executable file
BIN
deploy/docker/clickhouse-setup/user_scripts/histogramQuantile
Executable file
Binary file not shown.
237
deploy/docker/clickhouse-setup/user_scripts/histogramQuantile.go
Normal file
237
deploy/docker/clickhouse-setup/user_scripts/histogramQuantile.go
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NOTE: executable must be built with target OS and architecture set to linux/amd64
|
||||||
|
// env GOOS=linux GOARCH=amd64 go build -o histogramQuantile histogramQuantile.go
|
||||||
|
|
||||||
|
// The following code is adapted from the following source:
|
||||||
|
// https://github.com/prometheus/prometheus/blob/main/promql/quantile.go
|
||||||
|
|
||||||
|
type bucket struct {
|
||||||
|
upperBound float64
|
||||||
|
count float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// buckets implements sort.Interface.
|
||||||
|
type buckets []bucket
|
||||||
|
|
||||||
|
func (b buckets) Len() int { return len(b) }
|
||||||
|
func (b buckets) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
||||||
|
func (b buckets) Less(i, j int) bool { return b[i].upperBound < b[j].upperBound }
|
||||||
|
|
||||||
|
// bucketQuantile calculates the quantile 'q' based on the given buckets. The
|
||||||
|
// buckets will be sorted by upperBound by this function (i.e. no sorting
|
||||||
|
// needed before calling this function). The quantile value is interpolated
|
||||||
|
// assuming a linear distribution within a bucket. However, if the quantile
|
||||||
|
// falls into the highest bucket, the upper bound of the 2nd highest bucket is
|
||||||
|
// returned. A natural lower bound of 0 is assumed if the upper bound of the
|
||||||
|
// lowest bucket is greater 0. In that case, interpolation in the lowest bucket
|
||||||
|
// happens linearly between 0 and the upper bound of the lowest bucket.
|
||||||
|
// However, if the lowest bucket has an upper bound less or equal 0, this upper
|
||||||
|
// bound is returned if the quantile falls into the lowest bucket.
|
||||||
|
//
|
||||||
|
// There are a number of special cases (once we have a way to report errors
|
||||||
|
// happening during evaluations of AST functions, we should report those
|
||||||
|
// explicitly):
|
||||||
|
//
|
||||||
|
// If 'buckets' has 0 observations, NaN is returned.
|
||||||
|
//
|
||||||
|
// If 'buckets' has fewer than 2 elements, NaN is returned.
|
||||||
|
//
|
||||||
|
// If the highest bucket is not +Inf, NaN is returned.
|
||||||
|
//
|
||||||
|
// If q==NaN, NaN is returned.
|
||||||
|
//
|
||||||
|
// If q<0, -Inf is returned.
|
||||||
|
//
|
||||||
|
// If q>1, +Inf is returned.
|
||||||
|
func bucketQuantile(q float64, buckets buckets) float64 {
|
||||||
|
if math.IsNaN(q) {
|
||||||
|
return math.NaN()
|
||||||
|
}
|
||||||
|
if q < 0 {
|
||||||
|
return math.Inf(-1)
|
||||||
|
}
|
||||||
|
if q > 1 {
|
||||||
|
return math.Inf(+1)
|
||||||
|
}
|
||||||
|
sort.Sort(buckets)
|
||||||
|
if !math.IsInf(buckets[len(buckets)-1].upperBound, +1) {
|
||||||
|
return math.NaN()
|
||||||
|
}
|
||||||
|
|
||||||
|
buckets = coalesceBuckets(buckets)
|
||||||
|
ensureMonotonic(buckets)
|
||||||
|
|
||||||
|
if len(buckets) < 2 {
|
||||||
|
return math.NaN()
|
||||||
|
}
|
||||||
|
observations := buckets[len(buckets)-1].count
|
||||||
|
if observations == 0 {
|
||||||
|
return math.NaN()
|
||||||
|
}
|
||||||
|
rank := q * observations
|
||||||
|
b := sort.Search(len(buckets)-1, func(i int) bool { return buckets[i].count >= rank })
|
||||||
|
|
||||||
|
if b == len(buckets)-1 {
|
||||||
|
return buckets[len(buckets)-2].upperBound
|
||||||
|
}
|
||||||
|
if b == 0 && buckets[0].upperBound <= 0 {
|
||||||
|
return buckets[0].upperBound
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
bucketStart float64
|
||||||
|
bucketEnd = buckets[b].upperBound
|
||||||
|
count = buckets[b].count
|
||||||
|
)
|
||||||
|
if b > 0 {
|
||||||
|
bucketStart = buckets[b-1].upperBound
|
||||||
|
count -= buckets[b-1].count
|
||||||
|
rank -= buckets[b-1].count
|
||||||
|
}
|
||||||
|
return bucketStart + (bucketEnd-bucketStart)*(rank/count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// coalesceBuckets merges buckets with the same upper bound.
|
||||||
|
//
|
||||||
|
// The input buckets must be sorted.
|
||||||
|
func coalesceBuckets(buckets buckets) buckets {
|
||||||
|
last := buckets[0]
|
||||||
|
i := 0
|
||||||
|
for _, b := range buckets[1:] {
|
||||||
|
if b.upperBound == last.upperBound {
|
||||||
|
last.count += b.count
|
||||||
|
} else {
|
||||||
|
buckets[i] = last
|
||||||
|
last = b
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buckets[i] = last
|
||||||
|
return buckets[:i+1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// The assumption that bucket counts increase monotonically with increasing
|
||||||
|
// upperBound may be violated during:
|
||||||
|
//
|
||||||
|
// * Recording rule evaluation of histogram_quantile, especially when rate()
|
||||||
|
// has been applied to the underlying bucket timeseries.
|
||||||
|
// * Evaluation of histogram_quantile computed over federated bucket
|
||||||
|
// timeseries, especially when rate() has been applied.
|
||||||
|
//
|
||||||
|
// This is because scraped data is not made available to rule evaluation or
|
||||||
|
// federation atomically, so some buckets are computed with data from the
|
||||||
|
// most recent scrapes, but the other buckets are missing data from the most
|
||||||
|
// recent scrape.
|
||||||
|
//
|
||||||
|
// Monotonicity is usually guaranteed because if a bucket with upper bound
|
||||||
|
// u1 has count c1, then any bucket with a higher upper bound u > u1 must
|
||||||
|
// have counted all c1 observations and perhaps more, so that c >= c1.
|
||||||
|
//
|
||||||
|
// Randomly interspersed partial sampling breaks that guarantee, and rate()
|
||||||
|
// exacerbates it. Specifically, suppose bucket le=1000 has a count of 10 from
|
||||||
|
// 4 samples but the bucket with le=2000 has a count of 7 from 3 samples. The
|
||||||
|
// monotonicity is broken. It is exacerbated by rate() because under normal
|
||||||
|
// operation, cumulative counting of buckets will cause the bucket counts to
|
||||||
|
// diverge such that small differences from missing samples are not a problem.
|
||||||
|
// rate() removes this divergence.)
|
||||||
|
//
|
||||||
|
// bucketQuantile depends on that monotonicity to do a binary search for the
|
||||||
|
// bucket with the φ-quantile count, so breaking the monotonicity
|
||||||
|
// guarantee causes bucketQuantile() to return undefined (nonsense) results.
|
||||||
|
//
|
||||||
|
// As a somewhat hacky solution until ingestion is atomic per scrape, we
|
||||||
|
// calculate the "envelope" of the histogram buckets, essentially removing
|
||||||
|
// any decreases in the count between successive buckets.
|
||||||
|
|
||||||
|
func ensureMonotonic(buckets buckets) {
|
||||||
|
max := buckets[0].count
|
||||||
|
for i := 1; i < len(buckets); i++ {
|
||||||
|
switch {
|
||||||
|
case buckets[i].count > max:
|
||||||
|
max = buckets[i].count
|
||||||
|
case buckets[i].count < max:
|
||||||
|
buckets[i].count = max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// End of copied code.
|
||||||
|
|
||||||
|
func readLines() []string {
|
||||||
|
r := bufio.NewReader(os.Stdin)
|
||||||
|
bytes := []byte{}
|
||||||
|
lines := []string{}
|
||||||
|
for {
|
||||||
|
line, isPrefix, err := r.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
bytes = append(bytes, line...)
|
||||||
|
if !isPrefix {
|
||||||
|
str := strings.TrimSpace(string(bytes))
|
||||||
|
if len(str) > 0 {
|
||||||
|
lines = append(lines, str)
|
||||||
|
bytes = []byte{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(bytes) > 0 {
|
||||||
|
lines = append(lines, string(bytes))
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
lines := readLines()
|
||||||
|
for _, text := range lines {
|
||||||
|
// Example input
|
||||||
|
// "[1, 2, 4, 8, 16]", "[1, 5, 8, 10, 14]", 0.9"
|
||||||
|
// bounds - counts - quantile
|
||||||
|
parts := strings.Split(text, "\",")
|
||||||
|
|
||||||
|
var bucketNumbers []float64
|
||||||
|
// Strip the ends with square brackets
|
||||||
|
text = parts[0][2 : len(parts[0])-1]
|
||||||
|
// Parse the bucket bounds
|
||||||
|
for _, num := range strings.Split(text, ",") {
|
||||||
|
num = strings.TrimSpace(num)
|
||||||
|
number, err := strconv.ParseFloat(num, 64)
|
||||||
|
if err == nil {
|
||||||
|
bucketNumbers = append(bucketNumbers, number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var bucketCounts []float64
|
||||||
|
// Strip the ends with square brackets
|
||||||
|
text = parts[1][2 : len(parts[1])-1]
|
||||||
|
// Parse the bucket counts
|
||||||
|
for _, num := range strings.Split(text, ",") {
|
||||||
|
num = strings.TrimSpace(num)
|
||||||
|
number, err := strconv.ParseFloat(num, 64)
|
||||||
|
if err == nil {
|
||||||
|
bucketCounts = append(bucketCounts, number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the quantile
|
||||||
|
q, err := strconv.ParseFloat(parts[2], 64)
|
||||||
|
var b buckets
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
for i := 0; i < len(bucketNumbers); i++ {
|
||||||
|
b = append(b, bucket{upperBound: bucketNumbers[i], count: bucketCounts[i]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println(bucketQuantile(q, b))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -511,13 +511,15 @@ else
|
|||||||
echo ""
|
echo ""
|
||||||
echo -e "🟢 Your frontend is running on http://localhost:3301"
|
echo -e "🟢 Your frontend is running on http://localhost:3301"
|
||||||
echo ""
|
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 "ℹ️ To bring down SigNoz and clean volumes : $sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml down -v"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "+++++++++++++++++++++++++++++++++++++++++++++++++"
|
echo "+++++++++++++++++++++++++++++++++++++++++++++++++"
|
||||||
echo ""
|
echo ""
|
||||||
echo "👉 Need help Getting Started?"
|
echo "👉 Need help in Getting Started?"
|
||||||
echo -e "Join us on Slack https://signoz.io/slack"
|
echo -e "Join us on Slack https://signoz.io/slack"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "\n📨 Please share your email to receive support & updates about SigNoz!"
|
echo -e "\n📨 Please share your email to receive support & updates about SigNoz!"
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import (
|
|||||||
"go.signoz.io/signoz/pkg/query-service/healthcheck"
|
"go.signoz.io/signoz/pkg/query-service/healthcheck"
|
||||||
basealm "go.signoz.io/signoz/pkg/query-service/integrations/alertManager"
|
basealm "go.signoz.io/signoz/pkg/query-service/integrations/alertManager"
|
||||||
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
|
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"
|
pqle "go.signoz.io/signoz/pkg/query-service/pqlEngine"
|
||||||
rules "go.signoz.io/signoz/pkg/query-service/rules"
|
rules "go.signoz.io/signoz/pkg/query-service/rules"
|
||||||
"go.signoz.io/signoz/pkg/query-service/telemetry"
|
"go.signoz.io/signoz/pkg/query-service/telemetry"
|
||||||
@@ -271,42 +272,42 @@ func (lrw *loggingResponseWriter) Flush() {
|
|||||||
|
|
||||||
func extractDashboardMetaData(path string, r *http.Request) (map[string]interface{}, bool) {
|
func extractDashboardMetaData(path string, r *http.Request) (map[string]interface{}, bool) {
|
||||||
pathToExtractBodyFrom := "/api/v2/metrics/query_range"
|
pathToExtractBodyFrom := "/api/v2/metrics/query_range"
|
||||||
var requestBody map[string]interface{}
|
|
||||||
data := map[string]interface{}{}
|
data := map[string]interface{}{}
|
||||||
|
var postData *model.QueryRangeParamsV2
|
||||||
|
|
||||||
if path == pathToExtractBodyFrom && (r.Method == "POST") {
|
if path == pathToExtractBodyFrom && (r.Method == "POST") {
|
||||||
bodyBytes, _ := ioutil.ReadAll(r.Body)
|
if r.Body != nil {
|
||||||
r.Body.Close() // must close
|
bodyBytes, err := ioutil.ReadAll(r.Body)
|
||||||
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
r.Body.Close() // must close
|
||||||
|
r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||||
|
json.Unmarshal(bodyBytes, &postData)
|
||||||
|
|
||||||
json.Unmarshal(bodyBytes, &requestBody)
|
} else {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
compositeMetricQuery, compositeMetricQueryExists := requestBody["compositeMetricQuery"]
|
signozMetricNotFound := false
|
||||||
compositeMetricQueryMap := compositeMetricQuery.(map[string]interface{})
|
|
||||||
signozMetricFound := false
|
|
||||||
|
|
||||||
if compositeMetricQueryExists {
|
if postData != nil {
|
||||||
signozMetricFound = telemetry.GetInstance().CheckSigNozMetrics(compositeMetricQueryMap)
|
signozMetricNotFound = telemetry.GetInstance().CheckSigNozMetricsV2(postData.CompositeMetricQuery)
|
||||||
queryType, queryTypeExists := compositeMetricQueryMap["queryType"]
|
|
||||||
if queryTypeExists {
|
if postData.CompositeMetricQuery != nil {
|
||||||
data["queryType"] = queryType
|
data["queryType"] = postData.CompositeMetricQuery.QueryType
|
||||||
}
|
data["panelType"] = postData.CompositeMetricQuery.PanelType
|
||||||
panelType, panelTypeExists := compositeMetricQueryMap["panelType"]
|
|
||||||
if panelTypeExists {
|
|
||||||
data["panelType"] = panelType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data["datasource"] = postData.DataSource
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource, datasourceExists := requestBody["dataSource"]
|
if signozMetricNotFound {
|
||||||
if datasourceExists {
|
|
||||||
data["datasource"] = datasource
|
|
||||||
}
|
|
||||||
|
|
||||||
if !signozMetricFound {
|
|
||||||
telemetry.GetInstance().AddActiveMetricsUser()
|
telemetry.GetInstance().AddActiveMetricsUser()
|
||||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_DASHBOARDS_METADATA, data, true)
|
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_DASHBOARDS_METADATA, data, true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,9 +102,10 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
'@typescript-eslint/no-unused-vars': 'error',
|
'@typescript-eslint/no-unused-vars': 'error',
|
||||||
|
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
|
||||||
|
'arrow-body-style': ['error', 'as-needed'],
|
||||||
|
|
||||||
// eslint rules need to remove
|
// eslint rules need to remove
|
||||||
'no-shadow': 'off',
|
|
||||||
'@typescript-eslint/no-shadow': 'off',
|
'@typescript-eslint/no-shadow': 'off',
|
||||||
'import/no-cycle': 'off',
|
'import/no-cycle': 'off',
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
cd frontend && npm run commitlint
|
cd frontend && yarn run commitlint --edit $1
|
||||||
|
|||||||
1
frontend/.yarnrc
Normal file
1
frontend/.yarnrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
network-timeout 600000
|
||||||
@@ -9,8 +9,9 @@ ARG TARGETARCH
|
|||||||
|
|
||||||
WORKDIR /frontend
|
WORKDIR /frontend
|
||||||
|
|
||||||
# Copy the package.json to install dependencies
|
# Copy the package.json and .yarnrc files prior to install dependencies
|
||||||
COPY package.json ./
|
COPY package.json ./
|
||||||
|
COPY .yarnrc ./
|
||||||
|
|
||||||
# Install the dependencies and make the folder
|
# Install the dependencies and make the folder
|
||||||
RUN CI=1 yarn install
|
RUN CI=1 yarn install
|
||||||
|
|||||||
@@ -27,8 +27,8 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/colors": "^6.0.0",
|
"@ant-design/colors": "6.0.0",
|
||||||
"@ant-design/icons": "^4.6.2",
|
"@ant-design/icons": "4.8.0",
|
||||||
"@grafana/data": "^8.4.3",
|
"@grafana/data": "^8.4.3",
|
||||||
"@monaco-editor/react": "^4.3.1",
|
"@monaco-editor/react": "^4.3.1",
|
||||||
"@testing-library/jest-dom": "^5.11.4",
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"@testing-library/user-event": "^12.1.10",
|
"@testing-library/user-event": "^12.1.10",
|
||||||
"@welldone-software/why-did-you-render": "^6.2.1",
|
"@welldone-software/why-did-you-render": "^6.2.1",
|
||||||
"@xstate/react": "^3.0.0",
|
"@xstate/react": "^3.0.0",
|
||||||
"antd": "4.19.2",
|
"antd": "5.0.5",
|
||||||
"axios": "^0.21.0",
|
"axios": "^0.21.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^26.6.0",
|
"babel-jest": "^26.6.0",
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
"babel-plugin-named-asset-import": "^0.3.7",
|
"babel-plugin-named-asset-import": "^0.3.7",
|
||||||
"babel-preset-minify": "^0.5.1",
|
"babel-preset-minify": "^0.5.1",
|
||||||
"babel-preset-react-app": "^10.0.0",
|
"babel-preset-react-app": "^10.0.0",
|
||||||
"chart.js": "^3.4.0",
|
"chart.js": "3.9.1",
|
||||||
"chartjs-adapter-date-fns": "^2.0.0",
|
"chartjs-adapter-date-fns": "^2.0.0",
|
||||||
"chartjs-plugin-annotation": "^1.4.0",
|
"chartjs-plugin-annotation": "^1.4.0",
|
||||||
"color": "^4.2.1",
|
"color": "^4.2.1",
|
||||||
@@ -70,16 +70,18 @@
|
|||||||
"less-loader": "^10.2.0",
|
"less-loader": "^10.2.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"mini-css-extract-plugin": "2.4.5",
|
"mini-css-extract-plugin": "2.4.5",
|
||||||
"react": "17.0.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "17.0.0",
|
"react-dom": "18.2.0",
|
||||||
"react-force-graph": "^1.41.0",
|
"react-force-graph": "^1.41.0",
|
||||||
"react-graph-vis": "^1.0.5",
|
"react-graph-vis": "^1.0.5",
|
||||||
"react-grid-layout": "^1.3.4",
|
"react-grid-layout": "^1.3.4",
|
||||||
"react-i18next": "^11.16.1",
|
"react-i18next": "^11.16.1",
|
||||||
|
"react-intersection-observer": "9.4.1",
|
||||||
"react-query": "^3.34.19",
|
"react-query": "^3.34.19",
|
||||||
"react-redux": "^7.2.2",
|
"react-redux": "^7.2.2",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-use": "^17.3.2",
|
"react-use": "^17.3.2",
|
||||||
|
"react-virtuoso": "4.0.3",
|
||||||
"react-vis": "^1.11.7",
|
"react-vis": "^1.11.7",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
@@ -132,8 +134,8 @@
|
|||||||
"@types/lodash-es": "^4.17.4",
|
"@types/lodash-es": "^4.17.4",
|
||||||
"@types/mini-css-extract-plugin": "^2.5.1",
|
"@types/mini-css-extract-plugin": "^2.5.1",
|
||||||
"@types/node": "^16.10.3",
|
"@types/node": "^16.10.3",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-dom": "^16.9.9",
|
"@types/react-dom": "18.0.10",
|
||||||
"@types/react-grid-layout": "^1.1.2",
|
"@types/react-grid-layout": "^1.1.2",
|
||||||
"@types/react-redux": "^7.1.11",
|
"@types/react-redux": "^7.1.11",
|
||||||
"@types/react-router-dom": "^5.1.6",
|
"@types/react-router-dom": "^5.1.6",
|
||||||
@@ -186,7 +188,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/react": "17.0.0",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-dom": "17.0.0"
|
"@types/react-dom": "18.0.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
frontend/public/css/antd.dark.min.css
vendored
10
frontend/public/css/antd.dark.min.css
vendored
File diff suppressed because one or more lines are too long
10
frontend/public/css/antd.min.css
vendored
10
frontend/public/css/antd.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -1,6 +1,8 @@
|
|||||||
|
import { ConfigProvider } from 'antd';
|
||||||
import NotFound from 'components/NotFound';
|
import NotFound from 'components/NotFound';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import AppLayout from 'container/AppLayout';
|
import AppLayout from 'container/AppLayout';
|
||||||
|
import { useThemeConfig } from 'hooks/useDarkMode';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import React, { Suspense } from 'react';
|
import React, { Suspense } from 'react';
|
||||||
import { Route, Router, Switch } from 'react-router-dom';
|
import { Route, Router, Switch } from 'react-router-dom';
|
||||||
@@ -9,29 +11,31 @@ import PrivateRoute from './Private';
|
|||||||
import routes from './routes';
|
import routes from './routes';
|
||||||
|
|
||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
|
const themeConfig = useThemeConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router history={history}>
|
<ConfigProvider theme={themeConfig}>
|
||||||
<PrivateRoute>
|
<Router history={history}>
|
||||||
<AppLayout>
|
<PrivateRoute>
|
||||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
<AppLayout>
|
||||||
<Switch>
|
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||||
{routes.map(({ path, component, exact }) => {
|
<Switch>
|
||||||
return (
|
{routes.map(({ path, component, exact }) => (
|
||||||
<Route
|
<Route
|
||||||
key={`${path}`}
|
key={`${path}`}
|
||||||
exact={exact}
|
exact={exact}
|
||||||
path={path}
|
path={path}
|
||||||
component={component}
|
component={component}
|
||||||
/>
|
/>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
|
|
||||||
<Route path="*" component={NotFound} />
|
<Route path="*" component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
</Router>
|
</Router>
|
||||||
|
</ConfigProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import axios from 'api';
|
import { ApiV2Instance as axios } from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
@@ -8,9 +8,7 @@ const query = async (
|
|||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(
|
const response = await axios.post(`/variables/query`, props);
|
||||||
`/variables/query?query=${encodeURIComponent(props.query)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ const getSpans = async (
|
|||||||
const updatedSelectedTags = props.selectedTags.map((e) => ({
|
const updatedSelectedTags = props.selectedTags.map((e) => ({
|
||||||
Key: e.Key[0],
|
Key: e.Key[0],
|
||||||
Operator: e.Operator,
|
Operator: e.Operator,
|
||||||
Values: e.Values,
|
StringValues: e.StringValues,
|
||||||
|
NumberValues: e.NumberValues,
|
||||||
|
BoolValues: e.BoolValues,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const exclude: string[] = [];
|
const exclude: string[] = [];
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ const getSpanAggregate = async (
|
|||||||
const updatedSelectedTags = props.selectedTags.map((e) => ({
|
const updatedSelectedTags = props.selectedTags.map((e) => ({
|
||||||
Key: e.Key[0],
|
Key: e.Key[0],
|
||||||
Operator: e.Operator,
|
Operator: e.Operator,
|
||||||
Values: e.Values,
|
StringValues: e.StringValues,
|
||||||
|
NumberValues: e.NumberValues,
|
||||||
|
BoolValues: e.BoolValues,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const other = Object.fromEntries(props.selectedFilter);
|
const other = Object.fromEntries(props.selectedFilter);
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ const getTagValue = async (
|
|||||||
const response = await axios.post<PayloadProps>(`/getTagValues`, {
|
const response = await axios.post<PayloadProps>(`/getTagValues`, {
|
||||||
start: props.start.toString(),
|
start: props.start.toString(),
|
||||||
end: props.end.toString(),
|
end: props.end.toString(),
|
||||||
tagKey: props.tagKey,
|
tagKey: {
|
||||||
|
Key: props.tagKey.Key,
|
||||||
|
Type: props.tagKey.Type,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
error: null,
|
error: null,
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import generatePicker from 'antd/es/date-picker/generatePicker';
|
|
||||||
import { Dayjs } from 'dayjs';
|
|
||||||
// included in antd
|
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
||||||
import dayjsGenerateConfig from 'rc-picker/lib/generate/dayjs';
|
|
||||||
|
|
||||||
const DatePicker = generatePicker<Dayjs>(dayjsGenerateConfig);
|
|
||||||
|
|
||||||
export default DatePicker;
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import MEditor, { EditorProps } from '@monaco-editor/react';
|
import MEditor, { EditorProps } from '@monaco-editor/react';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import AppReducer from 'types/reducer/app';
|
|
||||||
|
|
||||||
function Editor({
|
function Editor({
|
||||||
value,
|
value,
|
||||||
@@ -12,7 +10,7 @@ function Editor({
|
|||||||
height,
|
height,
|
||||||
options,
|
options,
|
||||||
}: MEditorProps): JSX.Element {
|
}: MEditorProps): JSX.Element {
|
||||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
const isDarkMode = useIsDarkMode();
|
||||||
return (
|
return (
|
||||||
<MEditor
|
<MEditor
|
||||||
theme={isDarkMode ? 'vs-dark' : 'vs-light'}
|
theme={isDarkMode ? 'vs-dark' : 'vs-light'}
|
||||||
|
|||||||
321
frontend/src/components/Graph/Plugin/DragSelect.ts
Normal file
321
frontend/src/components/Graph/Plugin/DragSelect.ts
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
import { Chart, ChartTypeRegistry, Plugin } from 'chart.js';
|
||||||
|
import * as ChartHelpers from 'chart.js/helpers';
|
||||||
|
|
||||||
|
// utils
|
||||||
|
import { ChartEventHandler, mergeDefaultOptions } from './utils';
|
||||||
|
|
||||||
|
export const dragSelectPluginId = 'drag-select-plugin';
|
||||||
|
|
||||||
|
type ChartDragHandlers = {
|
||||||
|
mousedown: ChartEventHandler;
|
||||||
|
mousemove: ChartEventHandler;
|
||||||
|
mouseup: ChartEventHandler;
|
||||||
|
globalMouseup: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DragSelectPluginOptions = {
|
||||||
|
color?: string;
|
||||||
|
onSelect?: (startValueX: number, endValueX: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultDragSelectPluginOptions: Required<DragSelectPluginOptions> = {
|
||||||
|
color: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
onSelect: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createDragSelectPluginOptions(
|
||||||
|
isEnabled: boolean,
|
||||||
|
onSelect?: (start: number, end: number) => void,
|
||||||
|
color?: string,
|
||||||
|
): DragSelectPluginOptions | false {
|
||||||
|
if (!isEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
onSelect,
|
||||||
|
color,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMousedownHandler(
|
||||||
|
chart: Chart,
|
||||||
|
dragData: DragSelectData,
|
||||||
|
): ChartEventHandler {
|
||||||
|
return (ev): void => {
|
||||||
|
const { left, right } = chart.chartArea;
|
||||||
|
|
||||||
|
let { x: startDragPositionX } = ChartHelpers.getRelativePosition(ev, chart);
|
||||||
|
|
||||||
|
if (left > startDragPositionX) {
|
||||||
|
startDragPositionX = left;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (right < startDragPositionX) {
|
||||||
|
startDragPositionX = right;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startValuePositionX = chart.scales.x.getValueForPixel(
|
||||||
|
startDragPositionX,
|
||||||
|
);
|
||||||
|
|
||||||
|
dragData.onDragStart(startDragPositionX, startValuePositionX);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMousemoveHandler(
|
||||||
|
chart: Chart,
|
||||||
|
dragData: DragSelectData,
|
||||||
|
): ChartEventHandler {
|
||||||
|
return (ev): void => {
|
||||||
|
if (!dragData.isMouseDown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { left, right } = chart.chartArea;
|
||||||
|
|
||||||
|
let { x: dragPositionX } = ChartHelpers.getRelativePosition(ev, chart);
|
||||||
|
|
||||||
|
if (left > dragPositionX) {
|
||||||
|
dragPositionX = left;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (right < dragPositionX) {
|
||||||
|
dragPositionX = right;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valuePositionX = chart.scales.x.getValueForPixel(dragPositionX);
|
||||||
|
|
||||||
|
dragData.onDrag(dragPositionX, valuePositionX);
|
||||||
|
chart.update('none');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMouseupHandler(
|
||||||
|
chart: Chart,
|
||||||
|
options: DragSelectPluginOptions,
|
||||||
|
dragData: DragSelectData,
|
||||||
|
): ChartEventHandler {
|
||||||
|
return (ev): void => {
|
||||||
|
const { left, right } = chart.chartArea;
|
||||||
|
|
||||||
|
let { x: endRelativePostionX } = ChartHelpers.getRelativePosition(ev, chart);
|
||||||
|
|
||||||
|
if (left > endRelativePostionX) {
|
||||||
|
endRelativePostionX = left;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (right < endRelativePostionX) {
|
||||||
|
endRelativePostionX = right;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endValuePositionX = chart.scales.x.getValueForPixel(
|
||||||
|
endRelativePostionX,
|
||||||
|
);
|
||||||
|
|
||||||
|
dragData.onDragEnd(endRelativePostionX, endValuePositionX);
|
||||||
|
|
||||||
|
chart.update('none');
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof options.onSelect === 'function' &&
|
||||||
|
typeof dragData.startValuePositionX === 'number' &&
|
||||||
|
typeof dragData.endValuePositionX === 'number'
|
||||||
|
) {
|
||||||
|
const start = Math.min(
|
||||||
|
dragData.startValuePositionX,
|
||||||
|
dragData.endValuePositionX,
|
||||||
|
);
|
||||||
|
const end = Math.max(
|
||||||
|
dragData.startValuePositionX,
|
||||||
|
dragData.endValuePositionX,
|
||||||
|
);
|
||||||
|
|
||||||
|
options.onSelect(start, end);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGlobalMouseupHandler(
|
||||||
|
options: DragSelectPluginOptions,
|
||||||
|
dragData: DragSelectData,
|
||||||
|
): () => void {
|
||||||
|
return (): void => {
|
||||||
|
const { isDragging, endRelativePixelPositionX, endValuePositionX } = dragData;
|
||||||
|
|
||||||
|
if (!isDragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragData.onDragEnd(
|
||||||
|
endRelativePixelPositionX as number,
|
||||||
|
endValuePositionX as number,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof options.onSelect === 'function' &&
|
||||||
|
typeof dragData.startValuePositionX === 'number' &&
|
||||||
|
typeof dragData.endValuePositionX === 'number'
|
||||||
|
) {
|
||||||
|
const start = Math.min(
|
||||||
|
dragData.startValuePositionX,
|
||||||
|
dragData.endValuePositionX,
|
||||||
|
);
|
||||||
|
const end = Math.max(
|
||||||
|
dragData.startValuePositionX,
|
||||||
|
dragData.endValuePositionX,
|
||||||
|
);
|
||||||
|
|
||||||
|
options.onSelect(start, end);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class DragSelectData {
|
||||||
|
public isDragging = false;
|
||||||
|
|
||||||
|
public isMouseDown = false;
|
||||||
|
|
||||||
|
public startRelativePixelPositionX: number | null = null;
|
||||||
|
|
||||||
|
public startValuePositionX: number | null | undefined = null;
|
||||||
|
|
||||||
|
public endRelativePixelPositionX: number | null = null;
|
||||||
|
|
||||||
|
public endValuePositionX: number | null | undefined = null;
|
||||||
|
|
||||||
|
public initialize(): void {
|
||||||
|
this.isDragging = false;
|
||||||
|
this.isMouseDown = false;
|
||||||
|
this.startRelativePixelPositionX = null;
|
||||||
|
this.startValuePositionX = null;
|
||||||
|
this.endRelativePixelPositionX = null;
|
||||||
|
this.endValuePositionX = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDragStart(
|
||||||
|
startRelativePixelPositionX: number,
|
||||||
|
startValuePositionX: number | undefined,
|
||||||
|
): void {
|
||||||
|
this.isDragging = false;
|
||||||
|
this.isMouseDown = true;
|
||||||
|
this.startRelativePixelPositionX = startRelativePixelPositionX;
|
||||||
|
this.startValuePositionX = startValuePositionX;
|
||||||
|
this.endRelativePixelPositionX = null;
|
||||||
|
this.endValuePositionX = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDrag(
|
||||||
|
endRelativePixelPositionX: number,
|
||||||
|
endValuePositionX: number | undefined,
|
||||||
|
): void {
|
||||||
|
this.isDragging = true;
|
||||||
|
this.endRelativePixelPositionX = endRelativePixelPositionX;
|
||||||
|
this.endValuePositionX = endValuePositionX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDragEnd(
|
||||||
|
endRelativePixelPositionX: number,
|
||||||
|
endValuePositionX: number | undefined,
|
||||||
|
): void {
|
||||||
|
if (!this.isDragging) {
|
||||||
|
this.initialize();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isDragging = false;
|
||||||
|
this.isMouseDown = false;
|
||||||
|
this.endRelativePixelPositionX = endRelativePixelPositionX;
|
||||||
|
this.endValuePositionX = endValuePositionX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createDragSelectPlugin = (): Plugin<
|
||||||
|
keyof ChartTypeRegistry,
|
||||||
|
DragSelectPluginOptions
|
||||||
|
> => {
|
||||||
|
const dragData = new DragSelectData();
|
||||||
|
let pluginOptions: Required<DragSelectPluginOptions>;
|
||||||
|
|
||||||
|
const handlers: ChartDragHandlers = {
|
||||||
|
mousedown: () => {},
|
||||||
|
mousemove: () => {},
|
||||||
|
mouseup: () => {},
|
||||||
|
globalMouseup: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const dragSelectPlugin: Plugin<
|
||||||
|
keyof ChartTypeRegistry,
|
||||||
|
DragSelectPluginOptions
|
||||||
|
> = {
|
||||||
|
id: dragSelectPluginId,
|
||||||
|
start: (chart: Chart, _, passedOptions) => {
|
||||||
|
pluginOptions = mergeDefaultOptions(
|
||||||
|
passedOptions,
|
||||||
|
defaultDragSelectPluginOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { canvas } = chart;
|
||||||
|
|
||||||
|
dragData.initialize();
|
||||||
|
|
||||||
|
const mousedownHandler = createMousedownHandler(chart, dragData);
|
||||||
|
const mousemoveHandler = createMousemoveHandler(chart, dragData);
|
||||||
|
const mouseupHandler = createMouseupHandler(chart, pluginOptions, dragData);
|
||||||
|
const globalMouseupHandler = createGlobalMouseupHandler(
|
||||||
|
pluginOptions,
|
||||||
|
dragData,
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.addEventListener('mousedown', mousedownHandler, { passive: true });
|
||||||
|
canvas.addEventListener('mousemove', mousemoveHandler, { passive: true });
|
||||||
|
canvas.addEventListener('mouseup', mouseupHandler, { passive: true });
|
||||||
|
document.addEventListener('mouseup', globalMouseupHandler, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
handlers.mousedown = mousedownHandler;
|
||||||
|
handlers.mousemove = mousemoveHandler;
|
||||||
|
handlers.mouseup = mouseupHandler;
|
||||||
|
handlers.globalMouseup = globalMouseupHandler;
|
||||||
|
},
|
||||||
|
beforeDestroy: (chart: Chart) => {
|
||||||
|
const { canvas } = chart;
|
||||||
|
|
||||||
|
if (!canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.removeEventListener('mousedown', handlers.mousedown);
|
||||||
|
canvas.removeEventListener('mousemove', handlers.mousemove);
|
||||||
|
canvas.removeEventListener('mouseup', handlers.mouseup);
|
||||||
|
document.removeEventListener('mouseup', handlers.globalMouseup);
|
||||||
|
},
|
||||||
|
afterDatasetsDraw: (chart: Chart) => {
|
||||||
|
const {
|
||||||
|
startRelativePixelPositionX,
|
||||||
|
endRelativePixelPositionX,
|
||||||
|
isDragging,
|
||||||
|
} = dragData;
|
||||||
|
|
||||||
|
if (startRelativePixelPositionX && endRelativePixelPositionX && isDragging) {
|
||||||
|
const left = Math.min(
|
||||||
|
startRelativePixelPositionX,
|
||||||
|
endRelativePixelPositionX,
|
||||||
|
);
|
||||||
|
const right = Math.max(
|
||||||
|
startRelativePixelPositionX,
|
||||||
|
endRelativePixelPositionX,
|
||||||
|
);
|
||||||
|
const top = chart.chartArea.top - 5;
|
||||||
|
const bottom = chart.chartArea.bottom + 5;
|
||||||
|
|
||||||
|
/* eslint-disable-next-line no-param-reassign */
|
||||||
|
chart.ctx.fillStyle = pluginOptions.color;
|
||||||
|
chart.ctx.fillRect(left, top, right - left, bottom - top);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return dragSelectPlugin;
|
||||||
|
};
|
||||||
@@ -11,7 +11,7 @@ export const emptyGraph = {
|
|||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
ctx.font = '1.5rem sans-serif';
|
ctx.font = '1.5rem sans-serif';
|
||||||
ctx.fillStyle = `${grey.primary}`;
|
ctx.fillStyle = `${grey.primary}`;
|
||||||
ctx.fillText('No data to display', width / 2, height / 2);
|
ctx.fillText('No data', width / 2, height / 2);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
164
frontend/src/components/Graph/Plugin/IntersectionCursor.ts
Normal file
164
frontend/src/components/Graph/Plugin/IntersectionCursor.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { Chart, ChartEvent, ChartTypeRegistry, Plugin } from 'chart.js';
|
||||||
|
import * as ChartHelpers from 'chart.js/helpers';
|
||||||
|
|
||||||
|
// utils
|
||||||
|
import { ChartEventHandler, mergeDefaultOptions } from './utils';
|
||||||
|
|
||||||
|
export const intersectionCursorPluginId = 'intersection-cursor-plugin';
|
||||||
|
|
||||||
|
export type IntersectionCursorPluginOptions = {
|
||||||
|
color?: string;
|
||||||
|
dashSize?: number;
|
||||||
|
gapSize?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultIntersectionCursorPluginOptions: Required<IntersectionCursorPluginOptions> = {
|
||||||
|
color: 'white',
|
||||||
|
dashSize: 3,
|
||||||
|
gapSize: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createIntersectionCursorPluginOptions(
|
||||||
|
isEnabled: boolean,
|
||||||
|
color?: string,
|
||||||
|
dashSize?: number,
|
||||||
|
gapSize?: number,
|
||||||
|
): IntersectionCursorPluginOptions | false {
|
||||||
|
if (!isEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
color,
|
||||||
|
dashSize,
|
||||||
|
gapSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMousemoveHandler(
|
||||||
|
chart: Chart,
|
||||||
|
cursorData: IntersectionCursorData,
|
||||||
|
): ChartEventHandler {
|
||||||
|
return (ev: ChartEvent | MouseEvent): void => {
|
||||||
|
const { left, right, top, bottom } = chart.chartArea;
|
||||||
|
|
||||||
|
let { x, y } = ChartHelpers.getRelativePosition(ev, chart);
|
||||||
|
|
||||||
|
if (left > x) {
|
||||||
|
x = left;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (right < x) {
|
||||||
|
x = right;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y < top) {
|
||||||
|
y = top;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y > bottom) {
|
||||||
|
y = bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursorData.onMouseMove(x, y);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMouseoutHandler(
|
||||||
|
cursorData: IntersectionCursorData,
|
||||||
|
): ChartEventHandler {
|
||||||
|
return (): void => {
|
||||||
|
cursorData.onMouseOut();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class IntersectionCursorData {
|
||||||
|
public positionX: number | null | undefined;
|
||||||
|
|
||||||
|
public positionY: number | null | undefined;
|
||||||
|
|
||||||
|
public initialize(): void {
|
||||||
|
this.positionX = null;
|
||||||
|
this.positionY = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onMouseMove(x: number | undefined, y: number | undefined): void {
|
||||||
|
this.positionX = x;
|
||||||
|
this.positionY = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onMouseOut(): void {
|
||||||
|
this.positionX = null;
|
||||||
|
this.positionY = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createIntersectionCursorPlugin = (): Plugin<
|
||||||
|
keyof ChartTypeRegistry,
|
||||||
|
IntersectionCursorPluginOptions
|
||||||
|
> => {
|
||||||
|
const cursorData = new IntersectionCursorData();
|
||||||
|
let pluginOptions: Required<IntersectionCursorPluginOptions>;
|
||||||
|
|
||||||
|
let mousemoveHandler: (ev: ChartEvent | MouseEvent) => void;
|
||||||
|
let mouseoutHandler: (ev: ChartEvent | MouseEvent) => void;
|
||||||
|
|
||||||
|
const intersectionCursorPlugin: Plugin<
|
||||||
|
keyof ChartTypeRegistry,
|
||||||
|
IntersectionCursorPluginOptions
|
||||||
|
> = {
|
||||||
|
id: intersectionCursorPluginId,
|
||||||
|
start: (chart: Chart, _, passedOptions) => {
|
||||||
|
const { canvas } = chart;
|
||||||
|
|
||||||
|
cursorData.initialize();
|
||||||
|
pluginOptions = mergeDefaultOptions(
|
||||||
|
passedOptions,
|
||||||
|
defaultIntersectionCursorPluginOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
mousemoveHandler = createMousemoveHandler(chart, cursorData);
|
||||||
|
mouseoutHandler = createMouseoutHandler(cursorData);
|
||||||
|
|
||||||
|
canvas.addEventListener('mousemove', mousemoveHandler, { passive: true });
|
||||||
|
canvas.addEventListener('mouseout', mouseoutHandler, { passive: true });
|
||||||
|
},
|
||||||
|
beforeDestroy: (chart: Chart) => {
|
||||||
|
const { canvas } = chart;
|
||||||
|
|
||||||
|
if (!canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.removeEventListener('mousemove', mousemoveHandler);
|
||||||
|
canvas.removeEventListener('mouseout', mouseoutHandler);
|
||||||
|
},
|
||||||
|
afterDatasetsDraw: (chart: Chart) => {
|
||||||
|
const { positionX, positionY } = cursorData;
|
||||||
|
|
||||||
|
const lineDashData = [pluginOptions.dashSize, pluginOptions.gapSize];
|
||||||
|
|
||||||
|
if (typeof positionX === 'number' && typeof positionY === 'number') {
|
||||||
|
const { top, bottom, left, right } = chart.chartArea;
|
||||||
|
|
||||||
|
chart.ctx.beginPath();
|
||||||
|
/* eslint-disable-next-line no-param-reassign */
|
||||||
|
chart.ctx.strokeStyle = pluginOptions.color;
|
||||||
|
chart.ctx.setLineDash(lineDashData);
|
||||||
|
chart.ctx.moveTo(left, positionY);
|
||||||
|
chart.ctx.lineTo(right, positionY);
|
||||||
|
chart.ctx.stroke();
|
||||||
|
|
||||||
|
chart.ctx.beginPath();
|
||||||
|
chart.ctx.setLineDash(lineDashData);
|
||||||
|
/* eslint-disable-next-line no-param-reassign */
|
||||||
|
chart.ctx.strokeStyle = pluginOptions.color;
|
||||||
|
chart.ctx.moveTo(positionX, top);
|
||||||
|
chart.ctx.lineTo(positionX, bottom);
|
||||||
|
chart.ctx.stroke();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return intersectionCursorPlugin;
|
||||||
|
};
|
||||||
@@ -22,87 +22,86 @@ const getOrCreateLegendList = (
|
|||||||
listContainer.style.height = '100%';
|
listContainer.style.height = '100%';
|
||||||
listContainer.style.flexWrap = 'wrap';
|
listContainer.style.flexWrap = 'wrap';
|
||||||
listContainer.style.justifyContent = 'center';
|
listContainer.style.justifyContent = 'center';
|
||||||
|
listContainer.style.fontSize = '0.75rem';
|
||||||
legendContainer?.appendChild(listContainer);
|
legendContainer?.appendChild(listContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
return listContainer;
|
return listContainer;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => {
|
export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => ({
|
||||||
return {
|
id: 'htmlLegend',
|
||||||
id: 'htmlLegend',
|
afterUpdate(chart): void {
|
||||||
afterUpdate(chart): void {
|
const ul = getOrCreateLegendList(chart, id || 'legend', isLonger);
|
||||||
const ul = getOrCreateLegendList(chart, id || 'legend', isLonger);
|
|
||||||
|
|
||||||
// Remove old legend items
|
// Remove old legend items
|
||||||
while (ul.firstChild) {
|
while (ul.firstChild) {
|
||||||
ul.firstChild.remove();
|
ul.firstChild.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reuse the built-in legendItems generator
|
// Reuse the built-in legendItems generator
|
||||||
const items = get(chart, [
|
const items = get(chart, [
|
||||||
'options',
|
'options',
|
||||||
'plugins',
|
'plugins',
|
||||||
'legend',
|
'legend',
|
||||||
'labels',
|
'labels',
|
||||||
'generateLabels',
|
'generateLabels',
|
||||||
])
|
])
|
||||||
? get(chart, ['options', 'plugins', 'legend', 'labels', 'generateLabels'])(
|
? get(chart, ['options', 'plugins', 'legend', 'labels', 'generateLabels'])(
|
||||||
chart,
|
chart,
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
items?.forEach((item: Record<any, any>, index: number) => {
|
items?.forEach((item: Record<any, any>, index: number) => {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.style.alignItems = 'center';
|
li.style.alignItems = 'center';
|
||||||
li.style.cursor = 'pointer';
|
li.style.cursor = 'pointer';
|
||||||
li.style.display = 'flex';
|
li.style.display = 'flex';
|
||||||
li.style.marginLeft = '10px';
|
li.style.marginLeft = '10px';
|
||||||
li.style.marginTop = '5px';
|
// li.style.marginTop = '5px';
|
||||||
|
|
||||||
li.onclick = (): void => {
|
li.onclick = (): void => {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const { type } = chart.config;
|
const { type } = chart.config;
|
||||||
if (type === 'pie' || type === 'doughnut') {
|
if (type === 'pie' || type === 'doughnut') {
|
||||||
// Pie and doughnut charts only have a single dataset and visibility is per item
|
// Pie and doughnut charts only have a single dataset and visibility is per item
|
||||||
chart.toggleDataVisibility(index);
|
chart.toggleDataVisibility(index);
|
||||||
} else {
|
} else {
|
||||||
chart.setDatasetVisibility(
|
chart.setDatasetVisibility(
|
||||||
item.datasetIndex,
|
item.datasetIndex,
|
||||||
!chart.isDatasetVisible(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);
|
|
||||||
}
|
}
|
||||||
});
|
chart.update();
|
||||||
},
|
};
|
||||||
};
|
|
||||||
};
|
// Color box
|
||||||
|
const boxSpan = document.createElement('span');
|
||||||
|
boxSpan.style.background = `${item.strokeStyle}` || `${colors[0]}`;
|
||||||
|
boxSpan.style.borderColor = `${item?.strokeStyle}`;
|
||||||
|
boxSpan.style.borderWidth = `${item.lineWidth}px`;
|
||||||
|
boxSpan.style.display = 'inline-block';
|
||||||
|
boxSpan.style.minHeight = '0.75rem';
|
||||||
|
boxSpan.style.marginRight = '0.5rem';
|
||||||
|
boxSpan.style.minWidth = '0.75rem';
|
||||||
|
boxSpan.style.borderRadius = '50%';
|
||||||
|
|
||||||
|
if (item.text) {
|
||||||
|
// Text
|
||||||
|
const textContainer = document.createElement('span');
|
||||||
|
textContainer.style.margin = '0';
|
||||||
|
textContainer.style.padding = '0';
|
||||||
|
textContainer.style.textDecoration = item.hidden ? 'line-through' : '';
|
||||||
|
|
||||||
|
const text = document.createTextNode(item.text);
|
||||||
|
textContainer.appendChild(text);
|
||||||
|
|
||||||
|
li.appendChild(boxSpan);
|
||||||
|
li.appendChild(textContainer);
|
||||||
|
ul.appendChild(li);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
20
frontend/src/components/Graph/Plugin/utils.ts
Normal file
20
frontend/src/components/Graph/Plugin/utils.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { ChartEvent } from 'chart.js';
|
||||||
|
|
||||||
|
export type ChartEventHandler = (ev: ChartEvent | MouseEvent) => void;
|
||||||
|
|
||||||
|
export function mergeDefaultOptions<T extends Record<string, unknown>>(
|
||||||
|
options: T,
|
||||||
|
defaultOptions: Required<T>,
|
||||||
|
): Required<T> {
|
||||||
|
const sanitizedOptions = { ...options };
|
||||||
|
Object.keys(options).forEach((key) => {
|
||||||
|
if (sanitizedOptions[key as keyof T] === undefined) {
|
||||||
|
delete sanitizedOptions[key as keyof T];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...defaultOptions,
|
||||||
|
...sanitizedOptions,
|
||||||
|
};
|
||||||
|
}
|
||||||
8
frontend/src/components/Graph/helpers.ts
Normal file
8
frontend/src/components/Graph/helpers.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { themeColors } from 'constants/theme';
|
||||||
|
|
||||||
|
export const getAxisLabelColor = (currentTheme: string): string => {
|
||||||
|
if (currentTheme === 'light') {
|
||||||
|
return themeColors.black;
|
||||||
|
}
|
||||||
|
return themeColors.whiteCream;
|
||||||
|
};
|
||||||
@@ -23,14 +23,25 @@ import {
|
|||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
|
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
|
||||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import React, { 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 { hasData } from './hasData';
|
||||||
|
import { getAxisLabelColor } from './helpers';
|
||||||
import { legend } from './Plugin';
|
import { legend } from './Plugin';
|
||||||
|
import {
|
||||||
|
createDragSelectPlugin,
|
||||||
|
createDragSelectPluginOptions,
|
||||||
|
dragSelectPluginId,
|
||||||
|
DragSelectPluginOptions,
|
||||||
|
} from './Plugin/DragSelect';
|
||||||
import { emptyGraph } from './Plugin/EmptyGraph';
|
import { emptyGraph } from './Plugin/EmptyGraph';
|
||||||
|
import {
|
||||||
|
createIntersectionCursorPlugin,
|
||||||
|
createIntersectionCursorPluginOptions,
|
||||||
|
intersectionCursorPluginId,
|
||||||
|
IntersectionCursorPluginOptions,
|
||||||
|
} from './Plugin/IntersectionCursor';
|
||||||
import { LegendsContainer } from './styles';
|
import { LegendsContainer } from './styles';
|
||||||
import { useXAxisTimeUnit } from './xAxisConfig';
|
import { useXAxisTimeUnit } from './xAxisConfig';
|
||||||
import { getToolTipValue, getYAxisFormattedValue } from './yAxisConfig';
|
import { getToolTipValue, getYAxisFormattedValue } from './yAxisConfig';
|
||||||
@@ -66,9 +77,12 @@ function Graph({
|
|||||||
forceReRender,
|
forceReRender,
|
||||||
staticLine,
|
staticLine,
|
||||||
containerHeight,
|
containerHeight,
|
||||||
|
onDragSelect,
|
||||||
|
dragSelectColor,
|
||||||
}: GraphProps): JSX.Element {
|
}: GraphProps): JSX.Element {
|
||||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
|
||||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const currentTheme = isDarkMode ? 'dark' : 'light';
|
const currentTheme = isDarkMode ? 'dark' : 'light';
|
||||||
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
|
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
|
||||||
|
|
||||||
@@ -92,7 +106,7 @@ function Graph({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (chartRef.current !== null) {
|
if (chartRef.current !== null) {
|
||||||
const options: ChartOptions = {
|
const options: CustomChartOptions = {
|
||||||
animation: {
|
animation: {
|
||||||
duration: animate ? 200 : 0,
|
duration: animate ? 200 : 0,
|
||||||
},
|
},
|
||||||
@@ -149,6 +163,15 @@ function Graph({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[dragSelectPluginId]: createDragSelectPluginOptions(
|
||||||
|
!!onDragSelect,
|
||||||
|
onDragSelect,
|
||||||
|
dragSelectColor,
|
||||||
|
),
|
||||||
|
[intersectionCursorPluginId]: createIntersectionCursorPluginOptions(
|
||||||
|
!!onDragSelect,
|
||||||
|
currentTheme === 'dark' ? 'white' : 'black',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
layout: {
|
layout: {
|
||||||
padding: 0,
|
padding: 0,
|
||||||
@@ -178,6 +201,7 @@ function Graph({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
type: 'time',
|
type: 'time',
|
||||||
|
ticks: { color: getAxisLabelColor(currentTheme) },
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
display: true,
|
display: true,
|
||||||
@@ -186,6 +210,7 @@ function Graph({
|
|||||||
color: getGridColor(),
|
color: getGridColor(),
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
|
color: getAxisLabelColor(currentTheme),
|
||||||
// Include a dollar sign in the ticks
|
// Include a dollar sign in the ticks
|
||||||
callback(value) {
|
callback(value) {
|
||||||
return getYAxisFormattedValue(value.toString(), yAxisUnit);
|
return getYAxisFormattedValue(value.toString(), yAxisUnit);
|
||||||
@@ -212,7 +237,13 @@ function Graph({
|
|||||||
const chartHasData = hasData(data);
|
const chartHasData = hasData(data);
|
||||||
const chartPlugins = [];
|
const chartPlugins = [];
|
||||||
|
|
||||||
if (!chartHasData) chartPlugins.push(emptyGraph);
|
if (chartHasData) {
|
||||||
|
chartPlugins.push(createIntersectionCursorPlugin());
|
||||||
|
chartPlugins.push(createDragSelectPlugin());
|
||||||
|
} else {
|
||||||
|
chartPlugins.push(emptyGraph);
|
||||||
|
}
|
||||||
|
|
||||||
chartPlugins.push(legend(name, data.datasets.length > 3));
|
chartPlugins.push(legend(name, data.datasets.length > 3));
|
||||||
|
|
||||||
lineChartRef.current = new Chart(chartRef.current, {
|
lineChartRef.current = new Chart(chartRef.current, {
|
||||||
@@ -235,6 +266,9 @@ function Graph({
|
|||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
onClickHandler,
|
onClickHandler,
|
||||||
staticLine,
|
staticLine,
|
||||||
|
onDragSelect,
|
||||||
|
dragSelectColor,
|
||||||
|
currentTheme,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -249,6 +283,13 @@ function Graph({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CustomChartOptions = ChartOptions & {
|
||||||
|
plugins: {
|
||||||
|
[dragSelectPluginId]: DragSelectPluginOptions | false;
|
||||||
|
[intersectionCursorPluginId]: IntersectionCursorPluginOptions | false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
interface GraphProps {
|
interface GraphProps {
|
||||||
animate?: boolean;
|
animate?: boolean;
|
||||||
type: ChartType;
|
type: ChartType;
|
||||||
@@ -261,6 +302,8 @@ interface GraphProps {
|
|||||||
forceReRender?: boolean | null | number;
|
forceReRender?: boolean | null | number;
|
||||||
staticLine?: StaticLineProps | undefined;
|
staticLine?: StaticLineProps | undefined;
|
||||||
containerHeight?: string | number;
|
containerHeight?: string | number;
|
||||||
|
onDragSelect?: (start: number, end: number) => void;
|
||||||
|
dragSelectColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StaticLineProps {
|
export interface StaticLineProps {
|
||||||
@@ -287,6 +330,8 @@ Graph.defaultProps = {
|
|||||||
yAxisUnit: undefined,
|
yAxisUnit: undefined,
|
||||||
forceReRender: undefined,
|
forceReRender: undefined,
|
||||||
staticLine: undefined,
|
staticLine: undefined,
|
||||||
containerHeight: '85%',
|
containerHeight: '90%',
|
||||||
|
onDragSelect: undefined,
|
||||||
|
dragSelectColor: undefined,
|
||||||
};
|
};
|
||||||
export default Graph;
|
export default Graph;
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
|
import { themeColors } from 'constants/theme';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const LegendsContainer = styled.div`
|
export const LegendsContainer = styled.div`
|
||||||
height: 15%;
|
height: 10%;
|
||||||
|
|
||||||
* {
|
* {
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
display: none !important;
|
width: 0.5rem;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: ${themeColors.royalGrey};
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: ${themeColors.matterhornGrey};
|
||||||
}
|
}
|
||||||
|
|
||||||
-ms-overflow-style: none !important; /* IE and Edge */
|
|
||||||
scrollbar-width: none !important; /* Firefox */
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,29 +1,27 @@
|
|||||||
import { Popover } from 'antd';
|
import { notification, Popover } from 'antd';
|
||||||
import React from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { useCopyToClipboard } from 'react-use';
|
import { useCopyToClipboard } from 'react-use';
|
||||||
|
|
||||||
interface CopyClipboardHOCProps {
|
|
||||||
textToCopy: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
function CopyClipboardHOC({
|
function CopyClipboardHOC({
|
||||||
textToCopy,
|
textToCopy,
|
||||||
children,
|
children,
|
||||||
}: CopyClipboardHOCProps): JSX.Element {
|
}: CopyClipboardHOCProps): JSX.Element {
|
||||||
const [, setCopy] = useCopyToClipboard();
|
const [value, setCopy] = useCopyToClipboard();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value.value) {
|
||||||
|
notification.success({
|
||||||
|
message: 'Copied to clipboard',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const onClick = useCallback((): void => {
|
||||||
|
setCopy(textToCopy);
|
||||||
|
}, [setCopy, textToCopy]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span onClick={onClick} onKeyDown={onClick} role="button" tabIndex={0}>
|
||||||
style={{
|
|
||||||
margin: 0,
|
|
||||||
padding: 0,
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
onClick={(): void => setCopy(textToCopy)}
|
|
||||||
onKeyDown={(): void => setCopy(textToCopy)}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
<Popover
|
<Popover
|
||||||
placement="top"
|
placement="top"
|
||||||
content={<span style={{ fontSize: '0.9rem' }}>Copy to clipboard</span>}
|
content={<span style={{ fontSize: '0.9rem' }}>Copy to clipboard</span>}
|
||||||
@@ -34,4 +32,9 @@ function CopyClipboardHOC({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CopyClipboardHOCProps {
|
||||||
|
textToCopy: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
export default CopyClipboardHOC;
|
export default CopyClipboardHOC;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { blue, grey, orange } from '@ant-design/colors';
|
import { blue, grey, orange } from '@ant-design/colors';
|
||||||
import { CopyFilled, ExpandAltOutlined } from '@ant-design/icons';
|
import { CopyFilled, ExpandAltOutlined } from '@ant-design/icons';
|
||||||
import { Button, Divider, Row, Typography } from 'antd';
|
import { Button, Divider, notification, Row, Typography } from 'antd';
|
||||||
import { map } from 'd3';
|
import { map } from 'd3';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
@@ -14,7 +14,7 @@ import { ILogsReducer } from 'types/reducer/logs';
|
|||||||
|
|
||||||
import AddToQueryHOC from '../AddToQueryHOC';
|
import AddToQueryHOC from '../AddToQueryHOC';
|
||||||
import CopyClipboardHOC from '../CopyClipboardHOC';
|
import CopyClipboardHOC from '../CopyClipboardHOC';
|
||||||
import { Container } from './styles';
|
import { Container, LogContainer, Text, TextContainer } from './styles';
|
||||||
import { isValidLogField } from './util';
|
import { isValidLogField } from './util';
|
||||||
|
|
||||||
interface LogFieldProps {
|
interface LogFieldProps {
|
||||||
@@ -23,21 +23,17 @@ interface LogFieldProps {
|
|||||||
}
|
}
|
||||||
function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
|
function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div
|
<TextContainer>
|
||||||
style={{
|
<Text ellipsis type="secondary">
|
||||||
display: 'flex',
|
{fieldKey}
|
||||||
overflow: 'hidden',
|
</Text>
|
||||||
width: '100%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography.Text type="secondary">{fieldKey}</Typography.Text>
|
|
||||||
<CopyClipboardHOC textToCopy={fieldValue}>
|
<CopyClipboardHOC textToCopy={fieldValue}>
|
||||||
<Typography.Text ellipsis>
|
<Typography.Text ellipsis>
|
||||||
{': '}
|
{': '}
|
||||||
{fieldValue}
|
{fieldValue}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</CopyClipboardHOC>
|
</CopyClipboardHOC>
|
||||||
</div>
|
</TextContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function LogSelectedField({
|
function LogSelectedField({
|
||||||
@@ -93,40 +89,46 @@ function LogItem({ logData }: LogItemProps): JSX.Element {
|
|||||||
|
|
||||||
const handleCopyJSON = (): void => {
|
const handleCopyJSON = (): void => {
|
||||||
setCopy(JSON.stringify(logData, null, 2));
|
setCopy(JSON.stringify(logData, null, 2));
|
||||||
|
notification.success({
|
||||||
|
message: 'Copied to clipboard',
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<div style={{ maxWidth: '100%' }}>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
{'{'}
|
{'{'}
|
||||||
<div style={{ marginLeft: '0.5rem' }}>
|
<LogContainer>
|
||||||
<LogGeneralField
|
<>
|
||||||
fieldKey="log"
|
|
||||||
fieldValue={flattenLogData.body as never}
|
|
||||||
/>
|
|
||||||
{flattenLogData.stream && (
|
|
||||||
<LogGeneralField
|
<LogGeneralField
|
||||||
fieldKey="stream"
|
fieldKey="log"
|
||||||
fieldValue={flattenLogData.stream as never}
|
fieldValue={flattenLogData.body as never}
|
||||||
/>
|
/>
|
||||||
)}
|
{flattenLogData.stream && (
|
||||||
<LogGeneralField
|
<LogGeneralField
|
||||||
fieldKey="timestamp"
|
fieldKey="stream"
|
||||||
fieldValue={dayjs((flattenLogData.timestamp as never) / 1e6).format()}
|
fieldValue={flattenLogData.stream as never}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
|
<LogGeneralField
|
||||||
|
fieldKey="timestamp"
|
||||||
|
fieldValue={dayjs((flattenLogData.timestamp as never) / 1e6).format()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
</LogContainer>
|
||||||
{'}'}
|
{'}'}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{map(selected, (field) => {
|
{map(selected, (field) =>
|
||||||
return isValidLogField(flattenLogData[field.name] as never) ? (
|
isValidLogField(flattenLogData[field.name] as never) ? (
|
||||||
<LogSelectedField
|
<LogSelectedField
|
||||||
key={field.name}
|
key={field.name}
|
||||||
fieldKey={field.name}
|
fieldKey={field.name}
|
||||||
fieldValue={flattenLogData[field.name] as never}
|
fieldValue={flattenLogData[field.name] as never}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null,
|
||||||
})}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Divider style={{ padding: 0, margin: '0.4rem 0', opacity: 0.5 }} />
|
<Divider style={{ padding: 0, margin: '0.4rem 0', opacity: 0.5 }} />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Card } from 'antd';
|
import { Card, Typography } from 'antd';
|
||||||
import styled, { keyframes } from 'styled-components';
|
import styled, { keyframes } from 'styled-components';
|
||||||
|
|
||||||
const fadeInAnimation = keyframes`
|
const fadeInAnimation = keyframes`
|
||||||
@@ -16,3 +16,20 @@ export const Container = styled(Card)`
|
|||||||
animation-duration: 0.2s;
|
animation-duration: 0.2s;
|
||||||
animation-timing-function: ease-in;
|
animation-timing-function: ease-in;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const Text = styled(Typography.Text)`
|
||||||
|
&&& {
|
||||||
|
min-width: 1.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TextContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LogContainer = styled.div`
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
`;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ function CustomModal({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={title}
|
title={title}
|
||||||
visible={isModalVisible}
|
open={isModalVisible}
|
||||||
footer={footer}
|
footer={footer}
|
||||||
closable={closable}
|
closable={closable}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ function ReleaseNote({ path }: ReleaseNoteProps): JSX.Element | null {
|
|||||||
(state) => state.app,
|
(state) => state.app,
|
||||||
);
|
);
|
||||||
|
|
||||||
const c = allComponentMap.find((item) => {
|
const c = allComponentMap.find((item) =>
|
||||||
return item.match(path, currentVersion, userFlags);
|
item.match(path, currentVersion, userFlags),
|
||||||
});
|
);
|
||||||
|
|
||||||
if (!c) {
|
if (!c) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import React from 'react';
|
|||||||
|
|
||||||
import { SpinerStyle } from './styles';
|
import { SpinerStyle } from './styles';
|
||||||
|
|
||||||
function Spinner({ size, tip, height }: SpinnerProps): JSX.Element {
|
function Spinner({ size, tip, height, style }: SpinnerProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<SpinerStyle height={height}>
|
<SpinerStyle height={height} style={style}>
|
||||||
<Spin spinning size={size} tip={tip} indicator={<LoadingOutlined spin />} />
|
<Spin spinning size={size} tip={tip} indicator={<LoadingOutlined spin />} />
|
||||||
</SpinerStyle>
|
</SpinerStyle>
|
||||||
);
|
);
|
||||||
@@ -16,11 +16,13 @@ interface SpinnerProps {
|
|||||||
size?: SpinProps['size'];
|
size?: SpinProps['size'];
|
||||||
tip?: SpinProps['tip'];
|
tip?: SpinProps['tip'];
|
||||||
height?: React.CSSProperties['height'];
|
height?: React.CSSProperties['height'];
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
Spinner.defaultProps = {
|
Spinner.defaultProps = {
|
||||||
size: undefined,
|
size: undefined,
|
||||||
tip: undefined,
|
tip: undefined,
|
||||||
height: undefined,
|
height: undefined,
|
||||||
|
style: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Spinner;
|
export default Spinner;
|
||||||
|
|||||||
@@ -6,18 +6,16 @@ import React from 'react';
|
|||||||
function TextToolTip({ text, url }: TextToolTipProps): JSX.Element {
|
function TextToolTip({ text, url }: TextToolTipProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
overlay={(): JSX.Element => {
|
overlay={(): JSX.Element => (
|
||||||
return (
|
<div>
|
||||||
<div>
|
{`${text} `}
|
||||||
{`${text} `}
|
{url && (
|
||||||
{url && (
|
<a href={url} rel="noopener noreferrer" target="_blank">
|
||||||
<a href={url} rel="noopener noreferrer" target="_blank">
|
here
|
||||||
here
|
</a>
|
||||||
</a>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<QuestionCircleFilled style={{ fontSize: '1.3125rem' }} />
|
<QuestionCircleFilled style={{ fontSize: '1.3125rem' }} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export const TextContainer = styled.div<TextContainerProps>`
|
|||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
> button {
|
> button {
|
||||||
margin-left: ${({ noButtonMargin }): string => {
|
margin-left: ${({ noButtonMargin }): string =>
|
||||||
return noButtonMargin ? '0' : '0.5rem';
|
noButtonMargin ? '0' : '0.5rem'}
|
||||||
}}
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -3,4 +3,5 @@ export enum LOCALSTORAGE {
|
|||||||
IS_LOGGED_IN = 'IS_LOGGED_IN',
|
IS_LOGGED_IN = 'IS_LOGGED_IN',
|
||||||
AUTH_TOKEN = 'AUTH_TOKEN',
|
AUTH_TOKEN = 'AUTH_TOKEN',
|
||||||
REFRESH_AUTH_TOKEN = 'REFRESH_AUTH_TOKEN',
|
REFRESH_AUTH_TOKEN = 'REFRESH_AUTH_TOKEN',
|
||||||
|
THEME = 'THEME',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ export const OperatorConversions: Array<{
|
|||||||
{
|
{
|
||||||
label: 'IN',
|
label: 'IN',
|
||||||
metricValue: '=~',
|
metricValue: '=~',
|
||||||
traceValue: 'in',
|
traceValue: 'In',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Not IN',
|
label: 'Not IN',
|
||||||
metricValue: '!~',
|
metricValue: '!~',
|
||||||
traceValue: 'not in',
|
traceValue: 'NotIn',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
42
frontend/src/constants/theme.ts
Normal file
42
frontend/src/constants/theme.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
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',
|
||||||
|
};
|
||||||
|
|
||||||
|
export { themeColors };
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { NotificationInstance } from 'antd/lib/notification';
|
import { NotificationInstance } from 'antd/es/notification/interface';
|
||||||
import deleteChannel from 'api/channels/delete';
|
import deleteChannel from 'api/channels/delete';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { ColumnType } from 'antd/es/table';
|
import { ColumnType, TablePaginationConfig } from 'antd/es/table';
|
||||||
|
import { FilterValue, SorterResult } from 'antd/es/table/interface';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import { FilterConfirmProps } from 'antd/lib/table/interface';
|
import { FilterConfirmProps } from 'antd/lib/table/interface';
|
||||||
import getAll from 'api/errors/getAll';
|
import getAll from 'api/errors/getAll';
|
||||||
@@ -30,6 +31,7 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
|
|||||||
import { Exception, PayloadProps } from 'types/api/errors/getAll';
|
import { Exception, PayloadProps } from 'types/api/errors/getAll';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
|
import { FilterDropdownExtendsProps } from './types';
|
||||||
import {
|
import {
|
||||||
extractFilterValues,
|
extractFilterValues,
|
||||||
getDefaultFilterValue,
|
getDefaultFilterValue,
|
||||||
@@ -176,41 +178,45 @@ function AllErrors(): JSX.Element {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const filterDropdownWrapper = useCallback(
|
const filterDropdownWrapper = useCallback(
|
||||||
({ setSelectedKeys, selectedKeys, confirm, placeholder, filterKey }) => {
|
({
|
||||||
return (
|
setSelectedKeys,
|
||||||
<Card size="small">
|
selectedKeys,
|
||||||
<Space align="start" direction="vertical">
|
confirm,
|
||||||
<Input
|
placeholder,
|
||||||
placeholder={placeholder}
|
filterKey,
|
||||||
value={selectedKeys[0]}
|
}: FilterDropdownExtendsProps) => (
|
||||||
onChange={(e): void =>
|
<Card size="small">
|
||||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
<Space align="start" direction="vertical">
|
||||||
}
|
<Input
|
||||||
allowClear
|
placeholder={placeholder}
|
||||||
defaultValue={getDefaultFilterValue(
|
value={selectedKeys[0]}
|
||||||
filterKey,
|
onChange={(e): void =>
|
||||||
getUpdatedServiceName,
|
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||||
getUpdatedExceptionType,
|
}
|
||||||
)}
|
allowClear
|
||||||
onPressEnter={handleSearch(confirm, selectedKeys[0], filterKey)}
|
defaultValue={getDefaultFilterValue(
|
||||||
/>
|
filterKey,
|
||||||
<Button
|
getUpdatedServiceName,
|
||||||
type="primary"
|
getUpdatedExceptionType,
|
||||||
onClick={handleSearch(confirm, selectedKeys[0], filterKey)}
|
)}
|
||||||
icon={<SearchOutlined />}
|
onPressEnter={handleSearch(confirm, String(selectedKeys[0]), filterKey)}
|
||||||
size="small"
|
/>
|
||||||
>
|
<Button
|
||||||
Search
|
type="primary"
|
||||||
</Button>
|
onClick={handleSearch(confirm, String(selectedKeys[0]), filterKey)}
|
||||||
</Space>
|
icon={<SearchOutlined />}
|
||||||
</Card>
|
size="small"
|
||||||
);
|
>
|
||||||
},
|
Search
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
),
|
||||||
[getUpdatedExceptionType, getUpdatedServiceName, handleSearch],
|
[getUpdatedExceptionType, getUpdatedServiceName, handleSearch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onExceptionTypeFilter = useCallback(
|
const onExceptionTypeFilter: ColumnType<Exception>['onFilter'] = useCallback(
|
||||||
(value, record: Exception): boolean => {
|
(value: unknown, record: Exception): boolean => {
|
||||||
if (record.exceptionType && typeof value === 'string') {
|
if (record.exceptionType && typeof value === 'string') {
|
||||||
return record.exceptionType.toLowerCase().includes(value.toLowerCase());
|
return record.exceptionType.toLowerCase().includes(value.toLowerCase());
|
||||||
}
|
}
|
||||||
@@ -220,7 +226,7 @@ function AllErrors(): JSX.Element {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const onApplicationTypeFilter = useCallback(
|
const onApplicationTypeFilter = useCallback(
|
||||||
(value, record: Exception): boolean => {
|
(value: unknown, record: Exception): boolean => {
|
||||||
if (record.serviceName && typeof value === 'string') {
|
if (record.serviceName && typeof value === 'string') {
|
||||||
return record.serviceName.toLowerCase().includes(value.toLowerCase());
|
return record.serviceName.toLowerCase().includes(value.toLowerCase());
|
||||||
}
|
}
|
||||||
@@ -343,7 +349,11 @@ function AllErrors(): JSX.Element {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const onChangeHandler: TableProps<Exception>['onChange'] = useCallback(
|
const onChangeHandler: TableProps<Exception>['onChange'] = useCallback(
|
||||||
(paginations, filters, sorter) => {
|
(
|
||||||
|
paginations: TablePaginationConfig,
|
||||||
|
filters: Record<string, FilterValue | null>,
|
||||||
|
sorter: SorterResult<Exception>[] | SorterResult<Exception>,
|
||||||
|
) => {
|
||||||
if (!Array.isArray(sorter)) {
|
if (!Array.isArray(sorter)) {
|
||||||
const { pageSize = 0, current = 0 } = paginations;
|
const { pageSize = 0, current = 0 } = paginations;
|
||||||
const { columnKey = '', order } = sorter;
|
const { columnKey = '', order } = sorter;
|
||||||
|
|||||||
9
frontend/src/container/AllError/types.ts
Normal file
9
frontend/src/container/AllError/types.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { FilterDropdownProps } from 'antd/es/table/interface';
|
||||||
|
|
||||||
|
export interface FilterDropdownExtendsProps {
|
||||||
|
placeholder: string;
|
||||||
|
filterKey: string;
|
||||||
|
confirm: FilterDropdownProps['confirm'];
|
||||||
|
setSelectedKeys: FilterDropdownProps['setSelectedKeys'];
|
||||||
|
selectedKeys: FilterDropdownProps['selectedKeys'];
|
||||||
|
}
|
||||||
@@ -20,15 +20,14 @@ export const urlKey = {
|
|||||||
serviceName: 'serviceName',
|
serviceName: 'serviceName',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isOrderParams = (orderBy: string | null): orderBy is OrderBy => {
|
export const isOrderParams = (orderBy: string | null): orderBy is OrderBy =>
|
||||||
return !!(
|
!!(
|
||||||
orderBy === 'serviceName' ||
|
orderBy === 'serviceName' ||
|
||||||
orderBy === 'exceptionCount' ||
|
orderBy === 'exceptionCount' ||
|
||||||
orderBy === 'lastSeen' ||
|
orderBy === 'lastSeen' ||
|
||||||
orderBy === 'firstSeen' ||
|
orderBy === 'firstSeen' ||
|
||||||
orderBy === 'exceptionType'
|
orderBy === 'exceptionType'
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
export const getOrder = (order: string | null): Order => {
|
export const getOrder = (order: string | null): Order => {
|
||||||
if (isOrder(order)) {
|
if (isOrder(order)) {
|
||||||
@@ -82,12 +81,9 @@ export const getDefaultOrder = (
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getNanoSeconds = (date: string): string => {
|
export const getNanoSeconds = (date: string): string =>
|
||||||
return (
|
Math.floor(new Date(date).getTime() / 1e3).toString() +
|
||||||
Math.floor(new Date(date).getTime() / 1e3).toString() +
|
String(Timestamp.fromString(date).getNano().toString()).padStart(9, '0');
|
||||||
String(Timestamp.fromString(date).getNano().toString()).padStart(9, '0')
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUpdatePageSize = (pageSize: string | null): number => {
|
export const getUpdatePageSize = (pageSize: string | null): number => {
|
||||||
if (pageSize) {
|
if (pageSize) {
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import styled from 'styled-components';
|
|||||||
|
|
||||||
export const Layout = styled(LayoutComponent)`
|
export const Layout = styled(LayoutComponent)`
|
||||||
&&& {
|
&&& {
|
||||||
min-height: 92vh;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
min-height: calc(100vh - 4rem);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { Menu, Space } from 'antd';
|
import { Menu, Space } from 'antd';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import React, { Suspense, useMemo } from 'react';
|
import React, { Suspense, useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import { ConfigProps } from 'types/api/dynamicConfigs/getDynamicConfigs';
|
import { ConfigProps } from 'types/api/dynamicConfigs/getDynamicConfigs';
|
||||||
import AppReducer from 'types/reducer/app';
|
|
||||||
|
|
||||||
import ErrorLink from './ErrorLink';
|
import ErrorLink from './ErrorLink';
|
||||||
import LinkContainer from './Link';
|
import LinkContainer from './Link';
|
||||||
|
import { MenuItem } from './styles';
|
||||||
|
|
||||||
function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
|
function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
|
||||||
const sortedConfig = useMemo(
|
const sortedConfig = useMemo(
|
||||||
@@ -15,10 +14,10 @@ function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
|
|||||||
[config.components],
|
[config.components],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu.ItemGroup>
|
<Menu>
|
||||||
{sortedConfig.map((item) => {
|
{sortedConfig.map((item) => {
|
||||||
const iconName = `${isDarkMode ? item.darkIcon : item.lightIcon}`;
|
const iconName = `${isDarkMode ? item.darkIcon : item.lightIcon}`;
|
||||||
|
|
||||||
@@ -28,19 +27,19 @@ function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<ErrorLink key={item.text + item.href}>
|
<ErrorLink key={item.text + item.href}>
|
||||||
<Suspense fallback={<Spinner height="5vh" />}>
|
<Suspense fallback={<Spinner height="5vh" />}>
|
||||||
<Menu.Item>
|
<MenuItem>
|
||||||
<LinkContainer href={item.href}>
|
<LinkContainer href={item.href}>
|
||||||
<Space size="small" align="start">
|
<Space size="small" align="start">
|
||||||
<Component />
|
<Component />
|
||||||
{item.text}
|
{item.text}
|
||||||
</Space>
|
</Space>
|
||||||
</LinkContainer>
|
</LinkContainer>
|
||||||
</Menu.Item>
|
</MenuItem>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</ErrorLink>
|
</ErrorLink>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Menu.ItemGroup>
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
frontend/src/container/ConfigDropdown/Config/styles.ts
Normal file
10
frontend/src/container/ConfigDropdown/Config/styles.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Menu } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const MenuItem = styled(Menu.Item)`
|
||||||
|
&&& {
|
||||||
|
height: 1.75rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
QuestionCircleOutlined,
|
QuestionCircleOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Dropdown, Menu, Space } from 'antd';
|
import { Dropdown, Menu, Space } from 'antd';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@@ -15,9 +16,8 @@ import HelpToolTip from './Config';
|
|||||||
function DynamicConfigDropdown({
|
function DynamicConfigDropdown({
|
||||||
frontendId,
|
frontendId,
|
||||||
}: DynamicConfigDropdownProps): JSX.Element {
|
}: DynamicConfigDropdownProps): JSX.Element {
|
||||||
const { configs, isDarkMode } = useSelector<AppState, AppReducer>(
|
const { configs } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
(state) => state.app,
|
const isDarkMode = useIsDarkMode();
|
||||||
);
|
|
||||||
const [isHelpDropDownOpen, setIsHelpDropDownOpen] = useState<boolean>(false);
|
const [isHelpDropDownOpen, setIsHelpDropDownOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const config = useMemo(
|
const config = useMemo(
|
||||||
|
|||||||
@@ -78,16 +78,17 @@ function CreateAlertChannels({
|
|||||||
[type, selectedConfig],
|
[type, selectedConfig],
|
||||||
);
|
);
|
||||||
|
|
||||||
const prepareSlackRequest = useCallback(() => {
|
const prepareSlackRequest = useCallback(
|
||||||
return {
|
() => ({
|
||||||
api_url: selectedConfig?.api_url || '',
|
api_url: selectedConfig?.api_url || '',
|
||||||
channel: selectedConfig?.channel || '',
|
channel: selectedConfig?.channel || '',
|
||||||
name: selectedConfig?.name || '',
|
name: selectedConfig?.name || '',
|
||||||
send_resolved: true,
|
send_resolved: true,
|
||||||
text: selectedConfig?.text || '',
|
text: selectedConfig?.text || '',
|
||||||
title: selectedConfig?.title || '',
|
title: selectedConfig?.title || '',
|
||||||
};
|
}),
|
||||||
}, [selectedConfig]);
|
[selectedConfig],
|
||||||
|
);
|
||||||
|
|
||||||
const onSlackHandler = useCallback(async () => {
|
const onSlackHandler = useCallback(async () => {
|
||||||
setSavingState(true);
|
setSavingState(true);
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ function EditAlertChannels({
|
|||||||
setType(value as ChannelType);
|
setType(value as ChannelType);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const prepareSlackRequest = useCallback(() => {
|
const prepareSlackRequest = useCallback(
|
||||||
return {
|
() => ({
|
||||||
api_url: selectedConfig?.api_url || '',
|
api_url: selectedConfig?.api_url || '',
|
||||||
channel: selectedConfig?.channel || '',
|
channel: selectedConfig?.channel || '',
|
||||||
name: selectedConfig?.name || '',
|
name: selectedConfig?.name || '',
|
||||||
@@ -56,8 +56,9 @@ function EditAlertChannels({
|
|||||||
text: selectedConfig?.text || '',
|
text: selectedConfig?.text || '',
|
||||||
title: selectedConfig?.title || '',
|
title: selectedConfig?.title || '',
|
||||||
id,
|
id,
|
||||||
};
|
}),
|
||||||
}, [id, selectedConfig]);
|
[id, selectedConfig],
|
||||||
|
);
|
||||||
|
|
||||||
const onSlackEditHandler = useCallback(async () => {
|
const onSlackEditHandler = useCallback(async () => {
|
||||||
setSavingState(true);
|
setSavingState(true);
|
||||||
@@ -143,8 +144,8 @@ function EditAlertChannels({
|
|||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
}, [prepareWebhookRequest, t, notifications, selectedConfig]);
|
}, [prepareWebhookRequest, t, notifications, selectedConfig]);
|
||||||
|
|
||||||
const preparePagerRequest = useCallback(() => {
|
const preparePagerRequest = useCallback(
|
||||||
return {
|
() => ({
|
||||||
name: selectedConfig.name || '',
|
name: selectedConfig.name || '',
|
||||||
routing_key: selectedConfig.routing_key,
|
routing_key: selectedConfig.routing_key,
|
||||||
client: selectedConfig.client,
|
client: selectedConfig.client,
|
||||||
@@ -157,8 +158,9 @@ function EditAlertChannels({
|
|||||||
details: selectedConfig.details,
|
details: selectedConfig.details,
|
||||||
detailsArray: JSON.parse(selectedConfig.details || '{}'),
|
detailsArray: JSON.parse(selectedConfig.details || '{}'),
|
||||||
id,
|
id,
|
||||||
};
|
}),
|
||||||
}, [id, selectedConfig]);
|
[id, selectedConfig],
|
||||||
|
);
|
||||||
|
|
||||||
const onPagerEditHandler = useCallback(async () => {
|
const onPagerEditHandler = useCallback(async () => {
|
||||||
setSavingState(true);
|
setSavingState(true);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Select } from 'antd';
|
import { Form, Select } from 'antd';
|
||||||
import FormItem from 'antd/lib/form/FormItem';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AlertDef, Labels } from 'types/api/alerts/def';
|
import { AlertDef, Labels } from 'types/api/alerts/def';
|
||||||
@@ -31,7 +30,7 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
|
|||||||
<>
|
<>
|
||||||
<StepHeading> {t('alert_form_step3')} </StepHeading>
|
<StepHeading> {t('alert_form_step3')} </StepHeading>
|
||||||
<FormContainer>
|
<FormContainer>
|
||||||
<FormItem
|
<Form.Item
|
||||||
label={t('field_severity')}
|
label={t('field_severity')}
|
||||||
labelAlign="left"
|
labelAlign="left"
|
||||||
name={['labels', 'severity']}
|
name={['labels', 'severity']}
|
||||||
@@ -54,9 +53,9 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
|
|||||||
<Option value="warning">{t('option_warning')}</Option>
|
<Option value="warning">{t('option_warning')}</Option>
|
||||||
<Option value="info">{t('option_info')}</Option>
|
<Option value="info">{t('option_info')}</Option>
|
||||||
</SeveritySelect>
|
</SeveritySelect>
|
||||||
</FormItem>
|
</Form.Item>
|
||||||
|
|
||||||
<FormItem label={t('field_alert_name')} labelAlign="left" name="alert">
|
<Form.Item label={t('field_alert_name')} labelAlign="left" name="alert">
|
||||||
<InputSmall
|
<InputSmall
|
||||||
onChange={(e): void => {
|
onChange={(e): void => {
|
||||||
setAlertDef({
|
setAlertDef({
|
||||||
@@ -65,8 +64,8 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</Form.Item>
|
||||||
<FormItem
|
<Form.Item
|
||||||
label={t('field_alert_desc')}
|
label={t('field_alert_desc')}
|
||||||
labelAlign="left"
|
labelAlign="left"
|
||||||
name={['annotations', 'description']}
|
name={['annotations', 'description']}
|
||||||
@@ -82,7 +81,7 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</Form.Item>
|
||||||
<FormItemMedium label={t('field_labels')}>
|
<FormItemMedium label={t('field_labels')}>
|
||||||
<LabelSelect
|
<LabelSelect
|
||||||
onSetLabels={(l: Labels): void => {
|
onSetLabels={(l: Labels): void => {
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export const rawQueryToIChQuery = (
|
|||||||
// ClickHouseQueryBuilder format. The main difference is
|
// ClickHouseQueryBuilder format. The main difference is
|
||||||
// use of rawQuery (in ClickHouseQueryBuilder)
|
// use of rawQuery (in ClickHouseQueryBuilder)
|
||||||
// and query (in alert builder)
|
// and query (in alert builder)
|
||||||
export const toIClickHouseQuery = (src: IChQuery): IClickHouseQuery => {
|
export const toIClickHouseQuery = (src: IChQuery): IClickHouseQuery => ({
|
||||||
return { ...src, name: 'A', rawQuery: src.query };
|
...src,
|
||||||
};
|
name: 'A',
|
||||||
|
rawQuery: src.query,
|
||||||
|
});
|
||||||
|
|||||||
@@ -211,78 +211,70 @@ function QuerySection({
|
|||||||
setFormulaQueries({ ...formulas });
|
setFormulaQueries({ ...formulas });
|
||||||
}, [formulaQueries, setFormulaQueries]);
|
}, [formulaQueries, setFormulaQueries]);
|
||||||
|
|
||||||
const renderPromqlUI = (): JSX.Element => {
|
const renderPromqlUI = (): JSX.Element => (
|
||||||
return (
|
<PromqlSection promQueries={promQueries} setPromQueries={setPromQueries} />
|
||||||
<PromqlSection promQueries={promQueries} setPromQueries={setPromQueries} />
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderChQueryUI = (): JSX.Element => {
|
const renderChQueryUI = (): JSX.Element => (
|
||||||
return <ChQuerySection chQueries={chQueries} setChQueries={setChQueries} />;
|
<ChQuerySection chQueries={chQueries} setChQueries={setChQueries} />
|
||||||
};
|
);
|
||||||
|
|
||||||
const renderFormulaButton = (): JSX.Element => {
|
const renderFormulaButton = (): JSX.Element => (
|
||||||
return (
|
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
|
||||||
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
|
{t('button_formula')}
|
||||||
{t('button_formula')}
|
</QueryButton>
|
||||||
</QueryButton>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderQueryButton = (): JSX.Element => {
|
const renderQueryButton = (): JSX.Element => (
|
||||||
return (
|
<QueryButton onClick={addMetricQuery} icon={<PlusOutlined />}>
|
||||||
<QueryButton onClick={addMetricQuery} icon={<PlusOutlined />}>
|
{t('button_query')}
|
||||||
{t('button_query')}
|
</QueryButton>
|
||||||
</QueryButton>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderMetricUI = (): JSX.Element => {
|
const renderMetricUI = (): JSX.Element => (
|
||||||
return (
|
<div>
|
||||||
<div>
|
{metricQueries &&
|
||||||
{metricQueries &&
|
Object.keys(metricQueries).map((key: string) => {
|
||||||
Object.keys(metricQueries).map((key: string) => {
|
// todo(amol): need to handle this in fetch
|
||||||
|
const current = metricQueries[key];
|
||||||
|
current.name = key;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MetricsBuilder
|
||||||
|
key={key}
|
||||||
|
queryIndex={key}
|
||||||
|
queryData={toIMetricsBuilderQuery(current)}
|
||||||
|
selectedGraph="TIME_SERIES"
|
||||||
|
handleQueryChange={handleMetricQueryChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{queryCategory !== EQueryType.PROM && renderQueryButton()}
|
||||||
|
<div style={{ marginTop: '1rem' }}>
|
||||||
|
{formulaQueries &&
|
||||||
|
Object.keys(formulaQueries).map((key: string) => {
|
||||||
// todo(amol): need to handle this in fetch
|
// todo(amol): need to handle this in fetch
|
||||||
const current = metricQueries[key];
|
const current = formulaQueries[key];
|
||||||
current.name = key;
|
current.name = key;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MetricsBuilder
|
<MetricsBuilderFormula
|
||||||
key={key}
|
key={key}
|
||||||
queryIndex={key}
|
formulaIndex={key}
|
||||||
queryData={toIMetricsBuilderQuery(current)}
|
formulaData={current}
|
||||||
selectedGraph="TIME_SERIES"
|
handleFormulaChange={handleFormulaChange}
|
||||||
handleQueryChange={handleMetricQueryChange}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{queryCategory === EQueryType.QUERY_BUILDER &&
|
||||||
{queryCategory !== EQueryType.PROM && renderQueryButton()}
|
(!formulaQueries || Object.keys(formulaQueries).length === 0) &&
|
||||||
<div style={{ marginTop: '1rem' }}>
|
metricQueries &&
|
||||||
{formulaQueries &&
|
Object.keys(metricQueries).length > 0 &&
|
||||||
Object.keys(formulaQueries).map((key: string) => {
|
renderFormulaButton()}
|
||||||
// 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>
|
||||||
);
|
</div>
|
||||||
};
|
);
|
||||||
|
|
||||||
const handleRunQuery = (): void => {
|
const handleRunQuery = (): void => {
|
||||||
runQuery();
|
runQuery();
|
||||||
|
|||||||
@@ -38,106 +38,94 @@ function RuleOptions({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderCompareOps = (): JSX.Element => {
|
const renderCompareOps = (): JSX.Element => (
|
||||||
return (
|
<InlineSelect
|
||||||
<InlineSelect
|
defaultValue={defaultCompareOp}
|
||||||
defaultValue={defaultCompareOp}
|
value={alertDef.condition?.op}
|
||||||
value={alertDef.condition?.op}
|
style={{ minWidth: '120px' }}
|
||||||
style={{ minWidth: '120px' }}
|
onChange={(value: string | unknown): void => {
|
||||||
onChange={(value: string | unknown): void => {
|
const newOp = (value as string) || '';
|
||||||
const newOp = (value as string) || '';
|
|
||||||
|
|
||||||
setAlertDef({
|
setAlertDef({
|
||||||
...alertDef,
|
...alertDef,
|
||||||
condition: {
|
condition: {
|
||||||
...alertDef.condition,
|
...alertDef.condition,
|
||||||
op: newOp,
|
op: newOp,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Option value="1">{t('option_above')}</Option>
|
<Option value="1">{t('option_above')}</Option>
|
||||||
<Option value="2">{t('option_below')}</Option>
|
<Option value="2">{t('option_below')}</Option>
|
||||||
<Option value="3">{t('option_equal')}</Option>
|
<Option value="3">{t('option_equal')}</Option>
|
||||||
<Option value="4">{t('option_notequal')}</Option>
|
<Option value="4">{t('option_notequal')}</Option>
|
||||||
</InlineSelect>
|
</InlineSelect>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const renderThresholdMatchOpts = (): JSX.Element => {
|
const renderThresholdMatchOpts = (): JSX.Element => (
|
||||||
return (
|
<InlineSelect
|
||||||
<InlineSelect
|
defaultValue={defaultMatchType}
|
||||||
defaultValue={defaultMatchType}
|
style={{ minWidth: '130px' }}
|
||||||
style={{ minWidth: '130px' }}
|
value={alertDef.condition?.matchType}
|
||||||
value={alertDef.condition?.matchType}
|
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
|
||||||
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
|
>
|
||||||
>
|
<Option value="1">{t('option_atleastonce')}</Option>
|
||||||
<Option value="1">{t('option_atleastonce')}</Option>
|
<Option value="2">{t('option_allthetimes')}</Option>
|
||||||
<Option value="2">{t('option_allthetimes')}</Option>
|
<Option value="3">{t('option_onaverage')}</Option>
|
||||||
<Option value="3">{t('option_onaverage')}</Option>
|
<Option value="4">{t('option_intotal')}</Option>
|
||||||
<Option value="4">{t('option_intotal')}</Option>
|
</InlineSelect>
|
||||||
</InlineSelect>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderPromMatchOpts = (): JSX.Element => {
|
const renderPromMatchOpts = (): JSX.Element => (
|
||||||
return (
|
<InlineSelect
|
||||||
<InlineSelect
|
defaultValue={defaultMatchType}
|
||||||
defaultValue={defaultMatchType}
|
style={{ minWidth: '130px' }}
|
||||||
style={{ minWidth: '130px' }}
|
value={alertDef.condition?.matchType}
|
||||||
value={alertDef.condition?.matchType}
|
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
|
||||||
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
|
>
|
||||||
>
|
<Option value="1">{t('option_atleastonce')}</Option>
|
||||||
<Option value="1">{t('option_atleastonce')}</Option>
|
</InlineSelect>
|
||||||
</InlineSelect>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderEvalWindows = (): JSX.Element => {
|
const renderEvalWindows = (): JSX.Element => (
|
||||||
return (
|
<InlineSelect
|
||||||
<InlineSelect
|
defaultValue={defaultEvalWindow}
|
||||||
defaultValue={defaultEvalWindow}
|
style={{ minWidth: '120px' }}
|
||||||
style={{ minWidth: '120px' }}
|
value={alertDef.evalWindow}
|
||||||
value={alertDef.evalWindow}
|
onChange={(value: string | unknown): void => {
|
||||||
onChange={(value: string | unknown): void => {
|
const ew = (value as string) || alertDef.evalWindow;
|
||||||
const ew = (value as string) || alertDef.evalWindow;
|
setAlertDef({
|
||||||
setAlertDef({
|
...alertDef,
|
||||||
...alertDef,
|
evalWindow: ew,
|
||||||
evalWindow: ew,
|
});
|
||||||
});
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{' '}
|
||||||
{' '}
|
<Option value="5m0s">{t('option_5min')}</Option>
|
||||||
<Option value="5m0s">{t('option_5min')}</Option>
|
<Option value="10m0s">{t('option_10min')}</Option>
|
||||||
<Option value="10m0s">{t('option_10min')}</Option>
|
<Option value="15m0s">{t('option_15min')}</Option>
|
||||||
<Option value="15m0s">{t('option_15min')}</Option>
|
<Option value="60m0s">{t('option_60min')}</Option>
|
||||||
<Option value="60m0s">{t('option_60min')}</Option>
|
<Option value="4h0m0s">{t('option_4hours')}</Option>
|
||||||
<Option value="4h0m0s">{t('option_4hours')}</Option>
|
<Option value="24h0m0s">{t('option_24hours')}</Option>
|
||||||
<Option value="24h0m0s">{t('option_24hours')}</Option>
|
</InlineSelect>
|
||||||
</InlineSelect>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderThresholdRuleOpts = (): JSX.Element => {
|
const renderThresholdRuleOpts = (): JSX.Element => (
|
||||||
return (
|
<FormItem>
|
||||||
<FormItem>
|
<Typography.Text>
|
||||||
<Typography.Text>
|
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
|
||||||
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
|
{renderThresholdMatchOpts()} {t('text_condition3')} {renderEvalWindows()}
|
||||||
{renderThresholdMatchOpts()} {t('text_condition3')} {renderEvalWindows()}
|
</Typography.Text>
|
||||||
</Typography.Text>
|
</FormItem>
|
||||||
</FormItem>
|
);
|
||||||
);
|
const renderPromRuleOptions = (): JSX.Element => (
|
||||||
};
|
<FormItem>
|
||||||
const renderPromRuleOptions = (): JSX.Element => {
|
<Typography.Text>
|
||||||
return (
|
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
|
||||||
<FormItem>
|
{renderPromMatchOpts()}
|
||||||
<Typography.Text>
|
</Typography.Text>
|
||||||
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
|
</FormItem>
|
||||||
{renderPromMatchOpts()}
|
);
|
||||||
</Typography.Text>
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -15,154 +15,130 @@ function UserGuide({ queryType }: UserGuideProps): JSX.Element {
|
|||||||
// init namespace for translations
|
// init namespace for translations
|
||||||
const { t } = useTranslation('alerts');
|
const { t } = useTranslation('alerts');
|
||||||
|
|
||||||
const renderStep1QB = (): JSX.Element => {
|
const renderStep1QB = (): JSX.Element => (
|
||||||
return (
|
<>
|
||||||
<>
|
<StyledTopic>{t('user_guide_qb_step1')}</StyledTopic>
|
||||||
<StyledTopic>{t('user_guide_qb_step1')}</StyledTopic>
|
<StyledList>
|
||||||
<StyledList>
|
<StyledListItem>{t('user_guide_qb_step1a')}</StyledListItem>
|
||||||
<StyledListItem>{t('user_guide_qb_step1a')}</StyledListItem>
|
<StyledListItem>{t('user_guide_qb_step1b')}</StyledListItem>
|
||||||
<StyledListItem>{t('user_guide_qb_step1b')}</StyledListItem>
|
<StyledListItem>{t('user_guide_qb_step1c')}</StyledListItem>
|
||||||
<StyledListItem>{t('user_guide_qb_step1c')}</StyledListItem>
|
<StyledListItem>{t('user_guide_qb_step1d')}</StyledListItem>
|
||||||
<StyledListItem>{t('user_guide_qb_step1d')}</StyledListItem>
|
</StyledList>
|
||||||
</StyledList>
|
</>
|
||||||
</>
|
);
|
||||||
);
|
const renderStep2QB = (): JSX.Element => (
|
||||||
};
|
<>
|
||||||
const renderStep2QB = (): JSX.Element => {
|
<StyledTopic>{t('user_guide_qb_step2')}</StyledTopic>
|
||||||
return (
|
<StyledList>
|
||||||
<>
|
<StyledListItem>{t('user_guide_qb_step2a')}</StyledListItem>
|
||||||
<StyledTopic>{t('user_guide_qb_step2')}</StyledTopic>
|
<StyledListItem>{t('user_guide_qb_step2b')}</StyledListItem>
|
||||||
<StyledList>
|
</StyledList>
|
||||||
<StyledListItem>{t('user_guide_qb_step2a')}</StyledListItem>
|
</>
|
||||||
<StyledListItem>{t('user_guide_qb_step2b')}</StyledListItem>
|
);
|
||||||
</StyledList>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderStep3QB = (): JSX.Element => {
|
const renderStep3QB = (): JSX.Element => (
|
||||||
return (
|
<>
|
||||||
<>
|
<StyledTopic>{t('user_guide_qb_step3')}</StyledTopic>
|
||||||
<StyledTopic>{t('user_guide_qb_step3')}</StyledTopic>
|
<StyledList>
|
||||||
<StyledList>
|
<StyledListItem>{t('user_guide_qb_step3a')}</StyledListItem>
|
||||||
<StyledListItem>{t('user_guide_qb_step3a')}</StyledListItem>
|
<StyledListItem>{t('user_guide_qb_step3b')}</StyledListItem>
|
||||||
<StyledListItem>{t('user_guide_qb_step3b')}</StyledListItem>
|
</StyledList>
|
||||||
</StyledList>
|
</>
|
||||||
</>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderGuideForQB = (): JSX.Element => {
|
const renderGuideForQB = (): JSX.Element => (
|
||||||
return (
|
<>
|
||||||
<>
|
{renderStep1QB()}
|
||||||
{renderStep1QB()}
|
{renderStep2QB()}
|
||||||
{renderStep2QB()}
|
{renderStep3QB()}
|
||||||
{renderStep3QB()}
|
</>
|
||||||
</>
|
);
|
||||||
);
|
const renderStep1PQL = (): JSX.Element => (
|
||||||
};
|
<>
|
||||||
const renderStep1PQL = (): JSX.Element => {
|
<StyledTopic>{t('user_guide_pql_step1')}</StyledTopic>
|
||||||
return (
|
<StyledList>
|
||||||
<>
|
<StyledListItem>{t('user_guide_pql_step1a')}</StyledListItem>
|
||||||
<StyledTopic>{t('user_guide_pql_step1')}</StyledTopic>
|
<StyledListItem>{t('user_guide_pql_step1b')}</StyledListItem>
|
||||||
<StyledList>
|
</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>
|
||||||
const renderStep2PQL = (): JSX.Element => {
|
<StyledListItem>{t('user_guide_pql_step2a')}</StyledListItem>
|
||||||
return (
|
<StyledListItem>{t('user_guide_pql_step2b')}</StyledListItem>
|
||||||
<>
|
</StyledList>
|
||||||
<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 => {
|
const renderStep3PQL = (): JSX.Element => (
|
||||||
return (
|
<>
|
||||||
<>
|
<StyledTopic>{t('user_guide_pql_step3')}</StyledTopic>
|
||||||
<StyledTopic>{t('user_guide_pql_step3')}</StyledTopic>
|
<StyledList>
|
||||||
<StyledList>
|
<StyledListItem>{t('user_guide_pql_step3a')}</StyledListItem>
|
||||||
<StyledListItem>{t('user_guide_pql_step3a')}</StyledListItem>
|
<StyledListItem>{t('user_guide_pql_step3b')}</StyledListItem>
|
||||||
<StyledListItem>{t('user_guide_pql_step3b')}</StyledListItem>
|
</StyledList>
|
||||||
</StyledList>
|
</>
|
||||||
</>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderGuideForPQL = (): JSX.Element => {
|
const renderGuideForPQL = (): JSX.Element => (
|
||||||
return (
|
<>
|
||||||
<>
|
{renderStep1PQL()}
|
||||||
{renderStep1PQL()}
|
{renderStep2PQL()}
|
||||||
{renderStep2PQL()}
|
{renderStep3PQL()}
|
||||||
{renderStep3PQL()}
|
</>
|
||||||
</>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderStep1CH = (): JSX.Element => {
|
const renderStep1CH = (): JSX.Element => (
|
||||||
return (
|
<>
|
||||||
<>
|
<StyledTopic>{t('user_guide_ch_step1')}</StyledTopic>
|
||||||
<StyledTopic>{t('user_guide_ch_step1')}</StyledTopic>
|
<StyledList>
|
||||||
<StyledList>
|
<StyledListItem>
|
||||||
<StyledListItem>
|
<Trans
|
||||||
<Trans
|
i18nKey="user_guide_ch_step1a"
|
||||||
i18nKey="user_guide_ch_step1a"
|
t={t}
|
||||||
t={t}
|
components={[
|
||||||
components={[
|
// eslint-disable-next-line jsx-a11y/control-has-associated-label, jsx-a11y/anchor-has-content
|
||||||
// eslint-disable-next-line jsx-a11y/control-has-associated-label, jsx-a11y/anchor-has-content
|
<a
|
||||||
<a
|
key={1}
|
||||||
key={1}
|
target="_blank"
|
||||||
target="_blank"
|
href=" https://signoz.io/docs/tutorial/writing-clickhouse-queries-in-dashboard/?utm_source=frontend&utm_medium=product&utm_id=alerts</>"
|
||||||
href=" https://signoz.io/docs/tutorial/writing-clickhouse-queries-in-dashboard/?utm_source=frontend&utm_medium=product&utm_id=alerts</>"
|
/>,
|
||||||
/>,
|
]}
|
||||||
]}
|
/>
|
||||||
/>
|
</StyledListItem>
|
||||||
</StyledListItem>
|
<StyledListItem>{t('user_guide_ch_step1b')}</StyledListItem>
|
||||||
<StyledListItem>{t('user_guide_ch_step1b')}</StyledListItem>
|
</StyledList>
|
||||||
</StyledList>
|
</>
|
||||||
</>
|
);
|
||||||
);
|
const renderStep2CH = (): JSX.Element => (
|
||||||
};
|
<>
|
||||||
const renderStep2CH = (): JSX.Element => {
|
<StyledTopic>{t('user_guide_ch_step2')}</StyledTopic>
|
||||||
return (
|
<StyledList>
|
||||||
<>
|
<StyledListItem>{t('user_guide_ch_step2a')}</StyledListItem>
|
||||||
<StyledTopic>{t('user_guide_ch_step2')}</StyledTopic>
|
<StyledListItem>{t('user_guide_ch_step2b')}</StyledListItem>
|
||||||
<StyledList>
|
</StyledList>
|
||||||
<StyledListItem>{t('user_guide_ch_step2a')}</StyledListItem>
|
</>
|
||||||
<StyledListItem>{t('user_guide_ch_step2b')}</StyledListItem>
|
);
|
||||||
</StyledList>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderStep3CH = (): JSX.Element => {
|
const renderStep3CH = (): JSX.Element => (
|
||||||
return (
|
<>
|
||||||
<>
|
<StyledTopic>{t('user_guide_ch_step3')}</StyledTopic>
|
||||||
<StyledTopic>{t('user_guide_ch_step3')}</StyledTopic>
|
<StyledList>
|
||||||
<StyledList>
|
<StyledListItem>{t('user_guide_ch_step3a')}</StyledListItem>
|
||||||
<StyledListItem>{t('user_guide_ch_step3a')}</StyledListItem>
|
<StyledListItem>{t('user_guide_ch_step3b')}</StyledListItem>
|
||||||
<StyledListItem>{t('user_guide_ch_step3b')}</StyledListItem>
|
</StyledList>
|
||||||
</StyledList>
|
</>
|
||||||
</>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderGuideForCH = (): JSX.Element => {
|
const renderGuideForCH = (): JSX.Element => (
|
||||||
return (
|
<>
|
||||||
<>
|
{renderStep1CH()}
|
||||||
{renderStep1CH()}
|
{renderStep2CH()}
|
||||||
{renderStep2CH()}
|
{renderStep3CH()}
|
||||||
{renderStep3CH()}
|
</>
|
||||||
</>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<StyledMainContainer>
|
<StyledMainContainer>
|
||||||
<Row>
|
<Row>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
||||||
import { FormInstance, Modal, notification, Typography } from 'antd';
|
import { Col, FormInstance, Modal, notification, Typography } from 'antd';
|
||||||
import saveAlertApi from 'api/alerts/save';
|
import saveAlertApi from 'api/alerts/save';
|
||||||
import testAlertApi from 'api/alerts/testAlert';
|
import testAlertApi from 'api/alerts/testAlert';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
@@ -34,7 +34,6 @@ import {
|
|||||||
MainFormContainer,
|
MainFormContainer,
|
||||||
PanelContainer,
|
PanelContainer,
|
||||||
StyledLeftContainer,
|
StyledLeftContainer,
|
||||||
StyledRightContainer,
|
|
||||||
} from './styles';
|
} from './styles';
|
||||||
import useDebounce from './useDebounce';
|
import useDebounce from './useDebounce';
|
||||||
import UserGuide from './UserGuide';
|
import UserGuide from './UserGuide';
|
||||||
@@ -437,41 +436,35 @@ function FormAlertRules({
|
|||||||
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
|
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderQBChartPreview = (): JSX.Element => {
|
const renderQBChartPreview = (): JSX.Element => (
|
||||||
return (
|
<ChartPreview
|
||||||
<ChartPreview
|
headline={<PlotTag queryType={queryCategory} />}
|
||||||
headline={<PlotTag queryType={queryCategory} />}
|
name=""
|
||||||
name=""
|
threshold={alertDef.condition?.target}
|
||||||
threshold={alertDef.condition?.target}
|
query={debouncedStagedQuery}
|
||||||
query={debouncedStagedQuery}
|
selectedInterval={toChartInterval(alertDef.evalWindow)}
|
||||||
selectedInterval={toChartInterval(alertDef.evalWindow)}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderPromChartPreview = (): JSX.Element => {
|
const renderPromChartPreview = (): JSX.Element => (
|
||||||
return (
|
<ChartPreview
|
||||||
<ChartPreview
|
headline={<PlotTag queryType={queryCategory} />}
|
||||||
headline={<PlotTag queryType={queryCategory} />}
|
name="Chart Preview"
|
||||||
name="Chart Preview"
|
threshold={alertDef.condition?.target}
|
||||||
threshold={alertDef.condition?.target}
|
query={debouncedStagedQuery}
|
||||||
query={debouncedStagedQuery}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderChQueryChartPreview = (): JSX.Element => {
|
const renderChQueryChartPreview = (): JSX.Element => (
|
||||||
return (
|
<ChartPreview
|
||||||
<ChartPreview
|
headline={<PlotTag queryType={queryCategory} />}
|
||||||
headline={<PlotTag queryType={queryCategory} />}
|
name="Chart Preview"
|
||||||
name="Chart Preview"
|
threshold={alertDef.condition?.target}
|
||||||
threshold={alertDef.condition?.target}
|
query={manualStagedQuery}
|
||||||
query={manualStagedQuery}
|
userQueryKey={runQueryId}
|
||||||
userQueryKey={runQueryId}
|
selectedInterval={toChartInterval(alertDef.evalWindow)}
|
||||||
selectedInterval={toChartInterval(alertDef.evalWindow)}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{Element}
|
{Element}
|
||||||
@@ -535,9 +528,9 @@ function FormAlertRules({
|
|||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
</MainFormContainer>
|
</MainFormContainer>
|
||||||
</StyledLeftContainer>
|
</StyledLeftContainer>
|
||||||
<StyledRightContainer flex="1 1 300px">
|
<Col flex="1 1 300px">
|
||||||
<UserGuide queryType={queryCategory} />
|
<UserGuide queryType={queryCategory} />
|
||||||
</StyledRightContainer>
|
</Col>
|
||||||
</PanelContainer>
|
</PanelContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useMachine } from '@xstate/react';
|
import { useMachine } from '@xstate/react';
|
||||||
import { Button, Input, message, Modal } from 'antd';
|
import { Button, Input, message, Modal } from 'antd';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { map } from 'lodash-es';
|
import { map } from 'lodash-es';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import { Labels } from 'types/api/alerts/def';
|
import { Labels } from 'types/api/alerts/def';
|
||||||
import AppReducer from 'types/reducer/app';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import { ResourceAttributesFilterMachine } from './Labels.machine';
|
import { ResourceAttributesFilterMachine } from './Labels.machine';
|
||||||
@@ -29,7 +27,8 @@ function LabelSelect({
|
|||||||
initialValues,
|
initialValues,
|
||||||
}: LabelSelectProps): JSX.Element | null {
|
}: LabelSelectProps): JSX.Element | null {
|
||||||
const { t } = useTranslation('alerts');
|
const { t } = useTranslation('alerts');
|
||||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const [currentVal, setCurrentVal] = useState('');
|
const [currentVal, setCurrentVal] = useState('');
|
||||||
const [staging, setStaging] = useState<string[]>([]);
|
const [staging, setStaging] = useState<string[]>([]);
|
||||||
const [queries, setQueries] = useState<ILabelRecord[]>(
|
const [queries, setQueries] = useState<ILabelRecord[]>(
|
||||||
@@ -120,17 +119,15 @@ function LabelSelect({
|
|||||||
{queries.length > 0 &&
|
{queries.length > 0 &&
|
||||||
map(
|
map(
|
||||||
queries,
|
queries,
|
||||||
(query): JSX.Element => {
|
(query): JSX.Element => (
|
||||||
return (
|
<QueryChip key={query.key} queryData={query} onRemove={handleClose} />
|
||||||
<QueryChip key={query.key} queryData={query} onRemove={handleClose} />
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{map(staging, (item) => {
|
{map(staging, (item) => (
|
||||||
return <QueryChipItem key={uuid()}>{item}</QueryChipItem>;
|
<QueryChipItem key={uuid()}>{item}</QueryChipItem>
|
||||||
})}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', width: '100%' }}>
|
<div style={{ display: 'flex', width: '100%' }}>
|
||||||
|
|||||||
@@ -9,19 +9,15 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import FormItem from 'antd/lib/form/FormItem';
|
|
||||||
import TextArea from 'antd/lib/input/TextArea';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const { TextArea } = Input;
|
||||||
|
const { Item } = Form;
|
||||||
|
|
||||||
export const PanelContainer = styled(Row)`
|
export const PanelContainer = styled(Row)`
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledRightContainer = styled(Col)`
|
|
||||||
&&& {
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const StyledLeftContainer = styled(Col)`
|
export const StyledLeftContainer = styled(Col)`
|
||||||
&&& {
|
&&& {
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
@@ -111,7 +107,7 @@ export const TextareaMedium = styled(TextArea)`
|
|||||||
width: 70%;
|
width: 70%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FormItemMedium = styled(FormItem)`
|
export const FormItemMedium = styled(Item)`
|
||||||
width: 70%;
|
width: 70%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -42,17 +42,15 @@ export const toMetricQueries = (b: IBuilderQueries): IMetricQueries => {
|
|||||||
|
|
||||||
export const toIMetricsBuilderQuery = (
|
export const toIMetricsBuilderQuery = (
|
||||||
q: IMetricQuery,
|
q: IMetricQuery,
|
||||||
): IMetricsBuilderQuery => {
|
): IMetricsBuilderQuery => ({
|
||||||
return {
|
name: q.name,
|
||||||
name: q.name,
|
metricName: q.metricName,
|
||||||
metricName: q.metricName,
|
tagFilters: q.tagFilters,
|
||||||
tagFilters: q.tagFilters,
|
groupBy: q.groupBy,
|
||||||
groupBy: q.groupBy,
|
aggregateOperator: q.aggregateOperator,
|
||||||
aggregateOperator: q.aggregateOperator,
|
disabled: q.disabled,
|
||||||
disabled: q.disabled,
|
legend: q.legend,
|
||||||
legend: q.legend,
|
});
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const prepareBuilderQueries = (
|
export const prepareBuilderQueries = (
|
||||||
m: IMetricQueries,
|
m: IMetricQueries,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
||||||
import useThemeMode from 'hooks/useThemeMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { toFixed } from 'utils/toFixed';
|
import { toFixed } from 'utils/toFixed';
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ interface SpanLengthProps {
|
|||||||
|
|
||||||
function SpanLength(props: SpanLengthProps): JSX.Element {
|
function SpanLength(props: SpanLengthProps): JSX.Element {
|
||||||
const { width, leftOffset, bgColor, inMsCount } = props;
|
const { width, leftOffset, bgColor, inMsCount } = props;
|
||||||
const { isDarkMode } = useThemeMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
|
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
|
||||||
return (
|
return (
|
||||||
<SpanWrapper>
|
<SpanWrapper>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { CaretDownFilled, CaretRightFilled } from '@ant-design/icons';
|
import { CaretDownFilled, CaretRightFilled } from '@ant-design/icons';
|
||||||
import { Col } from 'antd';
|
import { Col, Typography } from 'antd';
|
||||||
import { StyledCol, StyledRow } from 'components/Styled';
|
import { StyledCol, StyledRow } from 'components/Styled';
|
||||||
import { IIntervalUnit } from 'container/TraceDetail/utils';
|
import { IIntervalUnit } from 'container/TraceDetail/utils';
|
||||||
import useThemeMode from 'hooks/useThemeMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants';
|
import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { ITraceTree } from 'types/api/trace/getTraceItem';
|
import { ITraceTree } from 'types/api/trace/getTraceItem';
|
||||||
|
|
||||||
import { ITraceMetaData } from '..';
|
import { ITraceMetaData } from '..';
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
styles,
|
styles,
|
||||||
Wrapper,
|
Wrapper,
|
||||||
} from './styles';
|
} from './styles';
|
||||||
|
import { getIconStyles } from './utils';
|
||||||
|
|
||||||
function Trace(props: TraceProps): JSX.Element {
|
function Trace(props: TraceProps): JSX.Element {
|
||||||
const {
|
const {
|
||||||
@@ -42,7 +43,8 @@ function Trace(props: TraceProps): JSX.Element {
|
|||||||
isMissing,
|
isMissing,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { isDarkMode } = useThemeMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const [isOpen, setOpen] = useState<boolean>(activeSpanPath[level] === id);
|
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.
|
const localTreeExpandInteraction = useRef<boolean | 0>(0); // Boolean is for the state of the expansion whereas the number i.e. 0 is for skipping the user interaction.
|
||||||
@@ -111,6 +113,18 @@ function Trace(props: TraceProps): JSX.Element {
|
|||||||
const width = (value * 1e2) / (globalSpread * 1e6);
|
const width = (value * 1e2) / (globalSpread * 1e6);
|
||||||
const panelWidth = SPAN_DETAILS_LEFT_COL_WIDTH - level * (16 + 1) - 48;
|
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 (
|
return (
|
||||||
<Wrapper
|
<Wrapper
|
||||||
onMouseEnter={onMouseEnterHandler}
|
onMouseEnter={onMouseEnterHandler}
|
||||||
@@ -136,10 +150,8 @@ function Trace(props: TraceProps): JSX.Element {
|
|||||||
isDarkMode={isDarkMode}
|
isDarkMode={isDarkMode}
|
||||||
onClick={onClickTreeExpansion}
|
onClick={onClickTreeExpansion}
|
||||||
>
|
>
|
||||||
{totalSpans}
|
<Typography>{totalSpans}</Typography>
|
||||||
<CaretContainer>
|
<CaretContainer>{icon}</CaretContainer>
|
||||||
{isOpen ? <CaretDownFilled /> : <CaretRightFilled />}
|
|
||||||
</CaretContainer>
|
|
||||||
</CardComponent>
|
</CardComponent>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
3
frontend/src/container/GantChart/Trace/utils.ts
Normal file
3
frontend/src/container/GantChart/Trace/utils.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const getIconStyles = (isDarkMode: boolean): Record<string, string> => ({
|
||||||
|
color: isDarkMode ? 'white' : 'black',
|
||||||
|
});
|
||||||
@@ -112,21 +112,19 @@ export const getNodeById = (
|
|||||||
|
|
||||||
const getSpanWithoutChildren = (
|
const getSpanWithoutChildren = (
|
||||||
span: ITraceTree,
|
span: ITraceTree,
|
||||||
): Omit<ITraceTree, 'children'> => {
|
): Omit<ITraceTree, 'children'> => ({
|
||||||
return {
|
id: span.id,
|
||||||
id: span.id,
|
name: span.name,
|
||||||
name: span.name,
|
parent: span.parent,
|
||||||
parent: span.parent,
|
serviceColour: span.serviceColour,
|
||||||
serviceColour: span.serviceColour,
|
serviceName: span.serviceName,
|
||||||
serviceName: span.serviceName,
|
startTime: span.startTime,
|
||||||
startTime: span.startTime,
|
tags: span.tags,
|
||||||
tags: span.tags,
|
time: span.time,
|
||||||
time: span.time,
|
value: span.value,
|
||||||
value: span.value,
|
event: span.event,
|
||||||
event: span.event,
|
hasError: span.hasError,
|
||||||
hasError: span.hasError,
|
});
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isSpanPresentInSearchString = (
|
export const isSpanPresentInSearchString = (
|
||||||
searchedString: string,
|
searchedString: string,
|
||||||
|
|||||||
@@ -572,7 +572,7 @@ function GeneralSettings({
|
|||||||
onOkHandler(category.name.toLowerCase() as TTTLType)
|
onOkHandler(category.name.toLowerCase() as TTTLType)
|
||||||
}
|
}
|
||||||
centered
|
centered
|
||||||
visible={category.save.modal}
|
open={category.save.modal}
|
||||||
confirmLoading={category.save.apiLoading}
|
confirmLoading={category.save.apiLoading}
|
||||||
>
|
>
|
||||||
<Typography>
|
<Typography>
|
||||||
@@ -590,20 +590,22 @@ function GeneralSettings({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
|
<>
|
||||||
{Element}
|
{Element}
|
||||||
<ErrorTextContainer>
|
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
|
||||||
<TextToolTip
|
<ErrorTextContainer>
|
||||||
{...{
|
<TextToolTip
|
||||||
text: `More details on how to set retention period`,
|
{...{
|
||||||
url: 'https://signoz.io/docs/userguide/retention-period/',
|
text: `More details on how to set retention period`,
|
||||||
}}
|
url: 'https://signoz.io/docs/userguide/retention-period/',
|
||||||
/>
|
}}
|
||||||
{errorText && <ErrorText>{errorText}</ErrorText>}
|
/>
|
||||||
</ErrorTextContainer>
|
{errorText && <ErrorText>{errorText}</ErrorText>}
|
||||||
|
</ErrorTextContainer>
|
||||||
|
|
||||||
<Row justify="start">{renderConfig}</Row>
|
<Row justify="start">{renderConfig}</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ function GridGraphComponent({
|
|||||||
name,
|
name,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
staticLine,
|
staticLine,
|
||||||
|
onDragSelect,
|
||||||
}: GridGraphComponentProps): JSX.Element | null {
|
}: GridGraphComponentProps): JSX.Element | null {
|
||||||
const location = history.location.pathname;
|
const location = history.location.pathname;
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ function GridGraphComponent({
|
|||||||
name,
|
name,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
staticLine,
|
staticLine,
|
||||||
|
onDragSelect,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -85,6 +87,7 @@ export interface GridGraphComponentProps {
|
|||||||
name: string;
|
name: string;
|
||||||
yAxisUnit?: string;
|
yAxisUnit?: string;
|
||||||
staticLine?: StaticLineProps;
|
staticLine?: StaticLineProps;
|
||||||
|
onDragSelect?: (start: number, end: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
GridGraphComponent.defaultProps = {
|
GridGraphComponent.defaultProps = {
|
||||||
@@ -94,6 +97,7 @@ GridGraphComponent.defaultProps = {
|
|||||||
onClickHandler: undefined,
|
onClickHandler: undefined,
|
||||||
yAxisUnit: undefined,
|
yAxisUnit: undefined,
|
||||||
staticLine: undefined,
|
staticLine: undefined,
|
||||||
|
onDragSelect: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GridGraphComponent;
|
export default GridGraphComponent;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button, Typography } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { GraphOnClickHandler } from 'components/Graph';
|
import { GraphOnClickHandler } from 'components/Graph';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import TimePreference from 'components/TimePreferenceDropDown';
|
import TimePreference from 'components/TimePreferenceDropDown';
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from 'container/NewWidget/RightContainer/timeItems';
|
} from 'container/NewWidget/RightContainer/timeItems';
|
||||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||||
import getChartData from 'lib/getChartData';
|
import getChartData from 'lib/getChartData';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
|
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
|
||||||
@@ -19,7 +19,7 @@ import { Widgets } from 'types/api/dashboard/getAll';
|
|||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
import { NotFoundContainer, TimeContainer } from './styles';
|
import { TimeContainer } from './styles';
|
||||||
|
|
||||||
function FullView({
|
function FullView({
|
||||||
widget,
|
widget,
|
||||||
@@ -27,6 +27,7 @@ function FullView({
|
|||||||
onClickHandler,
|
onClickHandler,
|
||||||
name,
|
name,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
|
onDragSelect,
|
||||||
}: FullViewProps): JSX.Element {
|
}: FullViewProps): JSX.Element {
|
||||||
const { selectedTime: globalSelectedTime } = useSelector<
|
const { selectedTime: globalSelectedTime } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
@@ -57,20 +58,23 @@ function FullView({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const isError = response?.error;
|
const chartDataSet = useMemo(
|
||||||
|
() =>
|
||||||
|
getChartData({
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
queryData: response?.data?.payload?.data?.result || [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[response],
|
||||||
|
);
|
||||||
|
|
||||||
const isLoading = response.isLoading === true;
|
const isLoading = response.isLoading === true;
|
||||||
const errorMessage = isError instanceof Error ? isError?.message : '';
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Spinner height="100%" size="large" tip="Loading..." />;
|
return <Spinner height="100%" size="large" tip="Loading..." />;
|
||||||
}
|
}
|
||||||
if (isError || !response?.data?.payload?.data?.result) {
|
|
||||||
return (
|
|
||||||
<NotFoundContainer>
|
|
||||||
<Typography>{errorMessage}</Typography>
|
|
||||||
</NotFoundContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -94,24 +98,15 @@ function FullView({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<GridGraphComponent
|
<GridGraphComponent
|
||||||
{...{
|
GRAPH_TYPES={widget.panelTypes}
|
||||||
GRAPH_TYPES: widget.panelTypes,
|
data={chartDataSet}
|
||||||
data: getChartData({
|
isStacked={widget.isStacked}
|
||||||
queryData: [
|
opacity={widget.opacity}
|
||||||
{
|
title={widget.title}
|
||||||
queryData: response.data?.payload?.data?.result
|
onClickHandler={onClickHandler}
|
||||||
? response.data?.payload?.data?.result
|
name={name}
|
||||||
: [],
|
yAxisUnit={yAxisUnit}
|
||||||
},
|
onDragSelect={onDragSelect}
|
||||||
],
|
|
||||||
}),
|
|
||||||
isStacked: widget.isStacked,
|
|
||||||
opacity: widget.opacity,
|
|
||||||
title: widget.title,
|
|
||||||
onClickHandler,
|
|
||||||
name,
|
|
||||||
yAxisUnit,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -123,12 +118,14 @@ interface FullViewProps {
|
|||||||
onClickHandler?: GraphOnClickHandler;
|
onClickHandler?: GraphOnClickHandler;
|
||||||
name: string;
|
name: string;
|
||||||
yAxisUnit?: string;
|
yAxisUnit?: string;
|
||||||
|
onDragSelect?: (start: number, end: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
FullView.defaultProps = {
|
FullView.defaultProps = {
|
||||||
fullViewOptions: undefined,
|
fullViewOptions: undefined,
|
||||||
onClickHandler: undefined,
|
onClickHandler: undefined,
|
||||||
yAxisUnit: undefined,
|
yAxisUnit: undefined,
|
||||||
|
onDragSelect: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FullView;
|
export default FullView;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import GetMaxMinTime from 'lib/getMaxMinTime';
|
|||||||
import GetMinMax from 'lib/getMinMax';
|
import GetMinMax from 'lib/getMinMax';
|
||||||
import getStartAndEndTime from 'lib/getStartAndEndTime';
|
import getStartAndEndTime from 'lib/getStartAndEndTime';
|
||||||
import getStep from 'lib/getStep';
|
import getStep from 'lib/getStep';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useQueries } from 'react-query';
|
import { useQueries } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@@ -30,6 +30,7 @@ function FullView({
|
|||||||
onClickHandler,
|
onClickHandler,
|
||||||
name,
|
name,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
|
onDragSelect,
|
||||||
}: FullViewProps): JSX.Element {
|
}: FullViewProps): JSX.Element {
|
||||||
const { minTime, maxTime, selectedTime: globalSelectedTime } = useSelector<
|
const { minTime, maxTime, selectedTime: globalSelectedTime } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
@@ -80,25 +81,22 @@ function FullView({
|
|||||||
const queryLength = widget.query.filter((e) => e.query.length !== 0);
|
const queryLength = widget.query.filter((e) => e.query.length !== 0);
|
||||||
|
|
||||||
const response = useQueries(
|
const response = useQueries(
|
||||||
queryLength.map((query) => {
|
queryLength.map((query) => ({
|
||||||
return {
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
queryFn: () =>
|
||||||
queryFn: () => {
|
getQueryResult({
|
||||||
return getQueryResult({
|
end: queryMinMax.max.toString(),
|
||||||
end: queryMinMax.max.toString(),
|
query: query.query,
|
||||||
query: query.query,
|
start: queryMinMax.min.toString(),
|
||||||
start: queryMinMax.min.toString(),
|
step: `${getStep({
|
||||||
step: `${getStep({
|
start: queryMinMax.min,
|
||||||
start: queryMinMax.min,
|
end: queryMinMax.max,
|
||||||
end: queryMinMax.max,
|
inputFormat: 's',
|
||||||
inputFormat: 's',
|
})}`,
|
||||||
})}`,
|
}),
|
||||||
});
|
queryHash: `${query.query}-${query.legend}-${selectedTime.enum}`,
|
||||||
},
|
retryOnMount: false,
|
||||||
queryHash: `${query.query}-${query.legend}-${selectedTime.enum}`,
|
})),
|
||||||
retryOnMount: false,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const isError =
|
const isError =
|
||||||
@@ -117,6 +115,18 @@ function FullView({
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const chartDataSet = useMemo(
|
||||||
|
() =>
|
||||||
|
getChartData({
|
||||||
|
queryData: data.map((e) => ({
|
||||||
|
query: e?.map((e) => e.query).join(' ') || '',
|
||||||
|
queryData: e?.map((e) => e.queryData) || [],
|
||||||
|
legend: e?.map((e) => e.legend).join('') || '',
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <Spinner height="100%" size="large" tip="Loading..." />;
|
return <Spinner height="100%" size="large" tip="Loading..." />;
|
||||||
}
|
}
|
||||||
@@ -151,22 +161,15 @@ function FullView({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<GridGraphComponent
|
<GridGraphComponent
|
||||||
{...{
|
GRAPH_TYPES={widget.panelTypes}
|
||||||
GRAPH_TYPES: widget.panelTypes,
|
data={chartDataSet}
|
||||||
data: getChartData({
|
isStacked={widget.isStacked}
|
||||||
queryData: data.map((e) => ({
|
opacity={widget.opacity}
|
||||||
query: e?.map((e) => e.query).join(' ') || '',
|
title={widget.title}
|
||||||
queryData: e?.map((e) => e.queryData) || [],
|
onClickHandler={onClickHandler}
|
||||||
legend: e?.map((e) => e.legend).join('') || '',
|
name={name}
|
||||||
})),
|
yAxisUnit={yAxisUnit}
|
||||||
}),
|
onDragSelect={onDragSelect}
|
||||||
isStacked: widget.isStacked,
|
|
||||||
opacity: widget.opacity,
|
|
||||||
title: widget.title,
|
|
||||||
onClickHandler,
|
|
||||||
name,
|
|
||||||
yAxisUnit,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -178,12 +181,14 @@ interface FullViewProps {
|
|||||||
onClickHandler?: GraphOnClickHandler;
|
onClickHandler?: GraphOnClickHandler;
|
||||||
name: string;
|
name: string;
|
||||||
yAxisUnit?: string;
|
yAxisUnit?: string;
|
||||||
|
onDragSelect?: (start: number, end: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
FullView.defaultProps = {
|
FullView.defaultProps = {
|
||||||
fullViewOptions: undefined,
|
fullViewOptions: undefined,
|
||||||
onClickHandler: undefined,
|
onClickHandler: undefined,
|
||||||
yAxisUnit: undefined,
|
yAxisUnit: undefined,
|
||||||
|
onDragSelect: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FullView;
|
export default FullView;
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const GraphContainer = styled.div`
|
|
||||||
min-height: 70vh;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const NotFoundContainer = styled.div`
|
export const NotFoundContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ChartData } from 'chart.js';
|
import { ChartData } from 'chart.js';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import GridGraphComponent from 'container/GridGraphComponent';
|
import GridGraphComponent from 'container/GridGraphComponent';
|
||||||
|
import usePreviousValue from 'hooks/usePreviousValue';
|
||||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||||
import getChartData from 'lib/getChartData';
|
import getChartData from 'lib/getChartData';
|
||||||
import isEmpty from 'lodash-es/isEmpty';
|
import isEmpty from 'lodash-es/isEmpty';
|
||||||
import React, { memo, useCallback, useEffect, useState } from 'react';
|
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { Layout } from 'react-grid-layout';
|
import { Layout } from 'react-grid-layout';
|
||||||
|
import { useInView } from 'react-intersection-observer';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
import { connect, useSelector } from 'react-redux';
|
import { connect, useSelector } from 'react-redux';
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
import { ThunkDispatch } from 'redux-thunk';
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
@@ -20,13 +22,14 @@ import { AppState } from 'store/reducers';
|
|||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import { GlobalTime } from 'types/actions/globalTime';
|
import { GlobalTime } from 'types/actions/globalTime';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
import DashboardReducer from 'types/reducer/dashboards';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
import { LayoutProps } from '..';
|
import { LayoutProps } from '..';
|
||||||
import EmptyWidget from '../EmptyWidget';
|
import EmptyWidget from '../EmptyWidget';
|
||||||
import WidgetHeader from '../WidgetHeader';
|
import WidgetHeader from '../WidgetHeader';
|
||||||
import FullView from './FullView/index.metricsBuilder';
|
import FullView from './FullView/index.metricsBuilder';
|
||||||
import { ErrorContainer, FullViewContainer, Modal } from './styles';
|
import { FullViewContainer, Modal } from './styles';
|
||||||
|
|
||||||
function GridCardGraph({
|
function GridCardGraph({
|
||||||
widget,
|
widget,
|
||||||
@@ -35,13 +38,14 @@ function GridCardGraph({
|
|||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
layout = [],
|
layout = [],
|
||||||
setLayout,
|
setLayout,
|
||||||
|
onDragSelect,
|
||||||
}: GridCardGraphProps): JSX.Element {
|
}: GridCardGraphProps): JSX.Element {
|
||||||
const [state, setState] = useState<GridCardGraphState>({
|
const { ref: graphRef, inView: isGraphVisible } = useInView({
|
||||||
loading: true,
|
threshold: 0,
|
||||||
errorMessage: '',
|
triggerOnce: true,
|
||||||
error: false,
|
|
||||||
payload: undefined,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | undefined>('');
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const [modal, setModal] = useState(false);
|
const [modal, setModal] = useState(false);
|
||||||
const [deleteModal, setDeleteModal] = useState(false);
|
const [deleteModal, setDeleteModal] = useState(false);
|
||||||
@@ -53,113 +57,57 @@ function GridCardGraph({
|
|||||||
AppState,
|
AppState,
|
||||||
GlobalReducer
|
GlobalReducer
|
||||||
>((state) => state.globalTime);
|
>((state) => state.globalTime);
|
||||||
|
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||||
|
(state) => state.dashboards,
|
||||||
|
);
|
||||||
|
const [selectedDashboard] = dashboards;
|
||||||
|
const selectedData = selectedDashboard?.data;
|
||||||
|
const { variables } = selectedData;
|
||||||
|
|
||||||
// const getMaxMinTime = GetMaxMinTime({
|
const queryResponse = useQuery(
|
||||||
// graphType: widget?.panelTypes,
|
[
|
||||||
// maxTime,
|
`GetMetricsQueryRange-${widget.timePreferance}-${globalSelectedInterval}-${widget.id}`,
|
||||||
// minTime,
|
{
|
||||||
// });
|
widget,
|
||||||
|
maxTime,
|
||||||
// const { start, end } = GetStartAndEndTime({
|
minTime,
|
||||||
// type: widget?.timePreferance,
|
globalSelectedInterval,
|
||||||
// maxTime: getMaxMinTime.maxTime,
|
variables,
|
||||||
// minTime: getMaxMinTime.minTime,
|
},
|
||||||
// });
|
],
|
||||||
|
() =>
|
||||||
// const queryLength = widget?.query?.filter((e) => e.query.length !== 0) || [];
|
GetMetricQueryRange({
|
||||||
|
selectedTime: widget.timePreferance,
|
||||||
// const response = useQueries(
|
graphType: widget.panelTypes,
|
||||||
// queryLength?.map((query) => {
|
query: widget.query,
|
||||||
// return {
|
globalSelectedInterval,
|
||||||
// // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
variables: getDashboardVariables(),
|
||||||
// queryFn: () => {
|
}),
|
||||||
// return getQueryResult({
|
{
|
||||||
// end,
|
keepPreviousData: true,
|
||||||
// query: query?.query,
|
enabled: isGraphVisible,
|
||||||
// start,
|
refetchOnMount: false,
|
||||||
// step: '60',
|
onError: (error) => {
|
||||||
// });
|
if (error instanceof Error) {
|
||||||
// },
|
setErrorMessage(error.message);
|
||||||
// queryHash: `${query?.query}-${query?.legend}-${start}-${end}`,
|
|
||||||
// retryOnMount: false,
|
|
||||||
// };
|
|
||||||
// }),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const isError =
|
|
||||||
// response.find((e) => e?.data?.statusCode !== 200) !== undefined ||
|
|
||||||
// response.some((e) => e.isError === true);
|
|
||||||
|
|
||||||
// const isLoading = response.some((e) => e.isLoading === true);
|
|
||||||
|
|
||||||
// const errorMessage = response.find((e) => e.data?.error !== null)?.data?.error;
|
|
||||||
|
|
||||||
// const data = response.map((responseOfQuery) =>
|
|
||||||
// responseOfQuery?.data?.payload?.result.map((e, index) => ({
|
|
||||||
// query: queryLength[index]?.query,
|
|
||||||
// queryData: e,
|
|
||||||
// legend: queryLength[index]?.legend,
|
|
||||||
// })),
|
|
||||||
// );
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async (): Promise<void> => {
|
|
||||||
try {
|
|
||||||
setState((state) => ({
|
|
||||||
...state,
|
|
||||||
error: false,
|
|
||||||
errorMessage: '',
|
|
||||||
loading: true,
|
|
||||||
}));
|
|
||||||
const response = await GetMetricQueryRange({
|
|
||||||
selectedTime: widget.timePreferance,
|
|
||||||
graphType: widget.panelTypes,
|
|
||||||
query: widget.query,
|
|
||||||
globalSelectedInterval,
|
|
||||||
variables: getDashboardVariables(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const isError = response.error;
|
|
||||||
|
|
||||||
if (isError != null) {
|
|
||||||
setState((state) => ({
|
|
||||||
...state,
|
|
||||||
error: true,
|
|
||||||
errorMessage: isError || 'Something went wrong',
|
|
||||||
loading: false,
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
const chartDataSet = getChartData({
|
|
||||||
queryData: [
|
|
||||||
{
|
|
||||||
queryData: response.payload?.data?.result
|
|
||||||
? response.payload?.data?.result
|
|
||||||
: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
setState((state) => ({
|
|
||||||
...state,
|
|
||||||
loading: false,
|
|
||||||
payload: chartDataSet,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
},
|
||||||
setState((state) => ({
|
},
|
||||||
...state,
|
);
|
||||||
error: true,
|
|
||||||
errorMessage: (error as AxiosError).toString(),
|
const chartData = useMemo(
|
||||||
loading: false,
|
() =>
|
||||||
}));
|
getChartData({
|
||||||
} finally {
|
queryData: [
|
||||||
setState((state) => ({
|
{
|
||||||
...state,
|
queryData: queryResponse?.data?.payload?.data?.result || [],
|
||||||
loading: false,
|
},
|
||||||
}));
|
],
|
||||||
}
|
}),
|
||||||
})();
|
[queryResponse],
|
||||||
}, [widget, maxTime, minTime, globalSelectedInterval]);
|
);
|
||||||
|
|
||||||
|
const prevChartDataSetRef = usePreviousValue<ChartData>(chartData);
|
||||||
|
|
||||||
const onToggleModal = useCallback(
|
const onToggleModal = useCallback(
|
||||||
(func: React.Dispatch<React.SetStateAction<boolean>>) => {
|
(func: React.Dispatch<React.SetStateAction<boolean>>) => {
|
||||||
@@ -177,70 +125,114 @@ function GridCardGraph({
|
|||||||
onToggleModal(setDeleteModal);
|
onToggleModal(setDeleteModal);
|
||||||
}, [deleteWidget, layout, onToggleModal, setLayout, widget]);
|
}, [deleteWidget, layout, onToggleModal, setLayout, widget]);
|
||||||
|
|
||||||
const getModals = (): JSX.Element => {
|
const getModals = (): JSX.Element => (
|
||||||
return (
|
<>
|
||||||
<>
|
<Modal
|
||||||
<Modal
|
destroyOnClose
|
||||||
destroyOnClose
|
onCancel={(): void => onToggleModal(setDeleteModal)}
|
||||||
onCancel={(): void => onToggleModal(setDeleteModal)}
|
open={deleteModal}
|
||||||
visible={deleteModal}
|
title="Delete"
|
||||||
title="Delete"
|
height="10vh"
|
||||||
height="10vh"
|
onOk={onDeleteHandler}
|
||||||
onOk={onDeleteHandler}
|
centered
|
||||||
centered
|
>
|
||||||
>
|
<Typography>Are you sure you want to delete this widget</Typography>
|
||||||
<Typography>Are you sure you want to delete this widget</Typography>
|
</Modal>
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title="View"
|
title="View"
|
||||||
footer={[]}
|
footer={[]}
|
||||||
centered
|
centered
|
||||||
visible={modal}
|
open={modal}
|
||||||
onCancel={(): void => onToggleModal(setModal)}
|
onCancel={(): void => onToggleModal(setModal)}
|
||||||
width="85%"
|
width="85%"
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
>
|
>
|
||||||
<FullViewContainer>
|
<FullViewContainer>
|
||||||
<FullView
|
<FullView name={`${name}expanded`} widget={widget} yAxisUnit={yAxisUnit} />
|
||||||
name={`${name}expanded`}
|
</FullViewContainer>
|
||||||
widget={widget}
|
</Modal>
|
||||||
yAxisUnit={yAxisUnit}
|
</>
|
||||||
/>
|
);
|
||||||
</FullViewContainer>
|
|
||||||
</Modal>
|
const handleOnView = (): void => {
|
||||||
</>
|
onToggleModal(setModal);
|
||||||
);
|
};
|
||||||
|
|
||||||
|
const handleOnDelete = (): void => {
|
||||||
|
onToggleModal(setDeleteModal);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget);
|
const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget);
|
||||||
|
|
||||||
if (state.error && !isEmptyLayout) {
|
if (queryResponse.isError && !isEmptyLayout) {
|
||||||
return (
|
return (
|
||||||
<>
|
<span ref={graphRef}>
|
||||||
{getModals()}
|
{getModals()}
|
||||||
<WidgetHeader
|
{!isEmpty(widget) && prevChartDataSetRef && (
|
||||||
parentHover={hovered}
|
<>
|
||||||
title={widget?.title}
|
<div className="drag-handle">
|
||||||
widget={widget}
|
<WidgetHeader
|
||||||
onView={(): void => onToggleModal(setModal)}
|
parentHover={hovered}
|
||||||
onDelete={(): void => onToggleModal(setDeleteModal)}
|
title={widget?.title}
|
||||||
/>
|
widget={widget}
|
||||||
|
onView={handleOnView}
|
||||||
<ErrorContainer>{state.errorMessage}</ErrorContainer>
|
onDelete={handleOnDelete}
|
||||||
</>
|
queryResponse={queryResponse}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<GridGraphComponent
|
||||||
|
GRAPH_TYPES={widget.panelTypes}
|
||||||
|
data={prevChartDataSetRef}
|
||||||
|
isStacked={widget.isStacked}
|
||||||
|
opacity={widget.opacity}
|
||||||
|
title={' '}
|
||||||
|
name={name}
|
||||||
|
yAxisUnit={yAxisUnit}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (prevChartDataSetRef?.labels === undefined && queryResponse.isLoading) {
|
||||||
(state.loading === true || state.payload === undefined) &&
|
return (
|
||||||
!isEmptyLayout
|
<span ref={graphRef}>
|
||||||
) {
|
{!isEmpty(widget) && prevChartDataSetRef?.labels ? (
|
||||||
return <Spinner height="20vh" tip="Loading..." />;
|
<>
|
||||||
|
<div className="drag-handle">
|
||||||
|
<WidgetHeader
|
||||||
|
parentHover={hovered}
|
||||||
|
title={widget?.title}
|
||||||
|
widget={widget}
|
||||||
|
onView={handleOnView}
|
||||||
|
onDelete={handleOnDelete}
|
||||||
|
queryResponse={queryResponse}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<GridGraphComponent
|
||||||
|
GRAPH_TYPES={widget.panelTypes}
|
||||||
|
data={prevChartDataSetRef}
|
||||||
|
isStacked={widget.isStacked}
|
||||||
|
opacity={widget.opacity}
|
||||||
|
title={' '}
|
||||||
|
name={name}
|
||||||
|
yAxisUnit={yAxisUnit}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Spinner height="20vh" tip="Loading..." />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
|
ref={graphRef}
|
||||||
onMouseOver={(): void => {
|
onMouseOver={(): void => {
|
||||||
setHovered(true);
|
setHovered(true);
|
||||||
}}
|
}}
|
||||||
@@ -255,28 +247,31 @@ function GridCardGraph({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isEmptyLayout && (
|
{!isEmptyLayout && (
|
||||||
<WidgetHeader
|
<div className="drag-handle">
|
||||||
parentHover={hovered}
|
<WidgetHeader
|
||||||
title={widget?.title}
|
parentHover={hovered}
|
||||||
widget={widget}
|
title={widget?.title}
|
||||||
onView={(): void => onToggleModal(setModal)}
|
widget={widget}
|
||||||
onDelete={(): void => onToggleModal(setDeleteModal)}
|
onView={handleOnView}
|
||||||
/>
|
onDelete={handleOnDelete}
|
||||||
|
queryResponse={queryResponse}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isEmptyLayout && getModals()}
|
{!isEmptyLayout && getModals()}
|
||||||
|
|
||||||
{!isEmpty(widget) && !!state.payload && (
|
{!isEmpty(widget) && !!queryResponse.data?.payload && (
|
||||||
<GridGraphComponent
|
<GridGraphComponent
|
||||||
{...{
|
GRAPH_TYPES={widget.panelTypes}
|
||||||
GRAPH_TYPES: widget.panelTypes,
|
data={chartData}
|
||||||
data: state.payload,
|
isStacked={widget.isStacked}
|
||||||
isStacked: widget.isStacked,
|
opacity={widget.opacity}
|
||||||
opacity: widget.opacity,
|
title={' '} // `empty title to accommodate absolutely positioned widget header
|
||||||
title: ' ', // empty title to accommodate absolutely positioned widget header
|
name={name}
|
||||||
name,
|
yAxisUnit={yAxisUnit}
|
||||||
yAxisUnit,
|
onDragSelect={onDragSelect}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -285,13 +280,6 @@ function GridCardGraph({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GridCardGraphState {
|
|
||||||
loading: boolean;
|
|
||||||
error: boolean;
|
|
||||||
errorMessage: string;
|
|
||||||
payload: ChartData | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DispatchProps {
|
interface DispatchProps {
|
||||||
deleteWidget: ({
|
deleteWidget: ({
|
||||||
widgetId,
|
widgetId,
|
||||||
@@ -306,8 +294,13 @@ interface GridCardGraphProps extends DispatchProps {
|
|||||||
layout?: Layout[];
|
layout?: Layout[];
|
||||||
// eslint-disable-next-line react/require-default-props
|
// eslint-disable-next-line react/require-default-props
|
||||||
setLayout?: React.Dispatch<React.SetStateAction<LayoutProps[]>>;
|
setLayout?: React.Dispatch<React.SetStateAction<LayoutProps[]>>;
|
||||||
|
onDragSelect?: (start: number, end: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GridCardGraph.defaultProps = {
|
||||||
|
onDragSelect: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (
|
const mapDispatchToProps = (
|
||||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||||
): DispatchProps => ({
|
): DispatchProps => ({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PlusOutlined, SaveFilled } from '@ant-design/icons';
|
import { PlusOutlined, SaveFilled } from '@ant-design/icons';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Layout } from 'react-grid-layout';
|
import { Layout } from 'react-grid-layout';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
@@ -27,7 +28,7 @@ function GraphLayout({
|
|||||||
setLayout,
|
setLayout,
|
||||||
}: GraphLayoutProps): JSX.Element {
|
}: GraphLayoutProps): JSX.Element {
|
||||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const [saveLayoutPermission, addPanelPermission] = useComponentPermission(
|
const [saveLayoutPermission, addPanelPermission] = useComponentPermission(
|
||||||
['save_layout', 'add_panel'],
|
['save_layout', 'add_panel'],
|
||||||
@@ -71,6 +72,7 @@ function GraphLayout({
|
|||||||
useCSSTransforms
|
useCSSTransforms
|
||||||
allowOverlap={false}
|
allowOverlap={false}
|
||||||
onLayoutChange={onLayoutChangeHandler}
|
onLayoutChange={onLayoutChangeHandler}
|
||||||
|
draggableHandle=".drag-handle"
|
||||||
>
|
>
|
||||||
{layouts.map(({ Component, ...rest }) => {
|
{layouts.map(({ Component, ...rest }) => {
|
||||||
const currentWidget = (widgets || [])?.find((e) => e.id === rest.i);
|
const currentWidget = (widgets || [])?.find((e) => e.id === rest.i);
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { themeColors } from 'constants/theme';
|
||||||
|
|
||||||
|
const positionCss: React.CSSProperties['position'] = 'fixed';
|
||||||
|
|
||||||
|
export const spinnerStyles = { position: positionCss, right: '0.5rem' };
|
||||||
|
export const tooltipStyles = {
|
||||||
|
fontSize: '1rem',
|
||||||
|
top: '0.313rem',
|
||||||
|
position: positionCss,
|
||||||
|
right: '0.313rem',
|
||||||
|
color: themeColors.errorColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const errorTooltipPosition = 'top';
|
||||||
|
|
||||||
|
export const overlayStyles: React.CSSProperties = {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
position: 'absolute',
|
||||||
|
};
|
||||||
@@ -2,22 +2,33 @@ import {
|
|||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
DownOutlined,
|
DownOutlined,
|
||||||
EditFilled,
|
EditFilled,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
FullscreenOutlined,
|
FullscreenOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Dropdown, Menu, Typography } from 'antd';
|
import { Dropdown, MenuProps, Tooltip, Typography } from 'antd';
|
||||||
|
import { MenuItemType } from 'antd/es/menu/hooks/useItems';
|
||||||
|
import Spinner from 'components/Spinner';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import React, { useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { UseQueryResult } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
|
import {
|
||||||
|
errorTooltipPosition,
|
||||||
|
overlayStyles,
|
||||||
|
spinnerStyles,
|
||||||
|
tooltipStyles,
|
||||||
|
} from './config';
|
||||||
import {
|
import {
|
||||||
ArrowContainer,
|
ArrowContainer,
|
||||||
HeaderContainer,
|
HeaderContainer,
|
||||||
HeaderContentContainer,
|
HeaderContentContainer,
|
||||||
MenuItemContainer,
|
|
||||||
} from './styles';
|
} from './styles';
|
||||||
|
|
||||||
type TWidgetOptions = 'view' | 'edit' | 'delete' | string;
|
type TWidgetOptions = 'view' | 'edit' | 'delete' | string;
|
||||||
@@ -27,6 +38,10 @@ interface IWidgetHeaderProps {
|
|||||||
onView: VoidFunction;
|
onView: VoidFunction;
|
||||||
onDelete: VoidFunction;
|
onDelete: VoidFunction;
|
||||||
parentHover: boolean;
|
parentHover: boolean;
|
||||||
|
queryResponse: UseQueryResult<
|
||||||
|
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
|
||||||
|
>;
|
||||||
|
errorMessage: string | undefined;
|
||||||
}
|
}
|
||||||
function WidgetHeader({
|
function WidgetHeader({
|
||||||
title,
|
title,
|
||||||
@@ -34,35 +49,49 @@ function WidgetHeader({
|
|||||||
onView,
|
onView,
|
||||||
onDelete,
|
onDelete,
|
||||||
parentHover,
|
parentHover,
|
||||||
|
queryResponse,
|
||||||
|
errorMessage,
|
||||||
}: IWidgetHeaderProps): JSX.Element {
|
}: IWidgetHeaderProps): JSX.Element {
|
||||||
const [localHover, setLocalHover] = useState(false);
|
const [localHover, setLocalHover] = useState(false);
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const onEditHandler = (): void => {
|
const onEditHandler = useCallback((): void => {
|
||||||
const widgetId = widget.id;
|
const widgetId = widget.id;
|
||||||
history.push(
|
history.push(
|
||||||
`${window.location.pathname}/new?widgetId=${widgetId}&graphType=${widget.panelTypes}`,
|
`${window.location.pathname}/new?widgetId=${widgetId}&graphType=${widget.panelTypes}`,
|
||||||
);
|
);
|
||||||
};
|
}, [widget.id, widget.panelTypes]);
|
||||||
|
|
||||||
const keyMethodMapping: {
|
const keyMethodMapping: {
|
||||||
[K in TWidgetOptions]: { key: TWidgetOptions; method: VoidFunction };
|
[K in TWidgetOptions]: { key: TWidgetOptions; method: VoidFunction };
|
||||||
} = {
|
} = useMemo(
|
||||||
view: {
|
() => ({
|
||||||
key: 'view',
|
view: {
|
||||||
method: onView,
|
key: 'view',
|
||||||
|
method: onView,
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
key: 'edit',
|
||||||
|
method: onEditHandler,
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
key: 'delete',
|
||||||
|
method: onDelete,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[onDelete, onEditHandler, onView],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onMenuItemSelectHandler: MenuProps['onClick'] = useCallback(
|
||||||
|
({ key }: { key: TWidgetOptions }): void => {
|
||||||
|
const functionToCall = keyMethodMapping[key]?.method;
|
||||||
|
if (functionToCall) {
|
||||||
|
functionToCall();
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
edit: {
|
[keyMethodMapping],
|
||||||
key: 'edit',
|
);
|
||||||
method: onEditHandler,
|
|
||||||
},
|
|
||||||
delete: {
|
|
||||||
key: 'delete',
|
|
||||||
method: onDelete,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const onMenuItemSelectHandler = ({ key }: { key: TWidgetOptions }): void => {
|
|
||||||
keyMethodMapping[key]?.method();
|
|
||||||
};
|
|
||||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
const [deleteWidget, editWidget] = useComponentPermission(
|
const [deleteWidget, editWidget] = useComponentPermission(
|
||||||
@@ -70,57 +99,85 @@ function WidgetHeader({
|
|||||||
role,
|
role,
|
||||||
);
|
);
|
||||||
|
|
||||||
const menu = (
|
const menuList: MenuItemType[] = useMemo(
|
||||||
<Menu onClick={onMenuItemSelectHandler}>
|
() => [
|
||||||
<Menu.Item key={keyMethodMapping.view.key}>
|
{
|
||||||
<MenuItemContainer>
|
key: keyMethodMapping.view.key,
|
||||||
<span>View</span> <FullscreenOutlined />
|
icon: <FullscreenOutlined />,
|
||||||
</MenuItemContainer>
|
disabled: queryResponse.isLoading,
|
||||||
</Menu.Item>
|
label: 'View',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: keyMethodMapping.edit.key,
|
||||||
|
icon: <EditFilled />,
|
||||||
|
disabled: !editWidget,
|
||||||
|
label: 'Edit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: keyMethodMapping.delete.key,
|
||||||
|
icon: <DeleteOutlined />,
|
||||||
|
disabled: !deleteWidget,
|
||||||
|
danger: true,
|
||||||
|
label: 'Delete',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
deleteWidget,
|
||||||
|
editWidget,
|
||||||
|
keyMethodMapping.delete.key,
|
||||||
|
keyMethodMapping.edit.key,
|
||||||
|
keyMethodMapping.view.key,
|
||||||
|
queryResponse.isLoading,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
{editWidget && (
|
const onClickHandler = useCallback(() => {
|
||||||
<Menu.Item key={keyMethodMapping.edit.key}>
|
setIsOpen((open) => !open);
|
||||||
<MenuItemContainer>
|
}, []);
|
||||||
<span>Edit</span> <EditFilled />
|
|
||||||
</MenuItemContainer>
|
|
||||||
</Menu.Item>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{deleteWidget && (
|
const menu = useMemo(
|
||||||
<>
|
() => ({
|
||||||
<Menu.Divider />
|
items: menuList,
|
||||||
<Menu.Item key={keyMethodMapping.delete.key} danger>
|
onClick: onMenuItemSelectHandler,
|
||||||
<MenuItemContainer>
|
}),
|
||||||
<span>Delete</span> <DeleteOutlined />
|
[menuList, onMenuItemSelectHandler],
|
||||||
</MenuItemContainer>
|
|
||||||
</Menu.Item>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<div>
|
||||||
overlay={menu}
|
<Dropdown
|
||||||
trigger={['click']}
|
destroyPopupOnHide
|
||||||
overlayStyle={{ minWidth: 100 }}
|
open={isOpen}
|
||||||
placement="bottom"
|
onOpenChange={setIsOpen}
|
||||||
>
|
menu={menu}
|
||||||
<HeaderContainer
|
trigger={['click']}
|
||||||
onMouseOver={(): void => setLocalHover(true)}
|
overlayStyle={overlayStyles}
|
||||||
onMouseOut={(): void => setLocalHover(false)}
|
|
||||||
hover={localHover}
|
|
||||||
>
|
>
|
||||||
<HeaderContentContainer onClick={(e): void => e.preventDefault()}>
|
<HeaderContainer
|
||||||
<Typography.Text style={{ maxWidth: '80%' }} ellipsis>
|
onMouseOver={(): void => setLocalHover(true)}
|
||||||
{title}
|
onMouseOut={(): void => setLocalHover(false)}
|
||||||
</Typography.Text>
|
hover={localHover}
|
||||||
<ArrowContainer hover={parentHover}>
|
onClick={onClickHandler}
|
||||||
<DownOutlined />
|
>
|
||||||
</ArrowContainer>
|
<HeaderContentContainer>
|
||||||
</HeaderContentContainer>
|
<Typography.Text style={{ maxWidth: '80%' }} ellipsis>
|
||||||
</HeaderContainer>
|
{title}
|
||||||
</Dropdown>
|
</Typography.Text>
|
||||||
|
<ArrowContainer hover={parentHover}>
|
||||||
|
<DownOutlined />
|
||||||
|
</ArrowContainer>
|
||||||
|
</HeaderContentContainer>
|
||||||
|
</HeaderContainer>
|
||||||
|
</Dropdown>
|
||||||
|
{queryResponse.isFetching && !queryResponse.isError && (
|
||||||
|
<Spinner height="5vh" style={spinnerStyles} />
|
||||||
|
)}
|
||||||
|
{queryResponse.isError && (
|
||||||
|
<Tooltip title={errorMessage} placement={errorTooltipPosition}>
|
||||||
|
<ExclamationCircleOutlined style={tooltipStyles} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import { grey } from '@ant-design/colors';
|
import { grey } from '@ant-design/colors';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const MenuItemContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const HeaderContainer = styled.div<{ hover: boolean }>`
|
export const HeaderContainer = styled.div<{ hover: boolean }>`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
import { ThunkDispatch } from 'redux-thunk';
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
|
import { AppDispatch } from 'store';
|
||||||
|
import { UpdateTimeInterval } from 'store/actions';
|
||||||
import {
|
import {
|
||||||
ToggleAddWidget,
|
ToggleAddWidget,
|
||||||
ToggleAddWidgetProps,
|
ToggleAddWidgetProps,
|
||||||
@@ -63,12 +65,22 @@ function GridGraph(props: Props): JSX.Element {
|
|||||||
const [selectedDashboard] = dashboards;
|
const [selectedDashboard] = dashboards;
|
||||||
const { data } = selectedDashboard;
|
const { data } = selectedDashboard;
|
||||||
const { widgets } = data;
|
const { widgets } = data;
|
||||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
const dispatch: AppDispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
|
|
||||||
const [layouts, setLayout] = useState<LayoutProps[]>(
|
const [layouts, setLayout] = useState<LayoutProps[]>(
|
||||||
getPreLayouts(widgets, selectedDashboard.data.layout || []),
|
getPreLayouts(widgets, selectedDashboard.data.layout || []),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onDragSelect = useCallback(
|
||||||
|
(start: number, end: number) => {
|
||||||
|
const startTimestamp = Math.trunc(start);
|
||||||
|
const endTimestamp = Math.trunc(end);
|
||||||
|
|
||||||
|
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async (): Promise<void> => {
|
(async (): Promise<void> => {
|
||||||
if (!isAddWidget) {
|
if (!isAddWidget) {
|
||||||
@@ -182,13 +194,14 @@ function GridGraph(props: Props): JSX.Element {
|
|||||||
yAxisUnit={currentWidget?.yAxisUnit}
|
yAxisUnit={currentWidget?.yAxisUnit}
|
||||||
layout={layout}
|
layout={layout}
|
||||||
setLayout={setLayout}
|
setLayout={setLayout}
|
||||||
|
onDragSelect={onDragSelect}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[widgets],
|
[widgets, onDragSelect],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onEmptyWidgetHandler = useCallback(async () => {
|
const onEmptyWidgetHandler = useCallback(async () => {
|
||||||
|
|||||||
@@ -3,67 +3,39 @@ import {
|
|||||||
CaretUpFilled,
|
CaretUpFilled,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import {
|
import { Divider, Dropdown, Menu, Space, Typography } from 'antd';
|
||||||
Avatar,
|
|
||||||
Divider,
|
|
||||||
Dropdown,
|
|
||||||
Layout,
|
|
||||||
Menu,
|
|
||||||
Space,
|
|
||||||
Typography,
|
|
||||||
} from 'antd';
|
|
||||||
import { Logout } from 'api/utils';
|
import { Logout } from 'api/utils';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import Config from 'container/ConfigDropdown';
|
import Config from 'container/ConfigDropdown';
|
||||||
import setTheme, { AppMode } from 'lib/theme/setTheme';
|
import { useIsDarkMode, useThemeMode } from 'hooks/useDarkMode';
|
||||||
import React, { Dispatch, SetStateAction, useCallback, useState } from 'react';
|
import React, { Dispatch, SetStateAction, useCallback, useState } from 'react';
|
||||||
import { connect, useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { bindActionCreators } from 'redux';
|
|
||||||
import { ThunkDispatch } from 'redux-thunk';
|
|
||||||
import { ToggleDarkMode } from 'store/actions';
|
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import AppActions from 'types/actions';
|
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
import CurrentOrganization from './CurrentOrganization';
|
import CurrentOrganization from './CurrentOrganization';
|
||||||
import ManageLicense from './ManageLicense';
|
import ManageLicense from './ManageLicense';
|
||||||
import SignedInAS from './SignedInAs';
|
import SignedInAS from './SignedInAs';
|
||||||
import {
|
import {
|
||||||
|
AvatarWrapper,
|
||||||
Container,
|
Container,
|
||||||
|
Header,
|
||||||
IconContainer,
|
IconContainer,
|
||||||
LogoutContainer,
|
LogoutContainer,
|
||||||
|
NavLinkWrapper,
|
||||||
ToggleButton,
|
ToggleButton,
|
||||||
} from './styles';
|
} from './styles';
|
||||||
|
|
||||||
function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
|
function HeaderContainer(): JSX.Element {
|
||||||
const { isDarkMode, user, currentVersion } = useSelector<AppState, AppReducer>(
|
const { user, currentVersion } = useSelector<AppState, AppReducer>(
|
||||||
(state) => state.app,
|
(state) => state.app,
|
||||||
);
|
);
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
const { toggleTheme } = useThemeMode();
|
||||||
|
|
||||||
const [isUserDropDownOpen, setIsUserDropDownOpen] = useState<boolean>(false);
|
const [isUserDropDownOpen, setIsUserDropDownOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const onToggleThemeHandler = useCallback(() => {
|
|
||||||
const preMode: AppMode = isDarkMode ? 'lightMode' : 'darkMode';
|
|
||||||
setTheme(preMode);
|
|
||||||
|
|
||||||
const id: AppMode = preMode;
|
|
||||||
const { head } = document;
|
|
||||||
const link = document.createElement('link');
|
|
||||||
link.rel = 'stylesheet';
|
|
||||||
link.type = 'text/css';
|
|
||||||
link.href = !isDarkMode ? '/css/antd.dark.min.css' : '/css/antd.min.css';
|
|
||||||
link.media = 'all';
|
|
||||||
link.id = id;
|
|
||||||
head.appendChild(link);
|
|
||||||
|
|
||||||
link.onload = (): void => {
|
|
||||||
toggleDarkMode();
|
|
||||||
const prevNode = document.getElementById('appMode');
|
|
||||||
prevNode?.remove();
|
|
||||||
};
|
|
||||||
}, [toggleDarkMode, isDarkMode]);
|
|
||||||
|
|
||||||
const onToggleHandler = useCallback(
|
const onToggleHandler = useCallback(
|
||||||
(functionToExecute: Dispatch<SetStateAction<boolean>>) => (): void => {
|
(functionToExecute: Dispatch<SetStateAction<boolean>>) => (): void => {
|
||||||
functionToExecute((state) => !state);
|
functionToExecute((state) => !state);
|
||||||
@@ -100,15 +72,18 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Header>
|
<Header>
|
||||||
<Container>
|
<Container>
|
||||||
<NavLink to={ROUTES.APPLICATION}>
|
<NavLink to={ROUTES.APPLICATION}>
|
||||||
<Space align="center" direction="horizontal">
|
<NavLinkWrapper>
|
||||||
<img src={`/signoz.svg?currentVersion=${currentVersion}`} alt="SigNoz" />
|
<img src={`/signoz.svg?currentVersion=${currentVersion}`} alt="SigNoz" />
|
||||||
<Typography.Title style={{ margin: 0, color: '#DBDBDB' }} level={4}>
|
<Typography.Title
|
||||||
|
style={{ margin: 0, color: 'rgb(219, 219, 219)' }}
|
||||||
|
level={4}
|
||||||
|
>
|
||||||
SigNoz
|
SigNoz
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
</Space>
|
</NavLinkWrapper>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
<Space style={{ height: '100%' }} align="center">
|
<Space style={{ height: '100%' }} align="center">
|
||||||
@@ -116,7 +91,7 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
|
|||||||
|
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
checked={isDarkMode}
|
checked={isDarkMode}
|
||||||
onChange={onToggleThemeHandler}
|
onChange={toggleTheme}
|
||||||
defaultChecked={isDarkMode}
|
defaultChecked={isDarkMode}
|
||||||
checkedChildren="🌜"
|
checkedChildren="🌜"
|
||||||
unCheckedChildren="🌞"
|
unCheckedChildren="🌞"
|
||||||
@@ -129,7 +104,7 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
|
|||||||
visible={isUserDropDownOpen}
|
visible={isUserDropDownOpen}
|
||||||
>
|
>
|
||||||
<Space>
|
<Space>
|
||||||
<Avatar shape="circle">{user?.name[0]}</Avatar>
|
<AvatarWrapper shape="circle">{user?.name[0]}</AvatarWrapper>
|
||||||
<IconContainer>
|
<IconContainer>
|
||||||
{!isUserDropDownOpen ? <CaretDownFilled /> : <CaretUpFilled />}
|
{!isUserDropDownOpen ? <CaretDownFilled /> : <CaretUpFilled />}
|
||||||
</IconContainer>
|
</IconContainer>
|
||||||
@@ -137,20 +112,8 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
</Space>
|
</Space>
|
||||||
</Container>
|
</Container>
|
||||||
</Layout.Header>
|
</Header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DispatchProps {
|
export default HeaderContainer;
|
||||||
toggleDarkMode: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = (
|
|
||||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
|
||||||
): DispatchProps => ({
|
|
||||||
toggleDarkMode: bindActionCreators(ToggleDarkMode, dispatch),
|
|
||||||
});
|
|
||||||
|
|
||||||
type Props = DispatchProps;
|
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(HeaderContainer);
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { Switch, Typography } from 'antd';
|
import { Avatar, Layout, Switch, Typography } from 'antd';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const Header = styled(Layout.Header)`
|
||||||
|
background: #1f1f1f !important;
|
||||||
|
`;
|
||||||
|
|
||||||
export const Container = styled.div`
|
export const Container = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
height: 100%;
|
height: 4rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const AvatarContainer = styled.div`
|
export const AvatarContainer = styled.div`
|
||||||
@@ -66,3 +70,15 @@ export const ToggleButton = styled(Switch)<DarkModeProps>`
|
|||||||
export const IconContainer = styled.div`
|
export const IconContainer = styled.div`
|
||||||
color: white;
|
color: white;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const NavLinkWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
gap: 0.5rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const AvatarWrapper = styled(Avatar)`
|
||||||
|
background-color: rgba(255, 255, 255, 0.25);
|
||||||
|
`;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { NotificationInstance } from 'antd/lib/notification/index';
|
import { NotificationInstance } from 'antd/es/notification/interface';
|
||||||
import deleteAlerts from 'api/alerts/delete';
|
import deleteAlerts from 'api/alerts/delete';
|
||||||
import { State } from 'hooks/useFetch';
|
import { State } from 'hooks/useFetch';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react/display-name */
|
/* eslint-disable react/display-name */
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
import { notification, Typography } from 'antd';
|
import { notification, Table, Typography } from 'antd';
|
||||||
import Table, { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import TextToolTip from 'components/TextToolTip';
|
import TextToolTip from 'components/TextToolTip';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
@@ -110,13 +110,11 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{withOutSeverityKeys.map((e) => {
|
{withOutSeverityKeys.map((e) => (
|
||||||
return (
|
<StyledTag key={e} color="magenta">
|
||||||
<StyledTag key={e} color="magenta">
|
{e}: {value[e]}
|
||||||
{e}: {value[e]}
|
</StyledTag>
|
||||||
</StyledTag>
|
))}
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -128,22 +126,20 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
title: 'Action',
|
title: 'Action',
|
||||||
dataIndex: 'id',
|
dataIndex: 'id',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
render: (id: GettableAlert['id'], record): JSX.Element => {
|
render: (id: GettableAlert['id'], record): JSX.Element => (
|
||||||
return (
|
<>
|
||||||
<>
|
<ToggleAlertState disabled={record.disabled} setData={setData} id={id} />
|
||||||
<ToggleAlertState disabled={record.disabled} setData={setData} id={id} />
|
|
||||||
|
|
||||||
<ColumnButton
|
<ColumnButton
|
||||||
onClick={(): void => onEditHandler(id.toString())}
|
onClick={(): void => onEditHandler(id.toString())}
|
||||||
type="link"
|
type="link"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</ColumnButton>
|
</ColumnButton>
|
||||||
|
|
||||||
<DeleteAlert notifications={notifications} setData={setData} id={id} />
|
<DeleteAlert notifications={notifications} setData={setData} id={id} />
|
||||||
</>
|
</>
|
||||||
);
|
),
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ function ToggleAlertState({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
if (response.statusCode === 200) {
|
||||||
setData((state) => {
|
setData((state) =>
|
||||||
return state.map((alert) => {
|
state.map((alert) => {
|
||||||
if (alert.id === id) {
|
if (alert.id === id) {
|
||||||
return {
|
return {
|
||||||
...alert,
|
...alert,
|
||||||
@@ -50,8 +50,8 @@ function ToggleAlertState({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return alert;
|
return alert;
|
||||||
});
|
}),
|
||||||
});
|
);
|
||||||
|
|
||||||
setAPIStatus((state) => ({
|
setAPIStatus((state) => ({
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ function ImportJSON({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={isImportJSONModalVisible}
|
open={isImportJSONModalVisible}
|
||||||
centered
|
centered
|
||||||
maskClosable
|
maskClosable
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ import { RefSelectProps } from 'antd/lib/select';
|
|||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { filter, map } from 'lodash-es';
|
import { filter, map } from 'lodash-es';
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
import AppReducer from 'types/reducer/app';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import { DashboardSearchAndFilter } from './Dashboard.machine';
|
import { DashboardSearchAndFilter } from './Dashboard.machine';
|
||||||
@@ -30,7 +27,6 @@ function SearchFilter({
|
|||||||
searchData: Dashboard[];
|
searchData: Dashboard[];
|
||||||
filterDashboards: (filteredDashboards: Dashboard[]) => void;
|
filterDashboards: (filteredDashboards: Dashboard[]) => void;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
|
||||||
const [category, setCategory] = useState<TCategory>();
|
const [category, setCategory] = useState<TCategory>();
|
||||||
const [optionsData, setOptionsData] = useState<IOptionsData>(
|
const [optionsData, setOptionsData] = useState<IOptionsData>(
|
||||||
OptionsSchemas.attribute,
|
OptionsSchemas.attribute,
|
||||||
@@ -154,14 +150,8 @@ function SearchFilter({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SearchContainer isDarkMode={isDarkMode}>
|
<SearchContainer>
|
||||||
<div
|
<div>
|
||||||
style={{
|
|
||||||
maxWidth: '70%',
|
|
||||||
display: 'flex',
|
|
||||||
overflowX: 'auto',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{map(queries, (query) => (
|
{map(queries, (query) => (
|
||||||
<QueryChip key={query.id} queryData={query} onRemove={removeQueryById} />
|
<QueryChip key={query.id} queryData={query} onRemove={removeQueryById} />
|
||||||
))}
|
))}
|
||||||
@@ -194,16 +184,14 @@ function SearchFilter({
|
|||||||
{optionsData.options &&
|
{optionsData.options &&
|
||||||
Array.isArray(optionsData.options) &&
|
Array.isArray(optionsData.options) &&
|
||||||
optionsData.options.map(
|
optionsData.options.map(
|
||||||
(optionItem): JSX.Element => {
|
(optionItem): JSX.Element => (
|
||||||
return (
|
<Select.Option
|
||||||
<Select.Option
|
key={(optionItem.value as string) || (optionItem.name as string)}
|
||||||
key={(optionItem.value as string) || (optionItem.name as string)}
|
value={optionItem.value || optionItem.name}
|
||||||
value={optionItem.value || optionItem.name}
|
>
|
||||||
>
|
{optionItem.name}
|
||||||
{optionItem.name}
|
</Select.Option>
|
||||||
</Select.Option>
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
)}
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ import { grey } from '@ant-design/colors';
|
|||||||
import { Tag } from 'antd';
|
import { Tag } from 'antd';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const SearchContainer = styled.div<{
|
export const SearchContainer = styled.div`
|
||||||
isDarkMode: boolean;
|
|
||||||
}>`
|
|
||||||
background: ${({ isDarkMode }): string => (isDarkMode ? '#000' : '#fff')};
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ export const convertQueriesToURLQuery = (
|
|||||||
|
|
||||||
export const convertURLQueryStringToQuery = (
|
export const convertURLQueryStringToQuery = (
|
||||||
queryString: string,
|
queryString: string,
|
||||||
): IQueryStructure[] => {
|
): IQueryStructure[] => JSON.parse(decode(queryString));
|
||||||
return JSON.parse(decode(queryString));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const resolveOperator = (
|
export const resolveOperator = (
|
||||||
result: unknown,
|
result: unknown,
|
||||||
|
|||||||
@@ -30,13 +30,11 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
|
|||||||
flattenLogData !== null &&
|
flattenLogData !== null &&
|
||||||
Object.keys(flattenLogData)
|
Object.keys(flattenLogData)
|
||||||
.filter((field) => fieldSearchFilter(field, fieldSearchInput))
|
.filter((field) => fieldSearchFilter(field, fieldSearchInput))
|
||||||
.map((key) => {
|
.map((key) => ({
|
||||||
return {
|
key,
|
||||||
key,
|
field: key,
|
||||||
field: key,
|
value: JSON.stringify(flattenLogData[key]),
|
||||||
value: JSON.stringify(flattenLogData[key]),
|
}));
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!dataSource) {
|
if (!dataSource) {
|
||||||
return null;
|
return null;
|
||||||
@@ -45,7 +43,7 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
|
|||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: 'Action',
|
title: 'Action',
|
||||||
width: 75,
|
width: 100,
|
||||||
render: (fieldData: Record<string, string>): JSX.Element | null => {
|
render: (fieldData: Record<string, string>): JSX.Element | null => {
|
||||||
const fieldKey = fieldData.field.split('.').slice(-1);
|
const fieldKey = fieldData.field.split('.').slice(-1);
|
||||||
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
|
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import { Drawer, Tabs } from 'antd';
|
import { Drawer, Tabs } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
||||||
import { ILogsReducer } from 'types/reducer/logs';
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
|
||||||
import JSONView from './JsonView';
|
import JSONView from './JsonView';
|
||||||
import TableView from './TableView';
|
import TableView from './TableView';
|
||||||
|
|
||||||
const { TabPane } = Tabs;
|
|
||||||
|
|
||||||
function LogDetailedView(): JSX.Element {
|
function LogDetailedView(): JSX.Element {
|
||||||
const { detailedLog } = useSelector<AppState, ILogsReducer>(
|
const { detailedLog } = useSelector<AppState, ILogsReducer>(
|
||||||
(state) => state.logs,
|
(state) => state.logs,
|
||||||
);
|
);
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
|
|
||||||
const onDrawerClose = (): void => {
|
const onDrawerClose = (): void => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SET_DETAILED_LOG_DATA,
|
type: SET_DETAILED_LOG_DATA,
|
||||||
@@ -23,30 +25,25 @@ function LogDetailedView(): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{}}>
|
<Drawer
|
||||||
<Drawer
|
width="60%"
|
||||||
width="60%"
|
title="Log Details"
|
||||||
title="Log Details"
|
placement="right"
|
||||||
placement="right"
|
closable
|
||||||
closable
|
onClose={onDrawerClose}
|
||||||
mask={false}
|
open={detailedLog !== null}
|
||||||
onClose={onDrawerClose}
|
style={{ overscrollBehavior: 'contain' }}
|
||||||
visible={detailedLog !== null}
|
destroyOnClose
|
||||||
getContainer={false}
|
>
|
||||||
style={{ overscrollBehavior: 'contain' }}
|
<Tabs defaultActiveKey="1">
|
||||||
>
|
<Tabs.TabPane tab="Table" key="1">
|
||||||
{detailedLog && (
|
{detailedLog && <TableView logData={detailedLog} />}
|
||||||
<Tabs defaultActiveKey="1">
|
</Tabs.TabPane>
|
||||||
<TabPane tab="Table" key="1">
|
<Tabs.TabPane tab="JSON" key="2">
|
||||||
<TableView logData={detailedLog} />
|
{detailedLog && <JSONView logData={detailedLog} />}
|
||||||
</TabPane>
|
</Tabs.TabPane>
|
||||||
<TabPane tab="JSON" key="2">
|
</Tabs>
|
||||||
<JSONView logData={detailedLog} />
|
</Drawer>
|
||||||
</TabPane>
|
|
||||||
</Tabs>
|
|
||||||
)}
|
|
||||||
</Drawer>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import { Button, Popover, Select, Space } from 'antd';
|
import { Button, Popover, Select, Space } from 'antd';
|
||||||
import { LiveTail } from 'api/logs/livetail';
|
import { LiveTail } from 'api/logs/livetail';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { throttle } from 'lodash-es';
|
import { throttle } from 'lodash-es';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
@@ -19,7 +20,6 @@ import {
|
|||||||
TOGGLE_LIVE_TAIL,
|
TOGGLE_LIVE_TAIL,
|
||||||
} from 'types/actions/logs';
|
} from 'types/actions/logs';
|
||||||
import { TLogsLiveTailState } from 'types/api/logs/liveTail';
|
import { TLogsLiveTailState } from 'types/api/logs/liveTail';
|
||||||
import AppReducer from 'types/reducer/app';
|
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import { ILogsReducer } from 'types/reducer/logs';
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
|
||||||
@@ -35,7 +35,8 @@ function LogLiveTail(): JSX.Element {
|
|||||||
liveTailStartRange,
|
liveTailStartRange,
|
||||||
logs,
|
logs,
|
||||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const { selectedAutoRefreshInterval } = useSelector<AppState, GlobalReducer>(
|
const { selectedAutoRefreshInterval } = useSelector<AppState, GlobalReducer>(
|
||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
@@ -54,22 +55,25 @@ function LogLiveTail(): JSX.Element {
|
|||||||
|
|
||||||
const batchedEventsRef = useRef<Record<string, unknown>[]>([]);
|
const batchedEventsRef = useRef<Record<string, unknown>[]>([]);
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
const pushLiveLog = useCallback(() => {
|
||||||
const pushLiveLog = useCallback(
|
dispatch({
|
||||||
throttle(() => {
|
type: PUSH_LIVE_TAIL_EVENT,
|
||||||
dispatch({
|
payload: batchedEventsRef.current.reverse(),
|
||||||
type: PUSH_LIVE_TAIL_EVENT,
|
});
|
||||||
payload: batchedEventsRef.current.reverse(),
|
batchedEventsRef.current = [];
|
||||||
});
|
}, [dispatch]);
|
||||||
batchedEventsRef.current = [];
|
|
||||||
}, 1500),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const batchLiveLog = (e: { data: string }): void => {
|
const pushLiveLogThrottled = useMemo(() => throttle(pushLiveLog, 1000), [
|
||||||
batchedEventsRef.current.push(JSON.parse(e.data as string) as never);
|
pushLiveLog,
|
||||||
pushLiveLog();
|
]);
|
||||||
};
|
|
||||||
|
const batchLiveLog = useCallback(
|
||||||
|
(e: { data: string }): void => {
|
||||||
|
batchedEventsRef.current.push(JSON.parse(e.data as string) as never);
|
||||||
|
pushLiveLogThrottled();
|
||||||
|
},
|
||||||
|
[pushLiveLogThrottled],
|
||||||
|
);
|
||||||
|
|
||||||
// This ref depicts thats whether the live tail is played from paused state or not.
|
// This ref depicts thats whether the live tail is played from paused state or not.
|
||||||
const liveTailSourceRef = useRef<EventSource | null>(null);
|
const liveTailSourceRef = useRef<EventSource | null>(null);
|
||||||
|
|||||||
@@ -157,18 +157,16 @@ function Login({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderSAMLAction = (): JSX.Element => {
|
const renderSAMLAction = (): JSX.Element => (
|
||||||
return (
|
<Button
|
||||||
<Button
|
type="primary"
|
||||||
type="primary"
|
loading={isLoading}
|
||||||
loading={isLoading}
|
disabled={isLoading}
|
||||||
disabled={isLoading}
|
href={precheckResult.ssoUrl}
|
||||||
href={precheckResult.ssoUrl}
|
>
|
||||||
>
|
{t('login_with_sso')}
|
||||||
{t('login_with_sso')}
|
</Button>
|
||||||
</Button>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderOnSsoError = (): JSX.Element | null => {
|
const renderOnSsoError = (): JSX.Element | null => {
|
||||||
if (!ssoerror) {
|
if (!ssoerror) {
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [getLogsAggregate, maxTime, minTime, liveTail]);
|
}, [getLogsAggregate, maxTime, minTime, liveTail]);
|
||||||
|
|
||||||
const graphData = useMemo(() => {
|
const graphData = useMemo(
|
||||||
return {
|
() => ({
|
||||||
labels: logsAggregate.map((s) => new Date(s.timestamp / 1000000)),
|
labels: logsAggregate.map((s) => new Date(s.timestamp / 1000000)),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
@@ -86,8 +86,9 @@ function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
|
|||||||
backgroundColor: blue[4],
|
backgroundColor: blue[4],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
}),
|
||||||
}, [logsAggregate]);
|
[logsAggregate],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { LoadingOutlined } from '@ant-design/icons';
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
import { Button, Popover, Spin } from 'antd';
|
import { Button, Popover, Spin } from 'antd';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import AppReducer from 'types/reducer/app';
|
|
||||||
|
|
||||||
import { Field } from './styles';
|
import { Field } from './styles';
|
||||||
|
|
||||||
@@ -26,7 +24,7 @@ export function FieldItem({
|
|||||||
iconHoverText,
|
iconHoverText,
|
||||||
}: FieldItemProps): JSX.Element {
|
}: FieldItemProps): JSX.Element {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
const isDarkMode = useIsDarkMode();
|
||||||
return (
|
return (
|
||||||
<Field
|
<Field
|
||||||
onMouseEnter={(): void => {
|
onMouseEnter={(): void => {
|
||||||
|
|||||||
@@ -25,9 +25,7 @@ export const Field = styled.div<{ isDarkMode: boolean }>`
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${({ isDarkMode }): string => {
|
background: ${({ isDarkMode }): string => (isDarkMode ? grey[7] : '#ddd')};
|
||||||
return isDarkMode ? grey[7] : '#ddd';
|
|
||||||
}};
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ export interface QueryBuilderProps {
|
|||||||
onDropDownToggleHandler: (value: boolean) => VoidFunction;
|
onDropDownToggleHandler: (value: boolean) => VoidFunction;
|
||||||
fieldsQuery: QueryFields[][];
|
fieldsQuery: QueryFields[][];
|
||||||
setFieldsQuery: (q: QueryFields[][]) => void;
|
setFieldsQuery: (q: QueryFields[][]) => void;
|
||||||
|
syncKeyPrefix: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function QueryBuilder({
|
function QueryBuilder({
|
||||||
@@ -182,6 +183,7 @@ function QueryBuilder({
|
|||||||
fieldsQuery,
|
fieldsQuery,
|
||||||
setFieldsQuery,
|
setFieldsQuery,
|
||||||
onDropDownToggleHandler,
|
onDropDownToggleHandler,
|
||||||
|
syncKeyPrefix,
|
||||||
}: QueryBuilderProps): JSX.Element {
|
}: QueryBuilderProps): JSX.Element {
|
||||||
const handleUpdate = (query: Query, queryIndex: number): void => {
|
const handleUpdate = (query: Query, queryIndex: number): void => {
|
||||||
const updated = [...fieldsQuery];
|
const updated = [...fieldsQuery];
|
||||||
@@ -195,6 +197,9 @@ function QueryBuilder({
|
|||||||
else updated.splice(queryIndex, 2);
|
else updated.splice(queryIndex, 2);
|
||||||
|
|
||||||
setFieldsQuery(updated);
|
setFieldsQuery(updated);
|
||||||
|
|
||||||
|
// initiate re-render query panel
|
||||||
|
syncKeyPrefix();
|
||||||
};
|
};
|
||||||
|
|
||||||
const QueryUI = (
|
const QueryUI = (
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ function SearchFields({
|
|||||||
}
|
}
|
||||||
}, [parsedQuery]);
|
}, [parsedQuery]);
|
||||||
|
|
||||||
|
// syncKeyPrefix initiates re-render. useful in situations like
|
||||||
|
// delete field (in search panel). this method allows condiitonally
|
||||||
|
// setting keyPrefix as doing it on every update of query initiates
|
||||||
|
// a re-render. this is a problem for text fields where input focus goes away.
|
||||||
|
const syncKeyPrefix = (): void => {
|
||||||
|
keyPrefixRef.current = hashCode(JSON.stringify(fieldsQuery));
|
||||||
|
};
|
||||||
|
|
||||||
const addSuggestedField = useCallback(
|
const addSuggestedField = useCallback(
|
||||||
(name: string): void => {
|
(name: string): void => {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
@@ -98,6 +106,7 @@ function SearchFields({
|
|||||||
onDropDownToggleHandler={onDropDownToggleHandler}
|
onDropDownToggleHandler={onDropDownToggleHandler}
|
||||||
fieldsQuery={fieldsQuery}
|
fieldsQuery={fieldsQuery}
|
||||||
setFieldsQuery={setFieldsQuery}
|
setFieldsQuery={setFieldsQuery}
|
||||||
|
syncKeyPrefix={syncKeyPrefix}
|
||||||
/>
|
/>
|
||||||
<SearchFieldsActionBar
|
<SearchFieldsActionBar
|
||||||
applyUpdate={applyUpdate}
|
applyUpdate={applyUpdate}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user