Compare commits
75 Commits
query-limi
...
v0.38.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d224e08145 | ||
|
|
13ced00a35 | ||
|
|
5c60a862e5 | ||
|
|
78c9330666 | ||
|
|
01fc7a7fd4 | ||
|
|
0200fb3a21 | ||
|
|
e977963763 | ||
|
|
824d9aaf85 | ||
|
|
4db3e5e542 | ||
|
|
a8b293a510 | ||
|
|
4a4f48cec8 | ||
|
|
7e5cf65ea3 | ||
|
|
bb7417ffbd | ||
|
|
085cf34a49 | ||
|
|
be27a92fc9 | ||
|
|
253137a6b8 | ||
|
|
fce7ab7d24 | ||
|
|
71f6b355c4 | ||
|
|
110b545454 | ||
|
|
5b0e3d375a | ||
|
|
9e05cb48fe | ||
|
|
6d67ca72a0 | ||
|
|
0626081eee | ||
|
|
199d52b39f | ||
|
|
204cad8448 | ||
|
|
8c6096d60e | ||
|
|
9de9fb5863 | ||
|
|
64d854ffa2 | ||
|
|
6b073280a4 | ||
|
|
79e6699b37 | ||
|
|
d563778479 | ||
|
|
255b3dd3b0 | ||
|
|
00e97fa401 | ||
|
|
9755ba6b47 | ||
|
|
f3fdd2dd6c | ||
|
|
d4248fe933 | ||
|
|
a8d70206ab | ||
|
|
7b344f7a75 | ||
|
|
f0669a6dc1 | ||
|
|
4a7d972c85 | ||
|
|
51c1f88593 | ||
|
|
c1b9049176 | ||
|
|
46559014f7 | ||
|
|
0c1a500142 | ||
|
|
26d6a869c6 | ||
|
|
f99da73098 | ||
|
|
4a1c48b72b | ||
|
|
1163c16506 | ||
|
|
bb558dde8e | ||
|
|
e89c000252 | ||
|
|
4e8e7745c1 | ||
|
|
512fcda33d | ||
|
|
6f43b085b0 | ||
|
|
54038b8ddf | ||
|
|
00c9ef50de | ||
|
|
52750e5248 | ||
|
|
cbce1b1847 | ||
|
|
abaf6126e5 | ||
|
|
739b1bf387 | ||
|
|
cbf150ef7b | ||
|
|
c28f367f46 | ||
|
|
1e679a0d64 | ||
|
|
6f5f361a7e | ||
|
|
d65d75ef69 | ||
|
|
722a38491e | ||
|
|
361efd3b52 | ||
|
|
7b46f86f7f | ||
|
|
5b39dc36d6 | ||
|
|
5fe7948be9 | ||
|
|
a47a90b0f3 | ||
|
|
be6bca3717 | ||
|
|
92717774a2 | ||
|
|
e7fabca38e | ||
|
|
525dea343c | ||
|
|
7d960b79dd |
@@ -146,11 +146,11 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.36.2
|
||||
image: signoz/query-service:0.38.0
|
||||
command:
|
||||
[
|
||||
"-config=/root/config/prometheus.yml",
|
||||
"--prefer-delta=true"
|
||||
# "--prefer-delta=true"
|
||||
]
|
||||
# ports:
|
||||
# - "6060:6060" # pprof port
|
||||
@@ -186,7 +186,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.36.2
|
||||
image: signoz/frontend:0.38.0
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@@ -199,7 +199,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:0.88.6
|
||||
image: signoz/signoz-otel-collector:0.88.9
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
@@ -237,7 +237,7 @@ services:
|
||||
- query-service
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:0.88.6
|
||||
image: signoz/signoz-schema-migrator:0.88.9
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@@ -249,25 +249,6 @@ services:
|
||||
# - clickhouse-2
|
||||
# - clickhouse-3
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/signoz-otel-collector:0.88.6
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-metrics-config.yaml",
|
||||
"--feature-gates=-pkg.translator.prometheus.NormalizeName"
|
||||
]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
# ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
# - "8888:8888" # OtelCollector internal metrics
|
||||
# - "13133:13133" # Health check extension
|
||||
# - "55679:55679" # zPages extension
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
<<: *db-depend
|
||||
|
||||
logspout:
|
||||
image: "gliderlabs/logspout:v3.2.14"
|
||||
volumes:
|
||||
|
||||
@@ -15,13 +15,9 @@ receivers:
|
||||
# please remove names from below if you want to collect logs from them
|
||||
- type: filter
|
||||
id: signoz_logs_filter
|
||||
expr: 'attributes.container_name matches "^signoz_(logspout|frontend|alertmanager|query-service|otel-collector|otel-collector-metrics|clickhouse|zookeeper)"'
|
||||
expr: 'attributes.container_name matches "^signoz_(logspout|frontend|alertmanager|query-service|otel-collector|clickhouse|zookeeper)"'
|
||||
opencensus:
|
||||
endpoint: 0.0.0.0:55678
|
||||
otlp/spanmetrics:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: localhost:12345
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
@@ -69,8 +65,8 @@ processors:
|
||||
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
|
||||
detectors: [env, system] # include ec2 for AWS, gcp for GCP and azure for Azure.
|
||||
timeout: 2s
|
||||
signozspanmetrics/prometheus:
|
||||
metrics_exporter: prometheus
|
||||
signozspanmetrics/cumulative:
|
||||
metrics_exporter: clickhousemetricswrite
|
||||
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
|
||||
dimensions_cache_size: 100000
|
||||
dimensions:
|
||||
@@ -97,6 +93,20 @@ processors:
|
||||
# num_workers: 4
|
||||
# queue_size: 100
|
||||
# retry_on_failure: true
|
||||
signozspanmetrics/delta:
|
||||
metrics_exporter: clickhousemetricswrite
|
||||
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
|
||||
dimensions_cache_size: 100000
|
||||
aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA
|
||||
dimensions:
|
||||
- name: service.namespace
|
||||
default: default
|
||||
- name: deployment.environment
|
||||
default: default
|
||||
# This is added to ensure the uniqueness of the timeseries
|
||||
# Otherwise, identical timeseries produced by multiple replicas of
|
||||
# collectors result in incorrect APM metrics
|
||||
- name: signoz.collector.id
|
||||
|
||||
exporters:
|
||||
clickhousetraces:
|
||||
@@ -109,8 +119,6 @@ exporters:
|
||||
enabled: true
|
||||
clickhousemetricswrite/prometheus:
|
||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
||||
prometheus:
|
||||
endpoint: 0.0.0.0:8889
|
||||
# logging: {}
|
||||
clickhouselogsexporter:
|
||||
dsn: tcp://clickhouse:9000/
|
||||
@@ -140,7 +148,7 @@ service:
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [jaeger, otlp]
|
||||
processors: [signozspanmetrics/prometheus, batch]
|
||||
processors: [signozspanmetrics/cumulative, signozspanmetrics/delta, batch]
|
||||
exporters: [clickhousetraces]
|
||||
metrics:
|
||||
receivers: [otlp]
|
||||
@@ -154,9 +162,6 @@ service:
|
||||
receivers: [prometheus]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite/prometheus]
|
||||
metrics/spanmetrics:
|
||||
receivers: [otlp/spanmetrics]
|
||||
exporters: [prometheus]
|
||||
logs:
|
||||
receivers: [otlp, tcplog/docker]
|
||||
processors: [batch]
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
receivers:
|
||||
prometheus:
|
||||
config:
|
||||
scrape_configs:
|
||||
# otel-collector-metrics internal metrics
|
||||
- job_name: otel-collector-metrics
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:8888
|
||||
labels:
|
||||
job_name: otel-collector-metrics
|
||||
# SigNoz span metrics
|
||||
- job_name: signozspanmetrics-collector
|
||||
scrape_interval: 60s
|
||||
dns_sd_configs:
|
||||
- names:
|
||||
- tasks.otel-collector
|
||||
type: A
|
||||
port: 8889
|
||||
|
||||
processors:
|
||||
batch:
|
||||
send_batch_size: 10000
|
||||
send_batch_max_size: 11000
|
||||
timeout: 10s
|
||||
# memory_limiter:
|
||||
# # 80% of maximum memory up to 2G
|
||||
# limit_mib: 1500
|
||||
# # 25% of limit up to 2G
|
||||
# spike_limit_mib: 512
|
||||
# check_interval: 5s
|
||||
#
|
||||
# # 50% of the maximum memory
|
||||
# limit_percentage: 50
|
||||
# # 20% of max memory usage spike expected
|
||||
# spike_limit_percentage: 20
|
||||
# queued_retry:
|
||||
# num_workers: 4
|
||||
# queue_size: 100
|
||||
# retry_on_failure: true
|
||||
|
||||
exporters:
|
||||
clickhousemetricswrite:
|
||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
||||
|
||||
extensions:
|
||||
health_check:
|
||||
endpoint: 0.0.0.0:13133
|
||||
zpages:
|
||||
endpoint: 0.0.0.0:55679
|
||||
pprof:
|
||||
endpoint: 0.0.0.0:1777
|
||||
|
||||
service:
|
||||
telemetry:
|
||||
metrics:
|
||||
address: 0.0.0.0:8888
|
||||
extensions: [health_check, zpages, pprof]
|
||||
pipelines:
|
||||
metrics:
|
||||
receivers: [prometheus]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
@@ -66,7 +66,7 @@ services:
|
||||
- --storage.path=/data
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.6}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.9}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -81,7 +81,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
otel-collector:
|
||||
container_name: signoz-otel-collector
|
||||
image: signoz/signoz-otel-collector:0.88.6
|
||||
image: signoz/signoz-otel-collector:0.88.9
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
@@ -116,28 +116,6 @@ services:
|
||||
query-service:
|
||||
condition: service_healthy
|
||||
|
||||
otel-collector-metrics:
|
||||
container_name: signoz-otel-collector-metrics
|
||||
image: signoz/signoz-otel-collector:0.88.6
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-metrics-config.yaml",
|
||||
"--feature-gates=-pkg.translator.prometheus.NormalizeName"
|
||||
]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
# ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
# - "8888:8888" # OtelCollector internal metrics
|
||||
# - "13133:13133" # Health check extension
|
||||
# - "55679:55679" # zPages extension
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
otel-collector-migrator:
|
||||
condition: service_completed_successfully
|
||||
|
||||
logspout:
|
||||
image: "gliderlabs/logspout:v3.2.14"
|
||||
container_name: signoz-logspout
|
||||
|
||||
@@ -25,7 +25,7 @@ services:
|
||||
command:
|
||||
[
|
||||
"-config=/root/config/prometheus.yml",
|
||||
"--prefer-delta=true"
|
||||
# "--prefer-delta=true"
|
||||
]
|
||||
ports:
|
||||
- "6060:6060"
|
||||
|
||||
@@ -164,12 +164,12 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.36.2}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.38.0}
|
||||
container_name: signoz-query-service
|
||||
command:
|
||||
[
|
||||
"-config=/root/config/prometheus.yml",
|
||||
"--prefer-delta=true"
|
||||
# "--prefer-delta=true"
|
||||
]
|
||||
# ports:
|
||||
# - "6060:6060" # pprof port
|
||||
@@ -203,7 +203,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.36.2}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.38.0}
|
||||
container_name: signoz-frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@@ -215,7 +215,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.6}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.9}
|
||||
container_name: otel-migrator
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@@ -229,7 +229,7 @@ services:
|
||||
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.6}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.9}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
[
|
||||
@@ -268,24 +268,6 @@ services:
|
||||
query-service:
|
||||
condition: service_healthy
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.6}
|
||||
container_name: signoz-otel-collector-metrics
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-metrics-config.yaml",
|
||||
"--feature-gates=-pkg.translator.prometheus.NormalizeName"
|
||||
]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
# ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
# - "8888:8888" # OtelCollector internal metrics
|
||||
# - "13133:13133" # Health check extension
|
||||
# - "55679:55679" # zPages extension
|
||||
restart: on-failure
|
||||
<<: *db-depend
|
||||
|
||||
logspout:
|
||||
image: "gliderlabs/logspout:v3.2.14"
|
||||
container_name: signoz-logspout
|
||||
|
||||
@@ -15,13 +15,9 @@ receivers:
|
||||
# please remove names from below if you want to collect logs from them
|
||||
- type: filter
|
||||
id: signoz_logs_filter
|
||||
expr: 'attributes.container_name matches "^signoz-(logspout|frontend|alertmanager|query-service|otel-collector|otel-collector-metrics|clickhouse|zookeeper)"'
|
||||
expr: 'attributes.container_name matches "^signoz-(logspout|frontend|alertmanager|query-service|otel-collector|clickhouse|zookeeper)"'
|
||||
opencensus:
|
||||
endpoint: 0.0.0.0:55678
|
||||
otlp/spanmetrics:
|
||||
protocols:
|
||||
grpc:
|
||||
endpoint: localhost:12345
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
@@ -66,8 +62,9 @@ processors:
|
||||
send_batch_size: 10000
|
||||
send_batch_max_size: 11000
|
||||
timeout: 10s
|
||||
signozspanmetrics/prometheus:
|
||||
metrics_exporter: prometheus
|
||||
signozspanmetrics/cumulative:
|
||||
metrics_exporter: clickhousemetricswrite
|
||||
metrics_flush_interval: 60s
|
||||
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
|
||||
dimensions_cache_size: 100000
|
||||
dimensions:
|
||||
@@ -98,6 +95,21 @@ processors:
|
||||
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
|
||||
detectors: [env, system] # include ec2 for AWS, gcp for GCP and azure for Azure.
|
||||
timeout: 2s
|
||||
signozspanmetrics/delta:
|
||||
metrics_exporter: clickhousemetricswrite
|
||||
metrics_flush_interval: 60s
|
||||
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
|
||||
dimensions_cache_size: 100000
|
||||
aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA
|
||||
dimensions:
|
||||
- name: service.namespace
|
||||
default: default
|
||||
- name: deployment.environment
|
||||
default: default
|
||||
# This is added to ensure the uniqueness of the timeseries
|
||||
# Otherwise, identical timeseries produced by multiple replicas of
|
||||
# collectors result in incorrect APM metrics
|
||||
- name: signoz.collector.id
|
||||
|
||||
extensions:
|
||||
health_check:
|
||||
@@ -118,8 +130,6 @@ exporters:
|
||||
enabled: true
|
||||
clickhousemetricswrite/prometheus:
|
||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
||||
prometheus:
|
||||
endpoint: 0.0.0.0:8889
|
||||
# logging: {}
|
||||
|
||||
clickhouselogsexporter:
|
||||
@@ -145,7 +155,7 @@ service:
|
||||
pipelines:
|
||||
traces:
|
||||
receivers: [jaeger, otlp]
|
||||
processors: [signozspanmetrics/prometheus, batch]
|
||||
processors: [signozspanmetrics/cumulative, signozspanmetrics/delta, batch]
|
||||
exporters: [clickhousetraces]
|
||||
metrics:
|
||||
receivers: [otlp]
|
||||
@@ -159,9 +169,6 @@ service:
|
||||
receivers: [prometheus]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite/prometheus]
|
||||
metrics/spanmetrics:
|
||||
receivers: [otlp/spanmetrics]
|
||||
exporters: [prometheus]
|
||||
logs:
|
||||
receivers: [otlp, tcplog/docker]
|
||||
processors: [batch]
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
receivers:
|
||||
otlp:
|
||||
protocols:
|
||||
grpc:
|
||||
http:
|
||||
prometheus:
|
||||
config:
|
||||
scrape_configs:
|
||||
# otel-collector-metrics internal metrics
|
||||
- job_name: otel-collector-metrics
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets:
|
||||
- localhost:8888
|
||||
labels:
|
||||
job_name: otel-collector-metrics
|
||||
# SigNoz span metrics
|
||||
- job_name: signozspanmetrics-collector
|
||||
scrape_interval: 60s
|
||||
static_configs:
|
||||
- targets:
|
||||
- otel-collector:8889
|
||||
|
||||
processors:
|
||||
batch:
|
||||
send_batch_size: 10000
|
||||
send_batch_max_size: 11000
|
||||
timeout: 10s
|
||||
# memory_limiter:
|
||||
# # 80% of maximum memory up to 2G
|
||||
# limit_mib: 1500
|
||||
# # 25% of limit up to 2G
|
||||
# spike_limit_mib: 512
|
||||
# check_interval: 5s
|
||||
#
|
||||
# # 50% of the maximum memory
|
||||
# limit_percentage: 50
|
||||
# # 20% of max memory usage spike expected
|
||||
# spike_limit_percentage: 20
|
||||
# queued_retry:
|
||||
# num_workers: 4
|
||||
# queue_size: 100
|
||||
# retry_on_failure: true
|
||||
|
||||
extensions:
|
||||
health_check:
|
||||
endpoint: 0.0.0.0:13133
|
||||
zpages:
|
||||
endpoint: 0.0.0.0:55679
|
||||
pprof:
|
||||
endpoint: 0.0.0.0:1777
|
||||
|
||||
exporters:
|
||||
clickhousemetricswrite:
|
||||
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
|
||||
|
||||
service:
|
||||
telemetry:
|
||||
metrics:
|
||||
address: 0.0.0.0:8888
|
||||
extensions:
|
||||
- health_check
|
||||
- zpages
|
||||
- pprof
|
||||
pipelines:
|
||||
metrics:
|
||||
receivers: [prometheus]
|
||||
processors: [batch]
|
||||
exporters: [clickhousemetricswrite]
|
||||
@@ -152,9 +152,9 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
|
||||
router.HandleFunc("/api/v2/metrics/query_range", am.ViewAccess(ah.queryRangeMetricsV2)).Methods(http.MethodPost)
|
||||
|
||||
// PAT APIs
|
||||
router.HandleFunc("/api/v1/pat", am.OpenAccess(ah.createPAT)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/pat", am.OpenAccess(ah.getPATs)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/pat/{id}", am.OpenAccess(ah.deletePAT)).Methods(http.MethodDelete)
|
||||
router.HandleFunc("/api/v1/pat", am.AdminAccess(ah.createPAT)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/pat", am.AdminAccess(ah.getPATs)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/pat/{id}", am.AdminAccess(ah.deletePAT)).Methods(http.MethodDelete)
|
||||
|
||||
router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.checkout)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
|
||||
|
||||
@@ -40,6 +40,7 @@ type billingDetails struct {
|
||||
BillingPeriodEnd int64 `json:"billingPeriodEnd"`
|
||||
Details details `json:"details"`
|
||||
Discount float64 `json:"discount"`
|
||||
SubscriptionStatus string `json:"subscriptionStatus"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
}
|
||||
|
||||
// start the usagemanager
|
||||
usageManager, err := usage.New("sqlite", localDB, lm.GetRepo(), reader.GetConn())
|
||||
usageManager, err := usage.New("sqlite", modelDao, lm.GetRepo(), reader.GetConn())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -331,6 +331,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
||||
apiHandler.RegisterMetricsRoutes(r, am)
|
||||
apiHandler.RegisterLogsRoutes(r, am)
|
||||
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
||||
apiHandler.RegisterQueryRangeV4Routes(r, am)
|
||||
|
||||
c := cors.New(cors.Options{
|
||||
AllowedOrigins: []string{"*"},
|
||||
|
||||
@@ -20,6 +20,8 @@ type Usage struct {
|
||||
TimeStamp time.Time `json:"timestamp"`
|
||||
Count int64 `json:"count"`
|
||||
Size int64 `json:"size"`
|
||||
OrgName string `json:"orgName"`
|
||||
TenantId string `json:"tenantId"`
|
||||
}
|
||||
|
||||
type UsageDB struct {
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -11,10 +13,10 @@ import (
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
"github.com/go-co-op/gocron"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"go.signoz.io/signoz/ee/query-service/dao"
|
||||
licenseserver "go.signoz.io/signoz/ee/query-service/integrations/signozio"
|
||||
"go.signoz.io/signoz/ee/query-service/license"
|
||||
"go.signoz.io/signoz/ee/query-service/model"
|
||||
@@ -38,15 +40,29 @@ type Manager struct {
|
||||
licenseRepo *license.Repo
|
||||
|
||||
scheduler *gocron.Scheduler
|
||||
|
||||
modelDao dao.ModelDao
|
||||
|
||||
tenantID string
|
||||
}
|
||||
|
||||
func New(dbType string, db *sqlx.DB, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn) (*Manager, error) {
|
||||
func New(dbType string, modelDao dao.ModelDao, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn) (*Manager, error) {
|
||||
hostNameRegex := regexp.MustCompile(`tcp://(?P<hostname>.*):`)
|
||||
hostNameRegexMatches := hostNameRegex.FindStringSubmatch(os.Getenv("ClickHouseUrl"))
|
||||
|
||||
tenantID := ""
|
||||
if len(hostNameRegexMatches) == 2 {
|
||||
tenantID = hostNameRegexMatches[1]
|
||||
tenantID = strings.TrimRight(tenantID, "-clickhouse")
|
||||
}
|
||||
|
||||
m := &Manager{
|
||||
// repository: repo,
|
||||
clickhouseConn: clickhouseConn,
|
||||
licenseRepo: licenseRepo,
|
||||
scheduler: gocron.NewScheduler(time.UTC).Every(1).Day().At("00:00"), // send usage every at 00:00 UTC
|
||||
modelDao: modelDao,
|
||||
tenantID: tenantID,
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
@@ -123,6 +139,19 @@ func (lm *Manager) UploadUsage() {
|
||||
|
||||
zap.S().Info("uploading usage data")
|
||||
|
||||
// Try to get the org name
|
||||
orgName := ""
|
||||
orgNames, err := lm.modelDao.GetOrgs(ctx)
|
||||
if err != nil {
|
||||
zap.S().Errorf("failed to get org data: %v", zap.Error(err))
|
||||
} else {
|
||||
if len(orgNames) != 1 {
|
||||
zap.S().Errorf("expected one org but got %d orgs", len(orgNames))
|
||||
} else {
|
||||
orgName = orgNames[0].Name
|
||||
}
|
||||
}
|
||||
|
||||
usagesPayload := []model.Usage{}
|
||||
for _, usage := range usages {
|
||||
usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data))
|
||||
@@ -142,6 +171,8 @@ func (lm *Manager) UploadUsage() {
|
||||
usageData.ExporterID = usage.ExporterID
|
||||
usageData.Type = usage.Type
|
||||
usageData.Tenant = usage.Tenant
|
||||
usageData.OrgName = orgName
|
||||
usageData.TenantId = lm.tenantID
|
||||
usagesPayload = append(usagesPayload, usageData)
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"@mdx-js/loader": "2.3.0",
|
||||
"@mdx-js/react": "2.3.0",
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
"@signozhq/design-tokens": "0.0.6",
|
||||
"@uiw/react-md-editor": "3.23.5",
|
||||
"@xstate/react": "^3.0.0",
|
||||
"ansi-to-html": "0.7.2",
|
||||
|
||||
11
frontend/public/Logos/signoz-brand-logo.svg
Normal file
11
frontend/public/Logos/signoz-brand-logo.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2048_2251)">
|
||||
<path opacity="0.9" d="M8.02226 15.9866C3.56539 15.9866 -6.10352e-05 12.4896 -6.10352e-05 8.11832C-6.10352e-05 3.79075 3.56539 0.25 8.02226 0.25H13.0584C14.7075 0.25 15.9999 1.56139 15.9999 3.13506V8.11832C15.9999 12.4896 12.4345 15.9866 8.02226 15.9866Z" fill="#F25733"/>
|
||||
<path d="M7.95919 4.71207C4.63025 4.71207 2.75514 7.46868 2.67693 7.58603C2.48413 7.87508 2.48413 8.24888 2.67707 8.53816C2.75514 8.65528 4.63025 11.4119 7.95919 11.4119C11.2881 11.4119 13.1633 8.65528 13.2414 8.53792C13.4342 8.24888 13.4342 7.87508 13.2413 7.58582C13.1632 7.46868 11.2881 4.71207 7.95919 4.71207ZM3.13771 8.23088C3.06925 8.12832 3.06925 7.99571 3.13771 7.89307C3.20059 7.79867 4.53564 5.83764 6.92256 5.36723C5.84092 5.78476 5.07127 6.83485 5.07127 8.062C5.07127 9.28912 5.84092 10.3392 6.92256 10.7567C4.53564 10.2863 3.20059 8.32528 3.13771 8.23088ZM6.62838 8.062C6.62838 8.21488 6.50443 8.3388 6.35151 8.3388C6.19859 8.3388 6.07465 8.21488 6.07465 8.062C6.07465 7.02287 6.92003 6.17748 7.95916 6.17748C8.11207 6.17748 8.23599 6.30141 8.23599 6.45434C8.23599 6.60727 8.11207 6.73119 7.95916 6.73119C7.22535 6.73119 6.62838 7.32815 6.62838 8.062ZM7.95919 8.73504C7.58803 8.73504 7.2861 8.43312 7.2861 8.062C7.2861 7.69085 7.58803 7.3889 7.95919 7.3889C8.33039 7.3889 8.63231 7.69083 8.63231 8.062C8.63231 8.43312 8.33039 8.73504 7.95919 8.73504ZM12.7806 8.23088C12.7178 8.32528 11.3827 10.2863 8.99583 10.7567C10.0775 10.3392 10.8471 9.28912 10.8471 8.062C10.8471 6.83487 10.0775 5.78477 8.99583 5.36724C11.3827 5.83768 12.7178 7.7987 12.7806 7.89307C12.8491 7.99571 12.8491 8.12832 12.7806 8.23088Z" fill="#F9F2F9"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2048_2251">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -62,6 +62,7 @@
|
||||
"button_cancel": "No",
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_notification_channel": "Notification Channel",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
@@ -100,7 +101,7 @@
|
||||
"user_guide_ch_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_ch_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_tooltip_more_help": "More details on how to create alerts",
|
||||
"choose_alert_type": "Choose a type for the alert:",
|
||||
"choose_alert_type": "Choose a type for the alert",
|
||||
"metric_based_alert": "Metric based Alert",
|
||||
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data",
|
||||
"log_based_alert": "Log-based Alert",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_notification_channel": "Notification Channel",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
"option_critical": "Critical",
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_notification_channel": "Notification Channel",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
"option_critical": "Critical",
|
||||
@@ -100,7 +101,7 @@
|
||||
"user_guide_ch_step3a": "Set alert severity, name and descriptions",
|
||||
"user_guide_ch_step3b": "Add tags to the alert in the Label field if needed",
|
||||
"user_tooltip_more_help": "More details on how to create alerts",
|
||||
"choose_alert_type": "Choose a type for the alert:",
|
||||
"choose_alert_type": "Choose a type for the alert",
|
||||
"metric_based_alert": "Metric based Alert",
|
||||
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data",
|
||||
"log_based_alert": "Log-based Alert",
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_notification_channel": "Notification Channel",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
"option_critical": "Critical",
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"NOT_FOUND": "SigNoz | Page Not Found",
|
||||
"LOGS": "SigNoz | Logs",
|
||||
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
|
||||
"OLD_LOGS_EXPLORER": "SigNoz | Old Logs Explorer",
|
||||
"LIVE_LOGS": "SigNoz | Live Logs",
|
||||
"LOGS_PIPELINES": "SigNoz | Logs Pipelines",
|
||||
"HOME_PAGE": "Open source Observability Platform | SigNoz",
|
||||
|
||||
@@ -28,7 +28,11 @@ import AppReducer, { User } from 'types/reducer/app';
|
||||
import { extractDomain, isCloudUser, isEECloudUser } from 'utils/app';
|
||||
|
||||
import PrivateRoute from './Private';
|
||||
import defaultRoutes, { AppRoutes, SUPPORT_ROUTE } from './routes';
|
||||
import defaultRoutes, {
|
||||
AppRoutes,
|
||||
LIST_LICENSES,
|
||||
SUPPORT_ROUTE,
|
||||
} from './routes';
|
||||
|
||||
function App(): JSX.Element {
|
||||
const themeConfig = useThemeConfig();
|
||||
@@ -150,6 +154,10 @@ function App(): JSX.Element {
|
||||
if (isCloudUserVal || isEECloudUser()) {
|
||||
const newRoutes = [...routes, SUPPORT_ROUTE];
|
||||
|
||||
setRoutes(newRoutes);
|
||||
} else {
|
||||
const newRoutes = [...routes, LIST_LICENSES];
|
||||
|
||||
setRoutes(newRoutes);
|
||||
}
|
||||
|
||||
|
||||
@@ -112,17 +112,25 @@ export const MySettings = Loadable(
|
||||
);
|
||||
|
||||
export const Logs = Loadable(
|
||||
() => import(/* webpackChunkName: "Logs" */ 'pages/Logs'),
|
||||
() => import(/* webpackChunkName: "Logs" */ 'pages/LogsModulePage'),
|
||||
);
|
||||
|
||||
export const LogsExplorer = Loadable(
|
||||
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/LogsExplorer'),
|
||||
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/LogsModulePage'),
|
||||
);
|
||||
|
||||
export const OldLogsExplorer = Loadable(
|
||||
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/Logs'),
|
||||
);
|
||||
|
||||
export const LiveLogs = Loadable(
|
||||
() => import(/* webpackChunkName: "Live Logs" */ 'pages/LiveLogs'),
|
||||
);
|
||||
|
||||
export const PipelinePage = Loadable(
|
||||
() => import(/* webpackChunkName: "Pipelines" */ 'pages/LogsModulePage'),
|
||||
);
|
||||
|
||||
export const Login = Loadable(
|
||||
() => import(/* webpackChunkName: "Login" */ 'pages/Login'),
|
||||
);
|
||||
@@ -151,10 +159,6 @@ export const LogsIndexToFields = Loadable(
|
||||
import(/* webpackChunkName: "LogsIndexToFields Page" */ 'pages/LogsSettings'),
|
||||
);
|
||||
|
||||
export const PipelinePage = Loadable(
|
||||
() => import(/* webpackChunkName: "Pipelines" */ 'pages/Pipelines'),
|
||||
);
|
||||
|
||||
export const BillingPage = Loadable(
|
||||
() => import(/* webpackChunkName: "BillingPage" */ 'pages/Billing'),
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
LogsIndexToFields,
|
||||
MySettings,
|
||||
NewDashboardPage,
|
||||
OldLogsExplorer,
|
||||
Onboarding,
|
||||
OrganizationSettings,
|
||||
PasswordReset,
|
||||
@@ -190,13 +191,6 @@ const routes: AppRoutes[] = [
|
||||
component: AllErrors,
|
||||
key: 'ALL_ERROR',
|
||||
},
|
||||
{
|
||||
path: ROUTES.LIST_LICENSES,
|
||||
exact: true,
|
||||
component: LicensePage,
|
||||
isPrivate: true,
|
||||
key: 'LIST_LICENSES',
|
||||
},
|
||||
{
|
||||
path: ROUTES.ERROR_DETAIL,
|
||||
exact: true,
|
||||
@@ -246,6 +240,13 @@ const routes: AppRoutes[] = [
|
||||
key: 'LOGS_EXPLORER',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.OLD_LOGS_EXPLORER,
|
||||
exact: true,
|
||||
component: OldLogsExplorer,
|
||||
key: 'OLD_LOGS_EXPLORER',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.LIVE_LOGS,
|
||||
exact: true,
|
||||
@@ -312,6 +313,14 @@ export const SUPPORT_ROUTE: AppRoutes = {
|
||||
isPrivate: true,
|
||||
};
|
||||
|
||||
export const LIST_LICENSES: AppRoutes = {
|
||||
path: ROUTES.LIST_LICENSES,
|
||||
exact: true,
|
||||
component: LicensePage,
|
||||
isPrivate: true,
|
||||
key: 'LIST_LICENSES',
|
||||
};
|
||||
|
||||
export interface AppRoutes {
|
||||
component: RouteProps['component'];
|
||||
path: RouteProps['path'];
|
||||
|
||||
@@ -4,5 +4,6 @@ import { MetricMetaProps } from 'types/api/metrics/getApDex';
|
||||
|
||||
export const getMetricMeta = (
|
||||
metricName: string,
|
||||
servicename: string,
|
||||
): Promise<AxiosResponse<MetricMetaProps>> =>
|
||||
axios.get(`/metric_meta?metricName=${metricName}`);
|
||||
axios.get(`/metric_meta?metricName=${metricName}&serviceName=${servicename}`);
|
||||
|
||||
@@ -66,7 +66,11 @@ export const Logout = (): void => {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.Intercom('shutdown');
|
||||
if (window && window.Intercom) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.Intercom('shutdown');
|
||||
}
|
||||
|
||||
history.push(ROUTES.LOGIN);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
.custom-time-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.time-options-container {
|
||||
.time-options-item {
|
||||
margin: 2px 0;
|
||||
padding: 8px;
|
||||
border-radius: 2px;
|
||||
|
||||
&.active {
|
||||
background-color: rgba($color: #000000, $alpha: 0.2);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba($color: #000000, $alpha: 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba($color: #000000, $alpha: 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.time-selection-dropdown-content {
|
||||
min-width: 172px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.timeSelection-input {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
padding-left: 0px !important;
|
||||
|
||||
&.custom-time {
|
||||
input:not(:focus) {
|
||||
min-width: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: white;
|
||||
}
|
||||
|
||||
input:focus::placeholder {
|
||||
color: rgba($color: #ffffff, $alpha: 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.valid-format-error {
|
||||
margin-top: 4px;
|
||||
color: var(--bg-cherry-400) !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.time-options-container {
|
||||
.time-options-item {
|
||||
&.active {
|
||||
background-color: rgba($color: #ffffff, $alpha: 0.2);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba($color: #ffffff, $alpha: 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba($color: #ffffff, $alpha: 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.timeSelection-input {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
padding-left: 0px !important;
|
||||
|
||||
input::placeholder {
|
||||
color: var(---bg-ink-300);
|
||||
}
|
||||
|
||||
input:focus::placeholder {
|
||||
color: rgba($color: #000000, $alpha: 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
238
frontend/src/components/CustomTimePicker/CustomTimePicker.tsx
Normal file
238
frontend/src/components/CustomTimePicker/CustomTimePicker.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import './CustomTimePicker.styles.scss';
|
||||
|
||||
import { Input, Popover, Tooltip, Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { Options } from 'container/TopNav/DateTimeSelection/config';
|
||||
import dayjs from 'dayjs';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
|
||||
import { ChangeEvent, useEffect, useState } from 'react';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
const maxAllowedMinTimeInMonths = 6;
|
||||
|
||||
interface CustomTimePickerProps {
|
||||
onSelect: (value: string) => void;
|
||||
onError: (value: boolean) => void;
|
||||
items: any[];
|
||||
selectedValue: string;
|
||||
selectedTime: string;
|
||||
onValidCustomDateChange: ([t1, t2]: any[]) => void;
|
||||
}
|
||||
|
||||
function CustomTimePicker({
|
||||
onSelect,
|
||||
onError,
|
||||
items,
|
||||
selectedValue,
|
||||
selectedTime,
|
||||
onValidCustomDateChange,
|
||||
}: CustomTimePickerProps): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [
|
||||
selectedTimePlaceholderValue,
|
||||
setSelectedTimePlaceholderValue,
|
||||
] = useState('Select / Enter Time Range');
|
||||
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [inputStatus, setInputStatus] = useState<'' | 'error' | 'success'>('');
|
||||
const [inputErrorMessage, setInputErrorMessage] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||
|
||||
const getSelectedTimeRangeLabel = (
|
||||
selectedTime: string,
|
||||
selectedTimeValue: string,
|
||||
): string => {
|
||||
if (selectedTime === 'custom') {
|
||||
return selectedTimeValue;
|
||||
}
|
||||
|
||||
for (let index = 0; index < Options.length; index++) {
|
||||
if (Options[index].value === selectedTime) {
|
||||
return Options[index].label;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const value = getSelectedTimeRangeLabel(selectedTime, selectedValue);
|
||||
|
||||
setSelectedTimePlaceholderValue(value);
|
||||
}, [selectedTime, selectedValue]);
|
||||
|
||||
const hide = (): void => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleOpenChange = (newOpen: boolean): void => {
|
||||
setOpen(newOpen);
|
||||
};
|
||||
|
||||
const debouncedHandleInputChange = debounce((inputValue): void => {
|
||||
const isValidFormat = /^(\d+)([mhdw])$/.test(inputValue);
|
||||
if (isValidFormat) {
|
||||
setInputStatus('success');
|
||||
onError(false);
|
||||
setInputErrorMessage(null);
|
||||
|
||||
const match = inputValue.match(/^(\d+)([mhdw])$/);
|
||||
|
||||
const value = parseInt(match[1], 10);
|
||||
const unit = match[2];
|
||||
|
||||
const currentTime = dayjs();
|
||||
const maxAllowedMinTime = currentTime.subtract(
|
||||
maxAllowedMinTimeInMonths,
|
||||
'month',
|
||||
);
|
||||
let minTime = null;
|
||||
|
||||
switch (unit) {
|
||||
case 'm':
|
||||
minTime = currentTime.subtract(value, 'minute');
|
||||
break;
|
||||
|
||||
case 'h':
|
||||
minTime = currentTime.subtract(value, 'hour');
|
||||
break;
|
||||
case 'd':
|
||||
minTime = currentTime.subtract(value, 'day');
|
||||
break;
|
||||
case 'w':
|
||||
minTime = currentTime.subtract(value, 'week');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (minTime && minTime < maxAllowedMinTime) {
|
||||
setInputStatus('error');
|
||||
onError(true);
|
||||
setInputErrorMessage('Please enter time less than 6 months');
|
||||
} else {
|
||||
onValidCustomDateChange([minTime, currentTime]);
|
||||
}
|
||||
} else {
|
||||
setInputStatus('error');
|
||||
onError(true);
|
||||
setInputErrorMessage(null);
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const handleInputChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
const inputValue = event.target.value;
|
||||
|
||||
if (inputValue.length > 0) {
|
||||
setOpen(false);
|
||||
} else {
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
setInputValue(inputValue);
|
||||
|
||||
// Call the debounced function with the input value
|
||||
debouncedHandleInputChange(inputValue);
|
||||
};
|
||||
|
||||
const content = (
|
||||
<div className="time-selection-dropdown-content">
|
||||
<div className="time-options-container">
|
||||
{items.map(({ value, label }) => (
|
||||
<div
|
||||
onClick={(): void => {
|
||||
onSelect(value);
|
||||
setSelectedTimePlaceholderValue(label);
|
||||
setInputStatus('');
|
||||
onError(false);
|
||||
setInputErrorMessage(null);
|
||||
setInputValue('');
|
||||
hide();
|
||||
}}
|
||||
key={value}
|
||||
className={cx(
|
||||
'time-options-item',
|
||||
selectedValue === value ? 'active' : '',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleFocus = (): void => {
|
||||
setIsInputFocused(true);
|
||||
};
|
||||
|
||||
const handleBlur = (): void => {
|
||||
setIsInputFocused(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="custom-time-picker">
|
||||
<Popover
|
||||
className={cx(
|
||||
'timeSelection-input-container',
|
||||
selectedTime === 'custom' && inputValue === '' ? 'custom-time' : '',
|
||||
)}
|
||||
placement="bottomRight"
|
||||
getPopupContainer={popupContainer}
|
||||
content={content}
|
||||
arrow={false}
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
trigger={['click']}
|
||||
style={{
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
className="timeSelection-input"
|
||||
type="text"
|
||||
status={inputValue && inputStatus === 'error' ? 'error' : ''}
|
||||
placeholder={
|
||||
isInputFocused
|
||||
? 'Time Format (1m or 2h or 3d or 4w)'
|
||||
: selectedTimePlaceholderValue
|
||||
}
|
||||
value={inputValue}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleInputChange}
|
||||
prefix={
|
||||
inputValue && inputStatus === 'success' ? (
|
||||
<CheckCircle size={14} color="#51E7A8" />
|
||||
) : (
|
||||
<Tooltip title="Enter time in format (e.g., 1m, 2h, 3d, 4w)">
|
||||
<Clock size={14} />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
suffix={
|
||||
<ChevronDown
|
||||
size={14}
|
||||
onClick={(): void => {
|
||||
setOpen(!open);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Popover>
|
||||
|
||||
{inputStatus === 'error' && inputErrorMessage && (
|
||||
<Typography.Title level={5} className="valid-format-error">
|
||||
{inputErrorMessage}
|
||||
</Typography.Title>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomTimePicker;
|
||||
@@ -13,6 +13,8 @@ import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
// hooks
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { isEmpty, isUndefined } from 'lodash-es';
|
||||
import {
|
||||
KeyboardEvent,
|
||||
MouseEvent,
|
||||
@@ -39,10 +41,13 @@ function RawLogView({
|
||||
data,
|
||||
linesPerRow,
|
||||
isTextOverflowEllipsisDisabled,
|
||||
selectedFields = [],
|
||||
}: RawLogViewProps): JSX.Element {
|
||||
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
|
||||
data.id,
|
||||
);
|
||||
const flattenLogData = useMemo(() => FlatLogData(data), [data]);
|
||||
|
||||
const {
|
||||
activeLog: activeContextLog,
|
||||
onSetActiveLog: handleSetActiveContextLog,
|
||||
@@ -62,12 +67,31 @@ function RawLogView({
|
||||
|
||||
const severityText = data.severity_text ? `${data.severity_text} |` : '';
|
||||
|
||||
const updatedSelecedFields = useMemo(
|
||||
() => selectedFields.filter((e) => e.name !== 'id'),
|
||||
[selectedFields],
|
||||
);
|
||||
|
||||
const attributesValues = updatedSelecedFields
|
||||
.map((field) => flattenLogData[field.name])
|
||||
.filter((attribute) => !isUndefined(attribute) && !isEmpty(attribute));
|
||||
|
||||
let attributesText = attributesValues.join(' | ');
|
||||
|
||||
if (attributesText.length > 0) {
|
||||
attributesText += ' | ';
|
||||
}
|
||||
|
||||
const text = useMemo(
|
||||
() =>
|
||||
typeof data.timestamp === 'string'
|
||||
? `${dayjs(data.timestamp).format()} | ${severityText} ${data.body}`
|
||||
: `${dayjs(data.timestamp / 1e6).format()} | ${severityText} ${data.body}`,
|
||||
[data.timestamp, data.body, severityText],
|
||||
? `${dayjs(data.timestamp).format()} | ${attributesText} ${severityText} ${
|
||||
data.body
|
||||
}`
|
||||
: `${dayjs(
|
||||
data.timestamp / 1e6,
|
||||
).format()} | ${attributesText} ${severityText} ${data.body}`,
|
||||
[data.timestamp, data.body, severityText, attributesText],
|
||||
);
|
||||
|
||||
const handleClickExpand = useCallback(() => {
|
||||
|
||||
@@ -48,8 +48,9 @@ export const RawLogContent = styled.div<RawLogContentProps>`
|
||||
line-clamp: ${linesPerRow};
|
||||
-webkit-box-orient: vertical;`};
|
||||
|
||||
font-size: 1rem;
|
||||
line-height: 2rem;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
padding: 4px;
|
||||
|
||||
cursor: ${({ $isActiveLog, $isReadOnly }): string =>
|
||||
$isActiveLog || $isReadOnly ? 'initial' : 'pointer'};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
export interface RawLogViewProps {
|
||||
@@ -6,6 +7,7 @@ export interface RawLogViewProps {
|
||||
isTextOverflowEllipsisDisabled?: boolean;
|
||||
data: ILog;
|
||||
linesPerRow: number;
|
||||
selectedFields?: IField[];
|
||||
}
|
||||
|
||||
export interface RawLogContentProps {
|
||||
|
||||
@@ -43,7 +43,7 @@ function DynamicColumnTable({
|
||||
: undefined,
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [columns]);
|
||||
}, [columns, dynamicColumns]);
|
||||
|
||||
const onToggleHandler = (index: number) => (
|
||||
checked: boolean,
|
||||
|
||||
@@ -73,12 +73,19 @@ function ResizeTable({
|
||||
}
|
||||
}, [columns]);
|
||||
|
||||
const paginationConfig = {
|
||||
hideOnSinglePage: true,
|
||||
showTotal: (total: number, range: number[]): string =>
|
||||
`${range[0]}-${range[1]} of ${total} items`,
|
||||
...tableParams.pagination,
|
||||
};
|
||||
|
||||
return onDragColumn ? (
|
||||
<ReactDragListView.DragColumn {...dragColumnParams} onDragEnd={onDragColumn}>
|
||||
<Table {...tableParams} />
|
||||
<Table {...tableParams} pagination={paginationConfig} />
|
||||
</ReactDragListView.DragColumn>
|
||||
) : (
|
||||
<Table {...tableParams} />
|
||||
<Table {...tableParams} pagination={paginationConfig} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export const Container = styled.div`
|
||||
&&& {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
const MAX_RPS_LIMIT = 100;
|
||||
export { MAX_RPS_LIMIT };
|
||||
|
||||
export const LEGEND = 'legend';
|
||||
|
||||
@@ -6,7 +6,6 @@ const ROUTES = {
|
||||
TRACE: '/trace',
|
||||
TRACE_DETAIL: '/trace/:id',
|
||||
TRACES_EXPLORER: '/traces-explorer',
|
||||
SETTINGS: '/settings',
|
||||
GET_STARTED: '/get-started',
|
||||
USAGE_EXPLORER: '/usage-explorer',
|
||||
APPLICATION: '/services',
|
||||
@@ -23,15 +22,18 @@ const ROUTES = {
|
||||
ERROR_DETAIL: '/error-detail',
|
||||
VERSION: '/status',
|
||||
MY_SETTINGS: '/my-settings',
|
||||
SETTINGS: '/settings',
|
||||
ORG_SETTINGS: '/settings/org-settings',
|
||||
INGESTION_SETTINGS: '/settings/ingestion-settings',
|
||||
SOMETHING_WENT_WRONG: '/something-went-wrong',
|
||||
UN_AUTHORIZED: '/un-authorized',
|
||||
NOT_FOUND: '/not-found',
|
||||
LOGS: '/logs',
|
||||
LOGS_EXPLORER: '/logs-explorer',
|
||||
LIVE_LOGS: '/logs-explorer/live',
|
||||
LOGS_PIPELINES: '/pipelines',
|
||||
LOGS_BASE: '/logs',
|
||||
LOGS: '/logs/logs-explorer',
|
||||
OLD_LOGS_EXPLORER: '/logs/old-logs-explorer',
|
||||
LOGS_EXPLORER: '/logs/logs-explorer',
|
||||
LIVE_LOGS: '/logs/logs-explorer/live',
|
||||
LOGS_PIPELINES: '/logs/pipelines',
|
||||
HOME_PAGE: '/',
|
||||
PASSWORD_RESET: '/password-reset',
|
||||
LIST_LICENSES: '/licenses',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const themeColors = {
|
||||
chartcolors: {
|
||||
robin: '#3F5ECC',
|
||||
dodgerBlue: '#2F80ED',
|
||||
mediumOrchid: '#BB6BD9',
|
||||
seaBuckthorn: '#F2994A',
|
||||
|
||||
@@ -15,6 +15,7 @@ export const ButtonContainer = styled.div`
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -48,6 +48,15 @@ import {
|
||||
urlKey,
|
||||
} from './utils';
|
||||
|
||||
type QueryParams = {
|
||||
order: string;
|
||||
offset: number;
|
||||
orderParam: string;
|
||||
pageSize: number;
|
||||
exceptionType?: string;
|
||||
serviceName?: string;
|
||||
};
|
||||
|
||||
function AllErrors(): JSX.Element {
|
||||
const { maxTime, minTime, loading } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
@@ -162,16 +171,23 @@ function AllErrors(): JSX.Element {
|
||||
filterKey,
|
||||
filterValue || '',
|
||||
);
|
||||
history.replace(
|
||||
`${pathname}?${createQueryParams({
|
||||
order: updatedOrder,
|
||||
offset: getUpdatedOffset,
|
||||
orderParam: getUpdatedParams,
|
||||
pageSize: getUpdatedPageSize,
|
||||
exceptionType: exceptionFilterValue,
|
||||
serviceName: serviceFilterValue,
|
||||
})}`,
|
||||
);
|
||||
|
||||
const queryParams: QueryParams = {
|
||||
order: updatedOrder,
|
||||
offset: getUpdatedOffset,
|
||||
orderParam: getUpdatedParams,
|
||||
pageSize: getUpdatedPageSize,
|
||||
};
|
||||
|
||||
if (exceptionFilterValue && exceptionFilterValue !== 'undefined') {
|
||||
queryParams.exceptionType = exceptionFilterValue;
|
||||
}
|
||||
|
||||
if (serviceFilterValue && serviceFilterValue !== 'undefined') {
|
||||
queryParams.serviceName = serviceFilterValue;
|
||||
}
|
||||
|
||||
history.replace(`${pathname}?${createQueryParams(queryParams)}`);
|
||||
confirm();
|
||||
},
|
||||
[
|
||||
@@ -198,8 +214,10 @@ function AllErrors(): JSX.Element {
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e): void =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
onChange={
|
||||
(e): void => setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
|
||||
// Need to fix this logic, when the value in empty, it's setting undefined string as value
|
||||
}
|
||||
allowClear
|
||||
defaultValue={getDefaultFilterValue(
|
||||
|
||||
53
frontend/src/container/AppLayout/AppLayout.styles.scss
Normal file
53
frontend/src/container/AppLayout/AppLayout.styles.scss
Normal file
@@ -0,0 +1,53 @@
|
||||
@import '@signozhq/design-tokens';
|
||||
|
||||
.app-layout {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.app-content {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.isDarkMode {
|
||||
.app-layout {
|
||||
.app-content {
|
||||
background: #0b0c0e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.isLightMode {
|
||||
.app-layout {
|
||||
.app-content {
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.trial-expiry-banner {
|
||||
padding: 8px;
|
||||
background-color: #f25733;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upgrade-link {
|
||||
padding: 0px;
|
||||
padding-right: 4px;
|
||||
display: inline !important;
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: white;
|
||||
text-decoration-thickness: 2px;
|
||||
text-underline-offset: 2px;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: white;
|
||||
text-decoration-thickness: 2px;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,22 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
import './AppLayout.styles.scss';
|
||||
|
||||
import { Flex } from 'antd';
|
||||
import getDynamicConfigs from 'api/dynamicConfigs/getDynamicConfigs';
|
||||
import getUserLatestVersion from 'api/user/getLatestVersion';
|
||||
import getUserVersion from 'api/user/getVersion';
|
||||
import cx from 'classnames';
|
||||
import ROUTES from 'constants/routes';
|
||||
import Header from 'container/Header';
|
||||
import SideNav from 'container/SideNav';
|
||||
import TopNav from 'container/TopNav';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useLicense from 'hooks/useLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ReactNode, useEffect, useMemo, useRef } from 'react';
|
||||
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -25,15 +34,20 @@ import {
|
||||
UPDATE_LATEST_VERSION_ERROR,
|
||||
} from 'types/actions/app';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
||||
|
||||
import { ChildrenContainer, Layout, LayoutContent } from './styles';
|
||||
import { getRouteKey } from './utils';
|
||||
|
||||
function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
const { isLoggedIn, user } = useSelector<AppState, AppReducer>(
|
||||
const { isLoggedIn, user, role } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const { data: licenseData, isFetching } = useLicense();
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const { t } = useTranslation(['titles']);
|
||||
|
||||
@@ -196,25 +210,68 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
const renderFullScreen =
|
||||
pathname === ROUTES.GET_STARTED || pathname === ROUTES.WORKSPACE_LOCKED;
|
||||
|
||||
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isFetching &&
|
||||
licenseData?.payload?.onTrial &&
|
||||
!licenseData?.payload?.trialConvertedToSubscription &&
|
||||
!licenseData?.payload?.workSpaceBlock &&
|
||||
getRemainingDays(licenseData?.payload.trialEnd) < 7
|
||||
) {
|
||||
setShowTrialExpiryBanner(true);
|
||||
}
|
||||
}, [licenseData, isFetching]);
|
||||
|
||||
const handleUpgrade = (): void => {
|
||||
if (role === 'ADMIN') {
|
||||
history.push(ROUTES.BILLING);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Layout className={isDarkMode ? 'darkMode' : 'lightMode'}>
|
||||
<Helmet>
|
||||
<title>{pageTitle}</title>
|
||||
</Helmet>
|
||||
|
||||
{isToDisplayLayout && <Header />}
|
||||
<Layout>
|
||||
{isToDisplayLayout && !renderFullScreen && <SideNav />}
|
||||
{showTrialExpiryBanner && (
|
||||
<div className="trial-expiry-banner">
|
||||
You are in free trial period. Your free trial will end on{' '}
|
||||
<span>
|
||||
{getFormattedDate(licenseData?.payload?.trialEnd || Date.now())}.
|
||||
</span>
|
||||
{role === 'ADMIN' ? (
|
||||
<span>
|
||||
{' '}
|
||||
Please{' '}
|
||||
<a className="upgrade-link" onClick={handleUpgrade}>
|
||||
upgrade
|
||||
</a>
|
||||
to continue using SigNoz features.
|
||||
</span>
|
||||
) : (
|
||||
'Please contact your administrator for upgrading to a paid plan.'
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||
<LayoutContent>
|
||||
<ChildrenContainer>
|
||||
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
||||
{children}
|
||||
</ChildrenContainer>
|
||||
</LayoutContent>
|
||||
</ErrorBoundary>
|
||||
</Layout>
|
||||
<Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}>
|
||||
{isToDisplayLayout && !renderFullScreen && (
|
||||
<SideNav licenseData={licenseData} isFetching={isFetching} />
|
||||
)}
|
||||
<div className="app-content">
|
||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||
<LayoutContent>
|
||||
<ChildrenContainer>
|
||||
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
||||
{children}
|
||||
</ChildrenContainer>
|
||||
</LayoutContent>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</Flex>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,11 +8,13 @@ export const Layout = styled(LayoutComponent)`
|
||||
min-height: calc(100vh - 8rem);
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export const LayoutContent = styled(LayoutComponent.Content)`
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
export const ChildrenContainer = styled.div`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Row } from 'antd';
|
||||
import { Row, Typography } from 'antd';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
@@ -33,7 +33,14 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<SelectTypeContainer>
|
||||
<h3> {t('choose_alert_type')} </h3>
|
||||
<Typography.Title
|
||||
level={4}
|
||||
style={{
|
||||
padding: '0 8px',
|
||||
}}
|
||||
>
|
||||
{t('choose_alert_type')}
|
||||
</Typography.Title>
|
||||
<Row>{renderOptions}</Row>
|
||||
</SelectTypeContainer>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Form, Select } from 'antd';
|
||||
import { Form, Select, Switch } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AlertDef, Labels } from 'types/api/alerts/def';
|
||||
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
|
||||
@@ -7,7 +8,6 @@ import { popupContainer } from 'utils/selectPopupContainer';
|
||||
import ChannelSelect from './ChannelSelect';
|
||||
import LabelSelect from './labels';
|
||||
import {
|
||||
ChannelSelectTip,
|
||||
FormContainer,
|
||||
FormItemMedium,
|
||||
InputSmall,
|
||||
@@ -19,14 +19,41 @@ import {
|
||||
const { Option } = Select;
|
||||
|
||||
interface BasicInfoProps {
|
||||
isNewRule: boolean;
|
||||
alertDef: AlertDef;
|
||||
setAlertDef: (a: AlertDef) => void;
|
||||
}
|
||||
|
||||
function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
function BasicInfo({
|
||||
isNewRule,
|
||||
alertDef,
|
||||
setAlertDef,
|
||||
}: BasicInfoProps): JSX.Element {
|
||||
const { t } = useTranslation('alerts');
|
||||
|
||||
const [
|
||||
shouldBroadCastToAllChannels,
|
||||
setShouldBroadCastToAllChannels,
|
||||
] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const hasPreferredChannels =
|
||||
(alertDef.preferredChannels && alertDef.preferredChannels.length > 0) ||
|
||||
isNewRule;
|
||||
|
||||
setShouldBroadCastToAllChannels(!hasPreferredChannels);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleBroadcastToAllChannels = (shouldBroadcast: boolean): void => {
|
||||
setShouldBroadCastToAllChannels(shouldBroadcast);
|
||||
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
broadcastToAll: shouldBroadcast,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StepHeading> {t('alert_form_step3')} </StepHeading>
|
||||
@@ -105,18 +132,38 @@ function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
|
||||
initialValues={alertDef.labels}
|
||||
/>
|
||||
</FormItemMedium>
|
||||
<FormItemMedium label="Notification Channels">
|
||||
<ChannelSelect
|
||||
currentValue={alertDef.preferredChannels}
|
||||
onSelectChannels={(preferredChannels): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
preferredChannels,
|
||||
});
|
||||
}}
|
||||
|
||||
<FormItemMedium
|
||||
name="alert_all_configured_channels"
|
||||
label="Alert all the configured channels"
|
||||
>
|
||||
<Switch
|
||||
checked={shouldBroadCastToAllChannels}
|
||||
onChange={handleBroadcastToAllChannels}
|
||||
/>
|
||||
<ChannelSelectTip> {t('channel_select_tooltip')}</ChannelSelectTip>
|
||||
</FormItemMedium>
|
||||
|
||||
{!shouldBroadCastToAllChannels && (
|
||||
<FormItemMedium
|
||||
label="Notification Channels"
|
||||
name="notification_channels"
|
||||
required
|
||||
rules={[
|
||||
{ required: true, message: requireErrorMessage(t('field_alert_name')) },
|
||||
]}
|
||||
>
|
||||
<ChannelSelect
|
||||
disabled={shouldBroadCastToAllChannels}
|
||||
currentValue={alertDef.preferredChannels}
|
||||
onSelectChannels={(preferredChannels): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
preferredChannels,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormItemMedium>
|
||||
)}
|
||||
</FormContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -8,11 +8,13 @@ import { useTranslation } from 'react-i18next';
|
||||
import { StyledSelect } from './styles';
|
||||
|
||||
export interface ChannelSelectProps {
|
||||
disabled?: boolean;
|
||||
currentValue?: string[];
|
||||
onSelectChannels: (s: string[]) => void;
|
||||
}
|
||||
|
||||
function ChannelSelect({
|
||||
disabled,
|
||||
currentValue,
|
||||
onSelectChannels,
|
||||
}: ChannelSelectProps): JSX.Element | null {
|
||||
@@ -52,6 +54,7 @@ function ChannelSelect({
|
||||
};
|
||||
return (
|
||||
<StyledSelect
|
||||
disabled={disabled}
|
||||
status={error ? 'error' : ''}
|
||||
mode="multiple"
|
||||
style={{ width: '100%' }}
|
||||
@@ -68,6 +71,7 @@ function ChannelSelect({
|
||||
}
|
||||
|
||||
ChannelSelect.defaultProps = {
|
||||
disabled: false,
|
||||
currentValue: [],
|
||||
};
|
||||
export default ChannelSelect;
|
||||
|
||||
@@ -150,6 +150,8 @@ function ChartPreview({
|
||||
thresholdUnit: alertDef?.condition.targetUnit,
|
||||
},
|
||||
],
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
}),
|
||||
[
|
||||
yAxisUnit,
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
import UserGuide from './UserGuide';
|
||||
import { getSelectedQueryOptions } from './utils';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function FormAlertRules({
|
||||
alertType,
|
||||
formInstance,
|
||||
@@ -78,6 +79,8 @@ function FormAlertRules({
|
||||
// use query client
|
||||
const ruleCache = useQueryClient();
|
||||
|
||||
const isNewRule = ruleId === 0;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// alertDef holds the form values to be posted
|
||||
@@ -108,8 +111,17 @@ function FormAlertRules({
|
||||
useShareBuilderUrl(sq);
|
||||
|
||||
useEffect(() => {
|
||||
setAlertDef(initialValue);
|
||||
}, [initialValue]);
|
||||
const broadcastToSpecificChannels =
|
||||
(initialValue &&
|
||||
initialValue.preferredChannels &&
|
||||
initialValue.preferredChannels.length > 0) ||
|
||||
isNewRule;
|
||||
|
||||
setAlertDef({
|
||||
...initialValue,
|
||||
broadcastToAll: !broadcastToSpecificChannels,
|
||||
});
|
||||
}, [initialValue, isNewRule]);
|
||||
|
||||
useEffect(() => {
|
||||
// Set selectedQueryName based on the length of queryOptions
|
||||
@@ -243,6 +255,7 @@ function FormAlertRules({
|
||||
const preparePostData = (): AlertDef => {
|
||||
const postableAlert: AlertDef = {
|
||||
...alertDef,
|
||||
preferredChannels: alertDef.broadcastToAll ? [] : alertDef.preferredChannels,
|
||||
alertType,
|
||||
source: window?.location.toString(),
|
||||
ruleType:
|
||||
@@ -386,7 +399,11 @@ function FormAlertRules({
|
||||
}, [t, isFormValid, memoizedPreparePostData, notifications]);
|
||||
|
||||
const renderBasicInfo = (): JSX.Element => (
|
||||
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
|
||||
<BasicInfo
|
||||
alertDef={alertDef}
|
||||
setAlertDef={setAlertDef}
|
||||
isNewRule={isNewRule}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderQBChartPreview = (): JSX.Element => (
|
||||
@@ -421,8 +438,6 @@ function FormAlertRules({
|
||||
/>
|
||||
);
|
||||
|
||||
const isNewRule = ruleId === 0;
|
||||
|
||||
const isAlertNameMissing = !formInstance.getFieldValue('alert');
|
||||
|
||||
const isAlertAvialableToSave =
|
||||
@@ -442,6 +457,10 @@ function FormAlertRules({
|
||||
}));
|
||||
};
|
||||
|
||||
const isChannelConfigurationValid =
|
||||
alertDef?.broadcastToAll ||
|
||||
(alertDef.preferredChannels && alertDef.preferredChannels.length > 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
@@ -489,7 +508,11 @@ function FormAlertRules({
|
||||
type="primary"
|
||||
onClick={onSaveHandler}
|
||||
icon={<SaveOutlined />}
|
||||
disabled={isAlertNameMissing || isAlertAvialableToSave}
|
||||
disabled={
|
||||
isAlertNameMissing ||
|
||||
isAlertAvialableToSave ||
|
||||
!isChannelConfigurationValid
|
||||
}
|
||||
>
|
||||
{isNewRule ? t('button_createrule') : t('button_savechanges')}
|
||||
</ActionButton>
|
||||
@@ -497,6 +520,7 @@ function FormAlertRules({
|
||||
|
||||
<ActionButton
|
||||
loading={loading || false}
|
||||
disabled={isAlertNameMissing || !isChannelConfigurationValid}
|
||||
type="default"
|
||||
onClick={onTestRuleHandler}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
.full-screen-header-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 24px 0;
|
||||
|
||||
.brand-logo {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.brand-logo-name {
|
||||
font-family: 'Work Sans', sans-serif;
|
||||
font-size: 24px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.brand-logo {
|
||||
.brand-logo-name {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
frontend/src/container/FullScreenHeader/FullScreenHeader.tsx
Normal file
28
frontend/src/container/FullScreenHeader/FullScreenHeader.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
import './FullScreenHeader.styles.scss';
|
||||
|
||||
import history from 'lib/history';
|
||||
|
||||
export default function FullScreenHeader({
|
||||
overrideRoute,
|
||||
}: {
|
||||
overrideRoute?: string;
|
||||
}): React.ReactElement {
|
||||
const handleLogoClick = (): void => {
|
||||
history.push(overrideRoute || '/');
|
||||
};
|
||||
return (
|
||||
<div className="full-screen-header-container">
|
||||
<div className="brand-logo" onClick={handleLogoClick}>
|
||||
<img src="/Logos/signoz-brand-logo.svg" alt="SigNoz" />
|
||||
|
||||
<div className="brand-logo-name">SigNoz</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FullScreenHeader.defaultProps = {
|
||||
overrideRoute: '/',
|
||||
};
|
||||
@@ -18,13 +18,14 @@ import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariab
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import uPlot from 'uplot';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import { getGraphVisibilityStateOnDataChange } from '../utils';
|
||||
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
|
||||
import GraphManager from './GraphManager';
|
||||
// import GraphManager from './GraphManager';
|
||||
@@ -36,13 +37,14 @@ function FullView({
|
||||
fullViewOptions = true,
|
||||
onClickHandler,
|
||||
name,
|
||||
originalName,
|
||||
yAxisUnit,
|
||||
options,
|
||||
onDragSelect,
|
||||
isDependedDataLoaded = false,
|
||||
graphsVisibilityStates,
|
||||
onToggleModelHandler,
|
||||
parentChartRef,
|
||||
setGraphsVisibilityStates,
|
||||
parentGraphVisibilityState,
|
||||
}: FullViewProps): JSX.Element {
|
||||
const { selectedTime: globalSelectedTime } = useSelector<
|
||||
AppState,
|
||||
@@ -55,6 +57,20 @@ function FullView({
|
||||
|
||||
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
||||
|
||||
const { graphVisibilityStates: localStoredVisibilityStates } = useMemo(
|
||||
() =>
|
||||
getGraphVisibilityStateOnDataChange({
|
||||
options,
|
||||
isExpandedName: false,
|
||||
name: originalName,
|
||||
}),
|
||||
[options, originalName],
|
||||
);
|
||||
|
||||
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState(
|
||||
localStoredVisibilityStates,
|
||||
);
|
||||
|
||||
const getSelectedTime = useCallback(
|
||||
() =>
|
||||
timeItems.find((e) => e.enum === (widget?.timePreferance || 'GLOBAL_TIME')),
|
||||
@@ -89,7 +105,7 @@ function FullView({
|
||||
panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE,
|
||||
});
|
||||
|
||||
const chartData = getUPlotChartData(response?.data?.payload);
|
||||
const chartData = getUPlotChartData(response?.data?.payload, widget.fillSpans);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
@@ -132,6 +148,8 @@ function FullView({
|
||||
thresholds: widget.thresholds,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
softMax: widget.softMax === undefined ? null : widget.softMax,
|
||||
softMin: widget.softMin === undefined ? null : widget.softMin,
|
||||
});
|
||||
|
||||
setChartOptions(newChartOptions);
|
||||
@@ -142,9 +160,9 @@ function FullView({
|
||||
useEffect(() => {
|
||||
graphsVisibilityStates?.forEach((e, i) => {
|
||||
fullViewChartRef?.current?.toggleGraph(i, e);
|
||||
parentChartRef?.current?.toggleGraph(i, e);
|
||||
});
|
||||
}, [graphsVisibilityStates, parentChartRef]);
|
||||
parentGraphVisibilityState(graphsVisibilityStates);
|
||||
}, [graphsVisibilityStates, parentGraphVisibilityState]);
|
||||
|
||||
if (response.isFetching) {
|
||||
return <Spinner height="100%" size="large" tip="Loading..." />;
|
||||
@@ -204,7 +222,7 @@ function FullView({
|
||||
{canModifyChart && chartOptions && !isDashboardLocked && (
|
||||
<GraphManager
|
||||
data={chartData}
|
||||
name={name}
|
||||
name={originalName}
|
||||
options={chartOptions}
|
||||
yAxisUnit={yAxisUnit}
|
||||
onToggleModelHandler={onToggleModelHandler}
|
||||
|
||||
@@ -50,13 +50,14 @@ export interface FullViewProps {
|
||||
fullViewOptions?: boolean;
|
||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||
name: string;
|
||||
originalName: string;
|
||||
options: uPlot.Options;
|
||||
yAxisUnit?: string;
|
||||
onDragSelect: (start: number, end: number) => void;
|
||||
isDependedDataLoaded?: boolean;
|
||||
graphsVisibilityStates?: boolean[];
|
||||
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
|
||||
setGraphsVisibilityStates: Dispatch<SetStateAction<boolean[]>>;
|
||||
parentChartRef: GraphManagerProps['lineChartRef'];
|
||||
parentGraphVisibilityState: Dispatch<SetStateAction<boolean[]>>;
|
||||
}
|
||||
|
||||
export interface GraphManagerProps extends UplotProps {
|
||||
@@ -64,8 +65,8 @@ export interface GraphManagerProps extends UplotProps {
|
||||
yAxisUnit?: string;
|
||||
onToggleModelHandler?: () => void;
|
||||
options: uPlot.Options;
|
||||
setGraphsVisibilityStates: FullViewProps['setGraphsVisibilityStates'];
|
||||
graphsVisibilityStates: FullViewProps['graphsVisibilityStates'];
|
||||
graphsVisibilityStates?: boolean[];
|
||||
setGraphsVisibilityStates: Dispatch<SetStateAction<boolean[]>>;
|
||||
lineChartRef?: MutableRefObject<ToggleGraphProps | undefined>;
|
||||
parentChartRef?: MutableRefObject<ToggleGraphProps | undefined>;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
@@ -39,13 +38,15 @@ function WidgetGraphComponent({
|
||||
queryResponse,
|
||||
errorMessage,
|
||||
name,
|
||||
onClickHandler,
|
||||
threshold,
|
||||
headerMenuList,
|
||||
isWarning,
|
||||
data,
|
||||
options,
|
||||
graphVisibiltyState,
|
||||
onClickHandler,
|
||||
onDragSelect,
|
||||
setGraphVisibility,
|
||||
}: WidgetGraphComponentProps): JSX.Element {
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
@@ -59,29 +60,28 @@ function WidgetGraphComponent({
|
||||
const lineChartRef = useRef<ToggleGraphProps>();
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { graphVisibilityStates: localStoredVisibilityStates } = useMemo(
|
||||
() =>
|
||||
getGraphVisibilityStateOnDataChange({
|
||||
useEffect(() => {
|
||||
if (queryResponse.isSuccess) {
|
||||
const {
|
||||
graphVisibilityStates: localStoredVisibilityState,
|
||||
} = getGraphVisibilityStateOnDataChange({
|
||||
options,
|
||||
isExpandedName: true,
|
||||
isExpandedName: false,
|
||||
name,
|
||||
}),
|
||||
[options, name],
|
||||
);
|
||||
|
||||
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState<
|
||||
boolean[]
|
||||
>(localStoredVisibilityStates);
|
||||
});
|
||||
setGraphVisibility(localStoredVisibilityState);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [queryResponse.isSuccess]);
|
||||
|
||||
useEffect(() => {
|
||||
setGraphsVisibilityStates(localStoredVisibilityStates);
|
||||
if (!lineChartRef.current) return;
|
||||
|
||||
localStoredVisibilityStates.forEach((state, index) => {
|
||||
graphVisibiltyState.forEach((state, index) => {
|
||||
lineChartRef.current?.toggleGraph(index, state);
|
||||
});
|
||||
setGraphsVisibilityStates(localStoredVisibilityStates);
|
||||
}, [localStoredVisibilityStates]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||
|
||||
@@ -124,6 +124,7 @@ function WidgetGraphComponent({
|
||||
if (setSelectedDashboard && updatedDashboard.payload) {
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
}
|
||||
setDeleteModal(false);
|
||||
featureResponse.refetch();
|
||||
},
|
||||
onError: () => {
|
||||
@@ -255,6 +256,7 @@ function WidgetGraphComponent({
|
||||
destroyOnClose
|
||||
onCancel={onDeleteModelHandler}
|
||||
open={deleteModal}
|
||||
confirmLoading={updateDashboardMutation.isLoading}
|
||||
title="Delete"
|
||||
height="10vh"
|
||||
onOk={onDeleteHandler}
|
||||
@@ -274,13 +276,14 @@ function WidgetGraphComponent({
|
||||
>
|
||||
<FullView
|
||||
name={`${name}expanded`}
|
||||
originalName={name}
|
||||
widget={widget}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
onToggleModelHandler={onToggleModelHandler}
|
||||
parentChartRef={lineChartRef}
|
||||
parentGraphVisibilityState={setGraphVisibility}
|
||||
onDragSelect={onDragSelect}
|
||||
setGraphsVisibilityStates={setGraphsVisibilityStates}
|
||||
graphsVisibilityStates={graphsVisibilityStates}
|
||||
options={options}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import getTimeString from 'lib/getTimeString';
|
||||
import history from 'lib/history';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import isEmpty from 'lodash-es/isEmpty';
|
||||
@@ -12,6 +17,7 @@ import _noop from 'lodash-es/noop';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
@@ -37,6 +43,12 @@ function GridCardGraph({
|
||||
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
const urlQuery = useUrlQuery();
|
||||
const location = useLocation();
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number): void => {
|
||||
@@ -46,10 +58,44 @@ function GridCardGraph({
|
||||
if (startTimestamp !== endTimestamp) {
|
||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||
}
|
||||
|
||||
const { maxTime, minTime } = GetMinMax('custom', [
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
]);
|
||||
|
||||
urlQuery.set(QueryParams.startTime, minTime.toString());
|
||||
urlQuery.set(QueryParams.endTime, maxTime.toString());
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
history.push(generatedUrl);
|
||||
},
|
||||
[dispatch],
|
||||
[dispatch, location.pathname, urlQuery],
|
||||
);
|
||||
|
||||
const handleBackNavigation = (): void => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const startTime = searchParams.get(QueryParams.startTime);
|
||||
const endTime = searchParams.get(QueryParams.endTime);
|
||||
|
||||
if (startTime && endTime && startTime !== endTime) {
|
||||
dispatch(
|
||||
UpdateTimeInterval('custom', [
|
||||
parseInt(getTimeString(startTime), 10),
|
||||
parseInt(getTimeString(endTime), 10),
|
||||
]),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('popstate', handleBackNavigation);
|
||||
|
||||
return (): void => {
|
||||
window.removeEventListener('popstate', handleBackNavigation);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isVisible = useIntersectionObserver(graphRef, undefined, true);
|
||||
@@ -70,11 +116,6 @@ function GridCardGraph({
|
||||
const isEmptyWidget =
|
||||
widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget);
|
||||
|
||||
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const queryResponse = useGetQueryRange(
|
||||
{
|
||||
selectedTime: widget?.timePreferance,
|
||||
@@ -122,6 +163,10 @@ function GridCardGraph({
|
||||
? headerMenuList.filter((menu) => menu !== MenuItemKeys.CreateAlerts)
|
||||
: headerMenuList;
|
||||
|
||||
const [graphVisibility, setGraphVisibility] = useState<boolean[]>(
|
||||
Array(queryResponse.data?.payload?.data.result.length || 0).fill(true),
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
getUPlotChartOptions({
|
||||
@@ -135,11 +180,17 @@ function GridCardGraph({
|
||||
thresholds: widget.thresholds,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
softMax: widget.softMax === undefined ? null : widget.softMax,
|
||||
softMin: widget.softMin === undefined ? null : widget.softMin,
|
||||
graphsVisibilityStates: graphVisibility,
|
||||
setGraphsVisibilityStates: setGraphVisibility,
|
||||
}),
|
||||
[
|
||||
widget?.id,
|
||||
widget?.yAxisUnit,
|
||||
widget.thresholds,
|
||||
widget.softMax,
|
||||
widget.softMin,
|
||||
queryResponse.data?.payload,
|
||||
containerDimensions,
|
||||
isDarkMode,
|
||||
@@ -147,6 +198,8 @@ function GridCardGraph({
|
||||
onClickHandler,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
graphVisibility,
|
||||
setGraphVisibility,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -167,6 +220,8 @@ function GridCardGraph({
|
||||
threshold={threshold}
|
||||
headerMenuList={menuList}
|
||||
onClickHandler={onClickHandler}
|
||||
graphVisibiltyState={graphVisibility}
|
||||
setGraphVisibility={setGraphVisibility}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { UplotProps } from 'components/Uplot/Uplot';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { MutableRefObject, ReactNode } from 'react';
|
||||
import { Dispatch, MutableRefObject, ReactNode, SetStateAction } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||
@@ -28,6 +28,8 @@ export interface WidgetGraphComponentProps extends UplotProps {
|
||||
threshold?: ReactNode;
|
||||
headerMenuList: MenuItemKeys[];
|
||||
isWarning: boolean;
|
||||
graphVisibiltyState: boolean[];
|
||||
setGraphVisibility: Dispatch<SetStateAction<boolean[]>>;
|
||||
}
|
||||
|
||||
export interface GridCardGraphProps {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
.fullscreen-grid-container {
|
||||
overflow: auto;
|
||||
margin-top: 1rem;
|
||||
|
||||
.react-grid-layout {
|
||||
border: none !important;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,3 +15,9 @@
|
||||
height: calc(100% - 30px);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.fullscreen-grid-container {
|
||||
background-color: rgb(250, 250, 250);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import './GridCardLayout.styles.scss';
|
||||
|
||||
import { PlusOutlined, SaveFilled } from '@ant-design/icons';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { themeColors } from 'constants/theme';
|
||||
@@ -8,9 +8,12 @@ import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { FullscreenIcon } from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -29,6 +32,7 @@ import {
|
||||
ReactGridLayout,
|
||||
} from './styles';
|
||||
import { GraphLayoutProps } from './types';
|
||||
import { removeUndefinedValuesFromLayout } from './utils';
|
||||
|
||||
function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
const {
|
||||
@@ -51,6 +55,8 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const [dashboardLayout, setDashboardLayout] = useState<Layout[]>([]);
|
||||
|
||||
const updateDashboardMutation = useUpdateDashboard();
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
@@ -71,6 +77,10 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
userRole,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setDashboardLayout(layouts);
|
||||
}, [layouts]);
|
||||
|
||||
const onSaveHandler = (): void => {
|
||||
if (!selectedDashboard) return;
|
||||
|
||||
@@ -78,7 +88,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
...selectedDashboard,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
layout: layouts.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET),
|
||||
layout: dashboardLayout.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET),
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
@@ -90,9 +100,6 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
setLayouts(updatedDashboard.payload.data.layout);
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
}
|
||||
notifications.success({
|
||||
message: t('dashboard:layout_saved_successfully'),
|
||||
});
|
||||
|
||||
featureResponse.refetch();
|
||||
},
|
||||
@@ -108,6 +115,32 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
? [...ViewMenuAction, ...EditMenuAction]
|
||||
: [...ViewMenuAction];
|
||||
|
||||
const handleLayoutChange = (layout: Layout[]): void => {
|
||||
const filterLayout = removeUndefinedValuesFromLayout(layout);
|
||||
const filterDashboardLayout = removeUndefinedValuesFromLayout(
|
||||
dashboardLayout,
|
||||
);
|
||||
if (!isEqual(filterLayout, filterDashboardLayout)) {
|
||||
setDashboardLayout(layout);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
dashboardLayout &&
|
||||
Array.isArray(dashboardLayout) &&
|
||||
dashboardLayout.length > 0 &&
|
||||
!isEqual(layouts, dashboardLayout) &&
|
||||
!isDashboardLocked &&
|
||||
saveLayoutPermission &&
|
||||
!updateDashboardMutation.isLoading
|
||||
) {
|
||||
onSaveHandler();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dashboardLayout]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonContainer>
|
||||
@@ -120,17 +153,6 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
{t('dashboard:full_view')}
|
||||
</Button>
|
||||
|
||||
{!isDashboardLocked && saveLayoutPermission && (
|
||||
<Button
|
||||
loading={updateDashboardMutation.isLoading}
|
||||
onClick={onSaveHandler}
|
||||
icon={<SaveFilled />}
|
||||
disabled={updateDashboardMutation.isLoading}
|
||||
>
|
||||
{t('dashboard:save_layout')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!isDashboardLocked && addPanelPermission && (
|
||||
<Button
|
||||
onClick={onAddPanelHandler}
|
||||
@@ -153,12 +175,12 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
isDroppable={!isDashboardLocked && addPanelPermission}
|
||||
isResizable={!isDashboardLocked && addPanelPermission}
|
||||
allowOverlap={false}
|
||||
onLayoutChange={setLayouts}
|
||||
onLayoutChange={handleLayoutChange}
|
||||
draggableHandle=".drag-handle"
|
||||
layout={layouts}
|
||||
layout={dashboardLayout}
|
||||
style={{ backgroundColor: isDarkMode ? '' : themeColors.snowWhite }}
|
||||
>
|
||||
{layouts.map((layout) => {
|
||||
{dashboardLayout.map((layout) => {
|
||||
const { i: id } = layout;
|
||||
const currentWidget = (widgets || [])?.find((e) => e.id === id);
|
||||
|
||||
@@ -178,6 +200,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
|
||||
name={currentWidget?.id || ''}
|
||||
headerMenuList={widgetActions}
|
||||
variables={variables}
|
||||
fillSpans={currentWidget?.fillSpans}
|
||||
/>
|
||||
</Card>
|
||||
</CardContainer>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.widget-header-title {
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useCallback } from 'react';
|
||||
import { Layout } from 'react-grid-layout';
|
||||
|
||||
import { EMPTY_WIDGET_LAYOUT } from './config';
|
||||
import GraphLayoutContainer from './GridCardLayout';
|
||||
|
||||
function GridGraph(): JSX.Element {
|
||||
const { handleToggleDashboardSlider, setLayouts } = useDashboard();
|
||||
const { handleToggleDashboardSlider } = useDashboard();
|
||||
|
||||
const onEmptyWidgetHandler = useCallback(() => {
|
||||
handleToggleDashboardSlider(true);
|
||||
|
||||
setLayouts((preLayout: Layout[]) => [
|
||||
EMPTY_WIDGET_LAYOUT,
|
||||
...(preLayout || []),
|
||||
]);
|
||||
}, [handleToggleDashboardSlider, setLayouts]);
|
||||
}, [handleToggleDashboardSlider]);
|
||||
|
||||
return <GraphLayoutContainer onAddPanelHandler={onEmptyWidgetHandler} />;
|
||||
}
|
||||
|
||||
8
frontend/src/container/GridCardLayout/utils.ts
Normal file
8
frontend/src/container/GridCardLayout/utils.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Layout } from 'react-grid-layout';
|
||||
|
||||
export const removeUndefinedValuesFromLayout = (layout: Layout[]): Layout[] =>
|
||||
layout.map((obj) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(obj).filter(([, value]) => value !== undefined),
|
||||
),
|
||||
) as Layout[];
|
||||
@@ -24,10 +24,14 @@ import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
import DateComponent from '../../components/ResizeTable/TableComponent/DateComponent';
|
||||
import useSortableTable from '../../hooks/ResizeTable/useSortableTable';
|
||||
import useUrlQuery from '../../hooks/useUrlQuery';
|
||||
import { GettableAlert } from '../../types/api/alerts/get';
|
||||
import ImportJSON from './ImportJSON';
|
||||
import { ButtonContainer, NewDashboardButton, TableContainer } from './styles';
|
||||
import DeleteButton from './TableComponents/DeleteButton';
|
||||
import Name from './TableComponents/Name';
|
||||
import { filterDashboard } from './utils';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
@@ -55,8 +59,26 @@ function DashboardsList(): JSX.Element {
|
||||
const [uploadedGrafana, setUploadedGrafana] = useState<boolean>(false);
|
||||
const [isFilteringDashboards, setIsFilteringDashboards] = useState(false);
|
||||
|
||||
const params = useUrlQuery();
|
||||
const orderColumnParam = params.get('columnKey');
|
||||
const orderQueryParam = params.get('order');
|
||||
const paginationParam = params.get('page');
|
||||
const searchParams = params.get('search');
|
||||
const [searchString, setSearchString] = useState<string>(searchParams || '');
|
||||
|
||||
const [dashboards, setDashboards] = useState<Dashboard[]>();
|
||||
|
||||
const sortingOrder: 'ascend' | 'descend' | null =
|
||||
orderQueryParam === 'ascend' || orderQueryParam === 'descend'
|
||||
? orderQueryParam
|
||||
: null;
|
||||
|
||||
const { sortedInfo, handleChange } = useSortableTable<GettableAlert>(
|
||||
sortingOrder,
|
||||
orderColumnParam || '',
|
||||
searchString,
|
||||
);
|
||||
|
||||
const sortDashboardsByCreatedAt = (dashboards: Dashboard[]): void => {
|
||||
const sortedDashboards = dashboards.sort(
|
||||
(a, b) =>
|
||||
@@ -67,7 +89,12 @@ function DashboardsList(): JSX.Element {
|
||||
|
||||
useEffect(() => {
|
||||
sortDashboardsByCreatedAt(dashboardListResponse);
|
||||
}, [dashboardListResponse]);
|
||||
const filteredDashboards = filterDashboard(
|
||||
searchString,
|
||||
dashboardListResponse,
|
||||
);
|
||||
setDashboards(filteredDashboards || []);
|
||||
}, [dashboardListResponse, searchString]);
|
||||
|
||||
const [newDashboardState, setNewDashboardState] = useState({
|
||||
loading: false,
|
||||
@@ -89,6 +116,10 @@ function DashboardsList(): JSX.Element {
|
||||
return prev - next;
|
||||
},
|
||||
render: DateComponent,
|
||||
sortOrder:
|
||||
sortedInfo.columnKey === DynamicColumnsKey.CreatedAt
|
||||
? sortedInfo.order
|
||||
: null,
|
||||
},
|
||||
{
|
||||
title: 'Created By',
|
||||
@@ -108,6 +139,10 @@ function DashboardsList(): JSX.Element {
|
||||
return prev - next;
|
||||
},
|
||||
render: DateComponent,
|
||||
sortOrder:
|
||||
sortedInfo.columnKey === DynamicColumnsKey.UpdatedAt
|
||||
? sortedInfo.order
|
||||
: null,
|
||||
},
|
||||
{
|
||||
title: 'Last Updated By',
|
||||
@@ -249,28 +284,13 @@ function DashboardsList(): JSX.Element {
|
||||
return menuItems;
|
||||
}, [createNewDashboard, isDashboardListLoading, onNewDashboardHandler, t]);
|
||||
|
||||
const searchArrayOfObjects = (searchValue: string): any[] => {
|
||||
// Convert the searchValue to lowercase for case-insensitive search
|
||||
const searchValueLowerCase = searchValue.toLowerCase();
|
||||
|
||||
// Use the filter method to find matching objects
|
||||
return dashboardListResponse.filter((item: any) => {
|
||||
// Convert each property value to lowercase for case-insensitive search
|
||||
const itemValues = Object.values(item?.data).map((value: any) =>
|
||||
value.toString().toLowerCase(),
|
||||
);
|
||||
|
||||
// Check if any property value contains the searchValue
|
||||
return itemValues.some((value) => value.includes(searchValueLowerCase));
|
||||
});
|
||||
};
|
||||
|
||||
const handleSearch = useDebouncedFn((event: unknown): void => {
|
||||
setIsFilteringDashboards(true);
|
||||
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
|
||||
const filteredDashboards = searchArrayOfObjects(searchText);
|
||||
const filteredDashboards = filterDashboard(searchText, dashboardListResponse);
|
||||
setDashboards(filteredDashboards);
|
||||
setIsFilteringDashboards(false);
|
||||
setSearchString(searchText);
|
||||
}, 500);
|
||||
|
||||
const GetHeader = useMemo(
|
||||
@@ -283,6 +303,7 @@ function DashboardsList(): JSX.Element {
|
||||
onChange={handleSearch}
|
||||
loading={isFilteringDashboards}
|
||||
style={{ marginBottom: 16, marginTop: 16 }}
|
||||
defaultValue={searchString}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
@@ -328,11 +349,12 @@ function DashboardsList(): JSX.Element {
|
||||
newDashboardState.loading,
|
||||
newDashboardState.error,
|
||||
getText,
|
||||
searchString,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card style={{ margin: '16px 0' }}>
|
||||
{GetHeader}
|
||||
|
||||
<TableContainer>
|
||||
@@ -349,12 +371,14 @@ function DashboardsList(): JSX.Element {
|
||||
pageSize: 10,
|
||||
defaultPageSize: 10,
|
||||
total: data?.length || 0,
|
||||
defaultCurrent: Number(paginationParam) || 1,
|
||||
}}
|
||||
showHeader
|
||||
bordered
|
||||
sticky
|
||||
loading={isDashboardListLoading}
|
||||
dataSource={data}
|
||||
onChange={handleChange}
|
||||
showSorterTooltip
|
||||
/>
|
||||
</TableContainer>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.delete-modal {
|
||||
.ant-modal-confirm-body {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import './DeleteButton.styles.scss';
|
||||
|
||||
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
|
||||
import { Modal, Tooltip, Typography } from 'antd';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
@@ -64,6 +66,7 @@ function DeleteButton({
|
||||
okText: 'Delete',
|
||||
okButtonProps: { danger: true },
|
||||
centered: true,
|
||||
className: 'delete-modal',
|
||||
});
|
||||
}, [modal, name, deleteDashboardMutation, notifications, t, queryClient]);
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { blue } from '@ant-design/colors';
|
||||
import { Typography } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const TableLinkText = styled(Typography.Text)`
|
||||
color: ${blue.primary} !important;
|
||||
color: #4e74f8 !important;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
27
frontend/src/container/ListOfDashboard/utils.ts
Normal file
27
frontend/src/container/ListOfDashboard/utils.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
export const filterDashboard = (
|
||||
searchValue: string,
|
||||
dashboardList: Dashboard[],
|
||||
): Dashboard[] => {
|
||||
const searchValueLowerCase = searchValue?.toLowerCase();
|
||||
|
||||
// Filter by title, description, tags
|
||||
return dashboardList.filter((item: Dashboard) => {
|
||||
const { title, description, tags } = item.data;
|
||||
const itemValuesNew = [title, description];
|
||||
|
||||
if (tags && tags.length > 0) {
|
||||
itemValuesNew.push(...tags);
|
||||
}
|
||||
|
||||
// Check if any property value contains the searchValue
|
||||
return itemValuesNew.some((value) => {
|
||||
if (value) {
|
||||
return value.toLowerCase().includes(searchValueLowerCase);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -70,7 +70,12 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
(_: number, log: ILog): JSX.Element => {
|
||||
if (options.format === 'raw') {
|
||||
return (
|
||||
<RawLogView key={log.id} data={log} linesPerRow={options.maxLines} />
|
||||
<RawLogView
|
||||
key={log.id}
|
||||
data={log}
|
||||
linesPerRow={options.maxLines}
|
||||
selectedFields={selectedFields}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { Button, ButtonProps } from 'antd';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
|
||||
|
||||
export const LiveButtonStyled = styled(Button)<ButtonProps>`
|
||||
background-color: rgba(${themeColors.buttonSuccessRgb}, 0.9);
|
||||
background-color: #1eb475;
|
||||
|
||||
${({ danger }): FlattenSimpleInterpolation =>
|
||||
!danger
|
||||
? css`
|
||||
&:hover {
|
||||
background-color: rgba(${themeColors.buttonSuccessRgb}, 1) !important;
|
||||
background-color: #1eb475 !important;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: rgba(${themeColors.buttonSuccessRgb}, 0.7) !important;
|
||||
background-color: #1eb475 !important;
|
||||
}
|
||||
`
|
||||
: css``}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Col, Row, Space } from 'antd';
|
||||
import { Col, Row, Space, Typography } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import NewExplorerCTA from 'container/NewExplorerCTA';
|
||||
import { FileText } from 'lucide-react';
|
||||
import { useLocation } from 'react-use';
|
||||
|
||||
import ShowBreadcrumbs from '../TopNav/Breadcrumbs';
|
||||
import DateTimeSelector from '../TopNav/DateTimeSelection';
|
||||
import { Container } from './styles';
|
||||
import { LocalTopNavProps } from './types';
|
||||
@@ -10,13 +12,25 @@ function LocalTopNav({
|
||||
actions,
|
||||
renderPermissions,
|
||||
}: LocalTopNavProps): JSX.Element | null {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const isLiveLogsPage = pathname === ROUTES.LIVE_LOGS;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Col span={16}>
|
||||
<ShowBreadcrumbs />
|
||||
</Col>
|
||||
{isLiveLogsPage && (
|
||||
<Col span={16}>
|
||||
<Space>
|
||||
<FileText color="#fff" size={16} />
|
||||
|
||||
<Col span={8}>
|
||||
<Typography.Title level={4} style={{ marginTop: 0, marginBottom: 0 }}>
|
||||
Live Logs
|
||||
</Typography.Title>
|
||||
</Space>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
<Col span={isLiveLogsPage ? 8 : 24}>
|
||||
<Row justify="end">
|
||||
<Space align="start" size={30} direction="horizontal">
|
||||
<NewExplorerCTA />
|
||||
|
||||
@@ -3,7 +3,7 @@ import styled from 'styled-components';
|
||||
|
||||
export const Container = styled(Row)`
|
||||
&&& {
|
||||
margin-top: 2rem;
|
||||
margin-top: 1rem;
|
||||
min-height: 8vh;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -63,7 +63,7 @@ function LogDetailedView({
|
||||
queryString,
|
||||
);
|
||||
|
||||
history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`);
|
||||
history.replace(`${ROUTES.OLD_LOGS_EXPLORER}?q=${updatedQueryString}`);
|
||||
},
|
||||
[history, queryString],
|
||||
);
|
||||
|
||||
@@ -80,7 +80,12 @@ function LogsExplorerList({
|
||||
(_: number, log: ILog): JSX.Element => {
|
||||
if (options.format === 'raw') {
|
||||
return (
|
||||
<RawLogView key={log.id} data={log} linesPerRow={options.maxLines} />
|
||||
<RawLogView
|
||||
key={log.id}
|
||||
data={log}
|
||||
linesPerRow={options.maxLines}
|
||||
selectedFields={selectedFields}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,14 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
||||
const log = logs[index];
|
||||
|
||||
if (viewMode === 'raw') {
|
||||
return <RawLogView key={log.id} data={log} linesPerRow={linesPerRow} />;
|
||||
return (
|
||||
<RawLogView
|
||||
key={log.id}
|
||||
data={log}
|
||||
linesPerRow={linesPerRow}
|
||||
selectedFields={selected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -74,6 +74,7 @@ function LogsTopNav(): JSX.Element {
|
||||
icon={<PlayCircleFilled />}
|
||||
onClick={handleGoLive}
|
||||
type="primary"
|
||||
size="small"
|
||||
>
|
||||
Go Live
|
||||
</LiveButtonStyled>
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { Button, ButtonProps } from 'antd';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
|
||||
|
||||
export const LiveButtonStyled = styled(Button)<ButtonProps>`
|
||||
background-color: rgba(${themeColors.buttonSuccessRgb}, 0.9);
|
||||
background-color: #1eb475;
|
||||
|
||||
${({ danger }): FlattenSimpleInterpolation =>
|
||||
!danger
|
||||
? css`
|
||||
&:hover {
|
||||
background-color: rgba(${themeColors.buttonSuccessRgb}, 1) !important;
|
||||
background-color: #1eb475 !important;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: rgba(${themeColors.buttonSuccessRgb}, 0.7) !important;
|
||||
background-color: #1eb475 !important;
|
||||
}
|
||||
`
|
||||
: css``}
|
||||
|
||||
@@ -20,4 +20,6 @@ export const getWidgetQueryBuilder = ({
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
title,
|
||||
yAxisUnit,
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
});
|
||||
|
||||
@@ -13,8 +13,10 @@ import {
|
||||
convertRawQueriesToTraceSelectedTags,
|
||||
resourceAttributesToTagFilterItems,
|
||||
} from 'hooks/useResourceAttribute/utils';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@@ -52,8 +54,10 @@ function Application(): JSX.Element {
|
||||
);
|
||||
const { servicename } = useParams<IServiceName>();
|
||||
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
||||
const { search } = useLocation();
|
||||
const { search, pathname } = useLocation();
|
||||
const { queries } = useResourceAttribute();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const selectedTags = useMemo(
|
||||
() => (convertRawQueriesToTraceSelectedTags(queries) as Tags[]) || [],
|
||||
[queries],
|
||||
@@ -104,7 +108,10 @@ function Application(): JSX.Element {
|
||||
);
|
||||
|
||||
const topLevelOperationsRoute = useMemo(
|
||||
() => (topLevelOperations ? topLevelOperations[servicename || ''] : []),
|
||||
() =>
|
||||
topLevelOperations
|
||||
? defaultTo(topLevelOperations[servicename || ''], [])
|
||||
: [],
|
||||
[servicename, topLevelOperations],
|
||||
);
|
||||
|
||||
@@ -157,11 +164,16 @@ function Application(): JSX.Element {
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
|
||||
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
|
||||
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
|
||||
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
|
||||
history.replace(generatedUrl);
|
||||
|
||||
if (startTimestamp !== endTimestamp) {
|
||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||
}
|
||||
},
|
||||
[dispatch],
|
||||
[dispatch, pathname, urlQuery],
|
||||
);
|
||||
|
||||
const onErrorTrackHandler = (timestamp: number): (() => void) => (): void => {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import Spinner from 'components/Spinner';
|
||||
import { useGetMetricMeta } from 'hooks/apDex/useGetMetricMeta';
|
||||
import useErrorNotification from 'hooks/useErrorNotification';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { IServiceName } from '../../types';
|
||||
import ApDexMetrics from './ApDexMetrics';
|
||||
import { metricMeta } from './constants';
|
||||
import { ApDexDataSwitcherProps } from './types';
|
||||
@@ -13,7 +15,8 @@ function ApDexMetricsApplication({
|
||||
thresholdValue,
|
||||
topLevelOperationsRoute,
|
||||
}: ApDexDataSwitcherProps): JSX.Element {
|
||||
const { data, isLoading, error } = useGetMetricMeta(metricMeta);
|
||||
const { servicename } = useParams<IServiceName>();
|
||||
const { data, isLoading, error } = useGetMetricMeta(metricMeta, servicename);
|
||||
useErrorNotification(error);
|
||||
|
||||
if (isLoading) {
|
||||
|
||||
5
frontend/src/container/MySettings/MySettings.styles.scss
Normal file
5
frontend/src/container/MySettings/MySettings.styles.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
.flexBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button, Space, Typography } from 'antd';
|
||||
import { Button, Card, Space, Typography } from 'antd';
|
||||
import changeMyPassword from 'api/user/changeMyPassword';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { Save } from 'lucide-react';
|
||||
import { isPasswordNotValidMessage, isPasswordValid } from 'pages/SignUp/utils';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -20,9 +21,7 @@ function PasswordContainer(): JSX.Element {
|
||||
false,
|
||||
);
|
||||
|
||||
const defaultPlaceHolder = t('input_password', {
|
||||
ns: 'settings',
|
||||
});
|
||||
const defaultPlaceHolder = '*************';
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
@@ -89,66 +88,69 @@ function PasswordContainer(): JSX.Element {
|
||||
currentPassword === updatePassword;
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large">
|
||||
<Typography.Title level={3}>
|
||||
{t('change_password', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
</Typography.Title>
|
||||
<Space direction="vertical">
|
||||
<Typography>
|
||||
{t('current_password', {
|
||||
<Card>
|
||||
<Space direction="vertical" size="small">
|
||||
<Typography.Title level={4} style={{ marginTop: 0 }}>
|
||||
{t('change_password', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
</Typography>
|
||||
<Password
|
||||
disabled={isLoading}
|
||||
placeholder={defaultPlaceHolder}
|
||||
onChange={(event): void => {
|
||||
setCurrentPassword(event.target.value);
|
||||
}}
|
||||
value={currentPassword}
|
||||
/>
|
||||
</Space>
|
||||
<Space direction="vertical">
|
||||
<Typography>
|
||||
{t('new_password', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
</Typography>
|
||||
<Password
|
||||
disabled={isLoading}
|
||||
placeholder={defaultPlaceHolder}
|
||||
onChange={(event): void => {
|
||||
const updatedValue = event.target.value;
|
||||
setUpdatePassword(updatedValue);
|
||||
}}
|
||||
value={updatePassword}
|
||||
/>
|
||||
</Space>
|
||||
<Space>
|
||||
{isPasswordPolicyError && (
|
||||
<Typography.Paragraph
|
||||
style={{
|
||||
color: '#D89614',
|
||||
marginTop: '0.50rem',
|
||||
</Typography.Title>
|
||||
<Space direction="vertical">
|
||||
<Typography>
|
||||
{t('current_password', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
</Typography>
|
||||
<Password
|
||||
disabled={isLoading}
|
||||
placeholder={defaultPlaceHolder}
|
||||
onChange={(event): void => {
|
||||
setCurrentPassword(event.target.value);
|
||||
}}
|
||||
>
|
||||
{isPasswordNotValidMessage}
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
value={currentPassword}
|
||||
/>
|
||||
</Space>
|
||||
<Space direction="vertical">
|
||||
<Typography>
|
||||
{t('new_password', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
</Typography>
|
||||
<Password
|
||||
disabled={isLoading}
|
||||
placeholder={defaultPlaceHolder}
|
||||
onChange={(event): void => {
|
||||
const updatedValue = event.target.value;
|
||||
setUpdatePassword(updatedValue);
|
||||
}}
|
||||
value={updatePassword}
|
||||
/>
|
||||
</Space>
|
||||
<Space>
|
||||
{isPasswordPolicyError && (
|
||||
<Typography.Paragraph
|
||||
style={{
|
||||
color: '#D89614',
|
||||
marginTop: '0.50rem',
|
||||
}}
|
||||
>
|
||||
{isPasswordNotValidMessage}
|
||||
</Typography.Paragraph>
|
||||
)}
|
||||
</Space>
|
||||
<Button
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
onClick={onChangePasswordClickHandler}
|
||||
type="primary"
|
||||
>
|
||||
<Save size={12} style={{ marginRight: '8px' }} />{' '}
|
||||
{t('change_password', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
</Button>
|
||||
</Space>
|
||||
<Button
|
||||
disabled={isDisabled}
|
||||
loading={isLoading}
|
||||
onClick={onChangePasswordClickHandler}
|
||||
type="primary"
|
||||
>
|
||||
{t('change_password', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
.userInfo-label {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.userInfo-value {
|
||||
min-width: 20rem;
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Button, Space, Typography } from 'antd';
|
||||
import '../MySettings.styles.scss';
|
||||
import './UserInfo.styles.scss';
|
||||
|
||||
import { Button, Card, Flex, Input, Space, Typography } from 'antd';
|
||||
import editUser from 'api/user/editUser';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { PencilIcon, UserSquare } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@@ -12,7 +16,7 @@ import AppReducer from 'types/reducer/app';
|
||||
|
||||
import { NameInput } from '../styles';
|
||||
|
||||
function UpdateName(): JSX.Element {
|
||||
function UserInfo(): JSX.Element {
|
||||
const { user, role, org, userFlags } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
@@ -72,28 +76,51 @@ function UpdateName(): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card>
|
||||
<Space direction="vertical" size="middle">
|
||||
<Typography>Name</Typography>
|
||||
<NameInput
|
||||
placeholder="Your Name"
|
||||
onChange={(event): void => {
|
||||
setChangedName(event.target.value);
|
||||
}}
|
||||
value={changedName}
|
||||
disabled={loading}
|
||||
/>
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
onClick={onClickUpdateHandler}
|
||||
type="primary"
|
||||
>
|
||||
Update Name
|
||||
</Button>
|
||||
<Flex gap={8}>
|
||||
<UserSquare />{' '}
|
||||
<Typography.Title level={4} style={{ marginTop: 0 }}>
|
||||
User Details
|
||||
</Typography.Title>
|
||||
</Flex>
|
||||
|
||||
<Flex gap={16}>
|
||||
<Space>
|
||||
<Typography className="userInfo-label">Name</Typography>
|
||||
<NameInput
|
||||
placeholder="Your Name"
|
||||
onChange={(event): void => {
|
||||
setChangedName(event.target.value);
|
||||
}}
|
||||
value={changedName}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
<Button
|
||||
className="flexBtn"
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
onClick={onClickUpdateHandler}
|
||||
type="primary"
|
||||
>
|
||||
<PencilIcon size={12} /> Update
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Space>
|
||||
<Typography className="userInfo-label"> Email </Typography>
|
||||
<Input className="userInfo-value" value={user.email} disabled />
|
||||
</Space>
|
||||
|
||||
<Space>
|
||||
<Typography className="userInfo-label"> Role </Typography>
|
||||
<Input className="userInfo-value" value={role || ''} disabled />
|
||||
</Space>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default UpdateName;
|
||||
export default UserInfo;
|
||||
@@ -1,16 +1,28 @@
|
||||
import { Space, Typography } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import './MySettings.styles.scss';
|
||||
|
||||
import { Button, Space } from 'antd';
|
||||
import { Logout } from 'api/utils';
|
||||
import { LogOut } from 'lucide-react';
|
||||
|
||||
import Password from './Password';
|
||||
import UpdateName from './UpdateName';
|
||||
import UserInfo from './UserInfo';
|
||||
|
||||
function MySettings(): JSX.Element {
|
||||
const { t } = useTranslation(['routes']);
|
||||
return (
|
||||
<Space direction="vertical" size="large">
|
||||
<Typography.Title level={2}>{t('my_settings')}</Typography.Title>
|
||||
<UpdateName />
|
||||
<Space
|
||||
direction="vertical"
|
||||
size="large"
|
||||
style={{
|
||||
margin: '16px 0',
|
||||
}}
|
||||
>
|
||||
<UserInfo />
|
||||
|
||||
<Password />
|
||||
|
||||
<Button className="flexBtn" onClick={(): void => Logout()} type="primary">
|
||||
<LogOut size={12} /> Logout
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,6 +63,8 @@ function DashboardGraphSlider(): JSX.Element {
|
||||
panelTypes: name,
|
||||
query: initialQueriesMap.metrics,
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
softMax: null,
|
||||
softMin: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -6,3 +6,11 @@
|
||||
text-overflow: ellipsis;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.variable-item {
|
||||
.variable-select {
|
||||
.ant-select-dropdown {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,16 @@ import { WarningOutlined } from '@ant-design/icons';
|
||||
import { Input, Popover, Select, Tooltip, Typography } from 'antd';
|
||||
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
||||
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
||||
import { debounce } from 'lodash-es';
|
||||
import map from 'lodash-es/map';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { variablePropsToPayloadVariables } from '../utils';
|
||||
import { SelectItemStyle, VariableContainer, VariableValue } from './styles';
|
||||
@@ -44,6 +45,7 @@ const getSelectValue = (
|
||||
return selectedValue?.toString() || '';
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function VariableItem({
|
||||
variableData,
|
||||
existingVariables,
|
||||
@@ -55,24 +57,8 @@ function VariableItem({
|
||||
[],
|
||||
);
|
||||
|
||||
const [variableValue, setVaribleValue] = useState(
|
||||
variableData?.selectedValue?.toString() || '',
|
||||
);
|
||||
|
||||
const debouncedVariableValue = useDebounce(variableValue, 500);
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<null | string>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const { selectedValue } = variableData;
|
||||
|
||||
if (selectedValue) {
|
||||
setVaribleValue(selectedValue?.toString());
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [variableData]);
|
||||
|
||||
const getDependentVariables = (queryValue: string): string[] => {
|
||||
const matches = queryValue.match(variableRegexPattern);
|
||||
|
||||
@@ -92,7 +78,12 @@ function VariableItem({
|
||||
const variableName = variableData.name || '';
|
||||
|
||||
dependentVariables?.forEach((element) => {
|
||||
dependentVariablesStr += `${element}${existingVariables[element]?.selectedValue}`;
|
||||
const [, variable] =
|
||||
Object.entries(existingVariables).find(
|
||||
([, value]) => value.name === element,
|
||||
) || [];
|
||||
|
||||
dependentVariablesStr += `${element}${variable?.selectedValue}`;
|
||||
});
|
||||
|
||||
const variableKey = dependentVariablesStr.replace(/\s/g, '');
|
||||
@@ -204,6 +195,9 @@ function VariableItem({
|
||||
}
|
||||
};
|
||||
|
||||
// do not debounce the above function as we do not need debounce in select variables
|
||||
const debouncedHandleChange = debounce(handleChange, 500);
|
||||
|
||||
const { selectedValue } = variableData;
|
||||
const selectedValueStringified = useMemo(() => getSelectValue(selectedValue), [
|
||||
selectedValue,
|
||||
@@ -219,14 +213,6 @@ function VariableItem({
|
||||
: undefined;
|
||||
const enableSelectAll = variableData.multiSelect && variableData.showALLOption;
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedVariableValue !== variableData?.selectedValue?.toString()) {
|
||||
handleChange(debouncedVariableValue);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedVariableValue]);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch options for CUSTOM Type
|
||||
if (variableData.type === 'CUSTOM') {
|
||||
@@ -240,7 +226,7 @@ function VariableItem({
|
||||
placement="top"
|
||||
title={isDashboardLocked ? 'Dashboard is locked' : ''}
|
||||
>
|
||||
<VariableContainer>
|
||||
<VariableContainer className="variable-item">
|
||||
<Typography.Text className="variable-name" ellipsis>
|
||||
${variableData.name}
|
||||
</Typography.Text>
|
||||
@@ -250,9 +236,10 @@ function VariableItem({
|
||||
placeholder="Enter value"
|
||||
disabled={isDashboardLocked}
|
||||
bordered={false}
|
||||
value={variableValue}
|
||||
key={variableData.selectedValue?.toString()}
|
||||
defaultValue={variableData.selectedValue?.toString()}
|
||||
onChange={(e): void => {
|
||||
setVaribleValue(e.target.value || '');
|
||||
debouncedHandleChange(e.target.value || '');
|
||||
}}
|
||||
style={{
|
||||
width:
|
||||
@@ -263,18 +250,25 @@ function VariableItem({
|
||||
!errorMessage &&
|
||||
optionsData && (
|
||||
<Select
|
||||
value={selectValue}
|
||||
key={
|
||||
selectValue && Array.isArray(selectValue)
|
||||
? selectValue.join(' ')
|
||||
: selectValue || variableData.id
|
||||
}
|
||||
defaultValue={selectValue}
|
||||
onChange={handleChange}
|
||||
bordered={false}
|
||||
placeholder="Select value"
|
||||
placement="bottomRight"
|
||||
mode={mode}
|
||||
dropdownMatchSelectWidth={false}
|
||||
style={SelectItemStyle}
|
||||
loading={isLoading}
|
||||
showArrow
|
||||
showSearch
|
||||
data-testid="variable-select"
|
||||
className="variable-select"
|
||||
disabled={isDashboardLocked}
|
||||
getPopupContainer={popupContainer}
|
||||
>
|
||||
{enableSelectAll && (
|
||||
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
|
||||
|
||||
@@ -7,5 +7,5 @@ export const RIBBON_STYLES = {
|
||||
export const buttonText = {
|
||||
[ROUTES.LOGS_EXPLORER]: 'Switch to Old Logs Explorer',
|
||||
[ROUTES.TRACE]: 'Try new Traces Explorer',
|
||||
[ROUTES.LOGS]: 'Switch to New Logs Explorer',
|
||||
[ROUTES.OLD_LOGS_EXPLORER]: 'Switch to New Logs Explorer',
|
||||
};
|
||||
|
||||
@@ -14,16 +14,16 @@ function NewExplorerCTA(): JSX.Element | null {
|
||||
() =>
|
||||
location.pathname === ROUTES.LOGS_EXPLORER ||
|
||||
location.pathname === ROUTES.TRACE ||
|
||||
location.pathname === ROUTES.LOGS,
|
||||
location.pathname === ROUTES.OLD_LOGS_EXPLORER,
|
||||
[location.pathname],
|
||||
);
|
||||
|
||||
const onClickHandler = useCallback((): void => {
|
||||
if (location.pathname === ROUTES.LOGS_EXPLORER) {
|
||||
history.push(ROUTES.LOGS);
|
||||
history.push(ROUTES.OLD_LOGS_EXPLORER);
|
||||
} else if (location.pathname === ROUTES.TRACE) {
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
} else if (location.pathname === ROUTES.LOGS) {
|
||||
} else if (location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
|
||||
history.push(ROUTES.LOGS_EXPLORER);
|
||||
}
|
||||
}, [location.pathname]);
|
||||
@@ -36,6 +36,7 @@ function NewExplorerCTA(): JSX.Element | null {
|
||||
danger
|
||||
data-testid="newExplorerCTA"
|
||||
type="primary"
|
||||
size="small"
|
||||
>
|
||||
{buttonText[location.pathname]}
|
||||
</Button>
|
||||
|
||||
@@ -30,11 +30,10 @@ function QueryHeader({
|
||||
const [collapse, setCollapse] = useState(false);
|
||||
return (
|
||||
<QueryWrapper>
|
||||
<Row style={{ justifyContent: 'space-between' }}>
|
||||
<Row style={{ justifyContent: 'space-between', marginBottom: '1rem' }}>
|
||||
<Row>
|
||||
<Button
|
||||
type="default"
|
||||
ghost
|
||||
icon={disabled ? <EyeInvisibleFilled /> : <EyeFilled />}
|
||||
onClick={onDisable}
|
||||
>
|
||||
@@ -42,7 +41,6 @@ function QueryHeader({
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
ghost
|
||||
icon={collapse ? <RightOutlined /> : <DownOutlined />}
|
||||
onClick={(): void => setCollapse(!collapse)}
|
||||
/>
|
||||
@@ -51,7 +49,6 @@ function QueryHeader({
|
||||
{deletable && (
|
||||
<Button
|
||||
type="default"
|
||||
ghost
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={onDelete}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Input } from 'antd';
|
||||
import MonacoEditor from 'components/Editor';
|
||||
import { LEGEND } from 'constants/global';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { ChangeEvent, useCallback } from 'react';
|
||||
import { IClickHouseQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { getFormatedLegend } from 'utils/getFormatedLegend';
|
||||
|
||||
import QueryHeader from '../QueryHeader';
|
||||
|
||||
@@ -57,7 +59,11 @@ function ClickHouseQueryBuilder({
|
||||
|
||||
const handleUpdateInput = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
const { name } = e.target;
|
||||
let { value } = e.target;
|
||||
if (name === LEGEND) {
|
||||
value = getFormatedLegend(value);
|
||||
}
|
||||
handleUpdateQuery(name as keyof IClickHouseQuery, value);
|
||||
},
|
||||
[handleUpdateQuery],
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Input } from 'antd';
|
||||
import { LEGEND } from 'constants/global';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { ChangeEvent, useCallback } from 'react';
|
||||
import { IPromQLQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { getFormatedLegend } from 'utils/getFormatedLegend';
|
||||
|
||||
import QueryHeader from '../QueryHeader';
|
||||
|
||||
@@ -28,7 +30,11 @@ function PromQLQueryBuilder({
|
||||
|
||||
const handleUpdateQuery = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
const { name } = e.target;
|
||||
let { value } = e.target;
|
||||
if (name === LEGEND) {
|
||||
value = getFormatedLegend(value);
|
||||
}
|
||||
const newQuery: IPromQLQuery = { ...queryData, [name]: value };
|
||||
|
||||
handleSetQueryItemData(queryIndex, EQueryType.PROM, newQuery);
|
||||
|
||||
@@ -14,6 +14,8 @@ function WidgetGraphContainer({
|
||||
selectedTime,
|
||||
thresholds,
|
||||
fillSpans = false,
|
||||
softMax,
|
||||
softMin,
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
const { selectedDashboard } = useDashboard();
|
||||
|
||||
@@ -59,6 +61,8 @@ function WidgetGraphContainer({
|
||||
selectedWidget={selectedWidget}
|
||||
thresholds={thresholds}
|
||||
fillSpans={fillSpans}
|
||||
softMax={softMax}
|
||||
softMin={softMin}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import getTimeString from 'lib/getTimeString';
|
||||
import history from 'lib/history';
|
||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
@@ -23,6 +28,8 @@ function WidgetGraph({
|
||||
yAxisUnit,
|
||||
thresholds,
|
||||
fillSpans,
|
||||
softMax,
|
||||
softMin,
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
|
||||
@@ -33,6 +40,7 @@ function WidgetGraph({
|
||||
|
||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(getWidgetQueryRange);
|
||||
@@ -62,14 +70,47 @@ function WidgetGraph({
|
||||
(start: number, end: number): void => {
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
|
||||
if (startTimestamp !== endTimestamp) {
|
||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||
}
|
||||
|
||||
const { maxTime, minTime } = GetMinMax('custom', [
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
]);
|
||||
|
||||
params.set(QueryParams.startTime, minTime.toString());
|
||||
params.set(QueryParams.endTime, maxTime.toString());
|
||||
const generatedUrl = `${location.pathname}?${params.toString()}`;
|
||||
history.push(generatedUrl);
|
||||
},
|
||||
[dispatch],
|
||||
[dispatch, location.pathname, params],
|
||||
);
|
||||
|
||||
const handleBackNavigation = (): void => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const startTime = searchParams.get(QueryParams.startTime);
|
||||
const endTime = searchParams.get(QueryParams.endTime);
|
||||
|
||||
if (startTime && endTime && startTime !== endTime) {
|
||||
dispatch(
|
||||
UpdateTimeInterval('custom', [
|
||||
parseInt(getTimeString(startTime), 10),
|
||||
parseInt(getTimeString(endTime), 10),
|
||||
]),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('popstate', handleBackNavigation);
|
||||
|
||||
return (): void => {
|
||||
window.removeEventListener('popstate', handleBackNavigation);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
getUPlotChartOptions({
|
||||
@@ -83,6 +124,8 @@ function WidgetGraph({
|
||||
fillSpans,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
softMax,
|
||||
softMin,
|
||||
}),
|
||||
[
|
||||
widgetId,
|
||||
@@ -95,6 +138,8 @@ function WidgetGraph({
|
||||
fillSpans,
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
softMax,
|
||||
softMin,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -125,6 +170,8 @@ interface WidgetGraphProps {
|
||||
SuccessResponse<MetricRangePayloadProps, unknown>,
|
||||
Error
|
||||
>;
|
||||
softMax: number | null;
|
||||
softMin: number | null;
|
||||
}
|
||||
|
||||
export default WidgetGraph;
|
||||
|
||||
@@ -17,6 +17,8 @@ function WidgetGraph({
|
||||
selectedTime,
|
||||
thresholds,
|
||||
fillSpans,
|
||||
softMax,
|
||||
softMin,
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { selectedDashboard } = useDashboard();
|
||||
@@ -53,6 +55,8 @@ function WidgetGraph({
|
||||
selectedGraph={selectedGraph}
|
||||
yAxisUnit={yAxisUnit}
|
||||
fillSpans={fillSpans}
|
||||
softMax={softMax}
|
||||
softMin={softMin}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,8 @@ function LeftContainer({
|
||||
selectedTime,
|
||||
thresholds,
|
||||
fillSpans,
|
||||
softMax,
|
||||
softMin,
|
||||
}: WidgetGraphProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
@@ -20,6 +22,8 @@ function LeftContainer({
|
||||
selectedGraph={selectedGraph}
|
||||
yAxisUnit={yAxisUnit}
|
||||
fillSpans={fillSpans}
|
||||
softMax={softMax}
|
||||
softMin={softMin}
|
||||
/>
|
||||
<QueryContainer>
|
||||
<QuerySection selectedTime={selectedTime} selectedGraph={selectedGraph} />
|
||||
|
||||
@@ -30,6 +30,15 @@ export const panelTypeVsThreshold: { [key in PANEL_TYPES]: boolean } = {
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||
} as const;
|
||||
|
||||
export const panelTypeVsSoftMinMax: { [key in PANEL_TYPES]: boolean } = {
|
||||
[PANEL_TYPES.TIME_SERIES]: true,
|
||||
[PANEL_TYPES.VALUE]: false,
|
||||
[PANEL_TYPES.TABLE]: false,
|
||||
[PANEL_TYPES.LIST]: false,
|
||||
[PANEL_TYPES.TRACE]: false,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: false,
|
||||
} as const;
|
||||
|
||||
export const panelTypeVsDragAndDrop: { [key in PANEL_TYPES]: boolean } = {
|
||||
[PANEL_TYPES.TIME_SERIES]: false,
|
||||
[PANEL_TYPES.VALUE]: true,
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Button,
|
||||
Divider,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
@@ -16,7 +17,7 @@ import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
||||
import { Dispatch, SetStateAction, useCallback } from 'react';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { panelTypeVsThreshold } from './constants';
|
||||
import { panelTypeVsSoftMinMax, panelTypeVsThreshold } from './constants';
|
||||
import { Container, Title } from './styles';
|
||||
import ThresholdSelector from './Threshold/ThresholdSelector';
|
||||
import { ThresholdProps } from './Threshold/types';
|
||||
@@ -42,6 +43,10 @@ function RightContainer({
|
||||
selectedWidget,
|
||||
isFillSpans,
|
||||
setIsFillSpans,
|
||||
softMax,
|
||||
softMin,
|
||||
setSoftMax,
|
||||
setSoftMin,
|
||||
}: RightContainerProps): JSX.Element {
|
||||
const onChangeHandler = useCallback(
|
||||
(setFunc: Dispatch<SetStateAction<string>>, value: string) => {
|
||||
@@ -56,6 +61,21 @@ function RightContainer({
|
||||
const onCreateAlertsHandler = useCreateAlerts(selectedWidget);
|
||||
|
||||
const allowThreshold = panelTypeVsThreshold[selectedGraph];
|
||||
const allowSoftMinMax = panelTypeVsSoftMinMax[selectedGraph];
|
||||
|
||||
const softMinHandler = useCallback(
|
||||
(value: number | null) => {
|
||||
setSoftMin(value);
|
||||
},
|
||||
[setSoftMin],
|
||||
);
|
||||
|
||||
const softMaxHandler = useCallback(
|
||||
(value: number | null) => {
|
||||
setSoftMax(value);
|
||||
},
|
||||
[setSoftMax],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@@ -129,6 +149,30 @@ function RightContainer({
|
||||
)}
|
||||
</Space>
|
||||
|
||||
{allowSoftMinMax && (
|
||||
<>
|
||||
<Divider />
|
||||
<Typography.Text style={{ display: 'block', margin: '5px 0' }}>
|
||||
Soft Min
|
||||
</Typography.Text>
|
||||
<InputNumber
|
||||
type="number"
|
||||
value={softMin}
|
||||
style={{ display: 'block', width: '100%' }}
|
||||
onChange={softMinHandler}
|
||||
/>
|
||||
<Typography.Text style={{ display: 'block', margin: '5px 0' }}>
|
||||
Soft Max
|
||||
</Typography.Text>
|
||||
<InputNumber
|
||||
value={softMax}
|
||||
type="number"
|
||||
style={{ display: 'block', width: '100%' }}
|
||||
onChange={softMaxHandler}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{allowThreshold && (
|
||||
<>
|
||||
<Divider />
|
||||
@@ -166,6 +210,10 @@ interface RightContainerProps {
|
||||
selectedWidget?: Widgets;
|
||||
isFillSpans: boolean;
|
||||
setIsFillSpans: Dispatch<SetStateAction<boolean>>;
|
||||
softMin: number | null;
|
||||
softMax: number | null;
|
||||
setSoftMin: Dispatch<SetStateAction<number | null>>;
|
||||
setSoftMax: Dispatch<SetStateAction<number | null>>;
|
||||
}
|
||||
|
||||
RightContainer.defaultProps = {
|
||||
|
||||
@@ -104,6 +104,18 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
const [saveModal, setSaveModal] = useState(false);
|
||||
const [discardModal, setDiscardModal] = useState(false);
|
||||
|
||||
const [softMin, setSoftMin] = useState<number | null>(
|
||||
selectedWidget?.softMin === null || selectedWidget?.softMin === undefined
|
||||
? null
|
||||
: selectedWidget?.softMin || 0,
|
||||
);
|
||||
|
||||
const [softMax, setSoftMax] = useState<number | null>(
|
||||
selectedWidget?.softMax === null || selectedWidget?.softMax === undefined
|
||||
? null
|
||||
: selectedWidget?.softMax || 0,
|
||||
);
|
||||
|
||||
const closeModal = (): void => {
|
||||
setSaveModal(false);
|
||||
setDiscardModal(false);
|
||||
@@ -178,6 +190,8 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
yAxisUnit,
|
||||
panelTypes: graphType,
|
||||
thresholds,
|
||||
softMin,
|
||||
softMax,
|
||||
fillSpans: isFillSpans,
|
||||
},
|
||||
...afterWidgets,
|
||||
@@ -213,6 +227,8 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
yAxisUnit,
|
||||
graphType,
|
||||
thresholds,
|
||||
softMin,
|
||||
softMax,
|
||||
isFillSpans,
|
||||
afterWidgets,
|
||||
updateDashboardMutation,
|
||||
@@ -291,7 +307,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
disabled={isSaveDisabled}
|
||||
onClick={onSaveDashboard}
|
||||
>
|
||||
Save
|
||||
Save Changes
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -300,13 +316,14 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
<Button
|
||||
type="primary"
|
||||
data-testid="new-widget-save"
|
||||
loading={updateDashboardMutation.isLoading}
|
||||
disabled={isSaveDisabled}
|
||||
onClick={onSaveDashboard}
|
||||
>
|
||||
Save
|
||||
Save Changes
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={onClickDiscardHandler}>Discard</Button>
|
||||
<Button onClick={onClickDiscardHandler}>Discard Changes</Button>
|
||||
</ButtonContainer>
|
||||
|
||||
<PanelContainer>
|
||||
@@ -317,6 +334,8 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
yAxisUnit={yAxisUnit}
|
||||
thresholds={thresholds}
|
||||
fillSpans={isFillSpans}
|
||||
softMax={softMax}
|
||||
softMin={softMin}
|
||||
/>
|
||||
</LeftContainerWrapper>
|
||||
|
||||
@@ -343,6 +362,10 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
selectedWidget={selectedWidget}
|
||||
isFillSpans={isFillSpans}
|
||||
setIsFillSpans={setIsFillSpans}
|
||||
softMin={softMin}
|
||||
setSoftMin={setSoftMin}
|
||||
softMax={softMax}
|
||||
setSoftMax={setSoftMax}
|
||||
/>
|
||||
</RightContainerWrapper>
|
||||
</PanelContainer>
|
||||
@@ -363,6 +386,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
|
||||
closable
|
||||
onCancel={closeModal}
|
||||
onOk={onClickSaveHandler}
|
||||
confirmLoading={updateDashboardMutation.isLoading}
|
||||
centered
|
||||
open={saveModal}
|
||||
width={600}
|
||||
|
||||
@@ -25,7 +25,7 @@ export const LeftContainerWrapper = styled(Col)`
|
||||
|
||||
export const ButtonContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
gap: 8px;
|
||||
margin-bottom: 1rem;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
@@ -13,4 +13,6 @@ export interface NewWidgetProps {
|
||||
export interface WidgetGraphProps extends NewWidgetProps {
|
||||
selectedTime: timePreferance;
|
||||
thresholds: ThresholdProps[];
|
||||
softMin: number | null;
|
||||
softMax: number | null;
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@
|
||||
...
|
||||
filelog/app:
|
||||
include: [ /tmp/app.log ]
|
||||
start_at: beginning
|
||||
start_at: end
|
||||
...
|
||||
```
|
||||
|
||||
`start_at: beginning` can be removed once you are done testing.
|
||||
Replace `/tmp/app.log` with the path to your log file.
|
||||
|
||||
Note: change the `start_at` value to `beginning` if you want to read the log file from the beginning. It may be useful if you want to send old logs to SigNoz. The log records older than the standard log retention period (default 15 days) will be discarded.
|
||||
|
||||
For parsing logs of different formats you will have to use operators, you can read more about operators [here](https://signoz.io/docs/userguide/logs/#operators-for-parsing-and-manipulating-logs).
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@ receivers:
|
||||
...
|
||||
filelog/app:
|
||||
include: [ /tmp/app.log ]
|
||||
start_at: beginning
|
||||
start_at: end
|
||||
...
|
||||
```
|
||||
Replace `/tmp/app.log` with the path to your log file.
|
||||
|
||||
Note: change the `start_at` value to `beginning` if you want to read the log file from the beginning. It may be useful if you want to send old logs to SigNoz. The log records older than the standard log retention period (default 15 days) will be discarded.
|
||||
|
||||
For more configurations that are available for syslog receiver please check [here](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/filelogreceiver).
|
||||
|
||||
### Step 2: Include filelog receiver in the Pipeline
|
||||
|
||||
@@ -7,11 +7,13 @@ receivers:
|
||||
...
|
||||
filelog/app:
|
||||
include: [ /tmp/app.log ]
|
||||
start_at: beginning
|
||||
start_at: end
|
||||
...
|
||||
```
|
||||
Replace `/tmp/app.log` with the path to your log file.
|
||||
|
||||
Note: change the `start_at` value to `beginning` if you want to read the log file from the beginning. It may be useful if you want to send old logs to SigNoz. The log records older than the standard log retention period (default 15 days) will be discarded.
|
||||
|
||||
For more configurations that are available for syslog receiver please check [here](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/filelogreceiver).
|
||||
|
||||
### Step 2: Include filelog receiver in the Pipeline
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user