Merge pull request #4400 from SigNoz/release/v0.37.0

Release/v0.37.0
This commit is contained in:
Prashant Shahi 2024-01-20 02:03:04 +05:30 committed by GitHub
commit 9755ba6b47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
171 changed files with 5362 additions and 1241 deletions

2
.github/CODEOWNERS vendored
View File

@ -2,7 +2,7 @@
# Owners are automatically requested for review for PRs that changes code
# that they own.
/frontend/ @palashgdev @YounixM
/frontend/ @YounixM
/frontend/src/container/MetricsApplication @srikanthccv
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv
/deploy/ @prashant-shahi

View File

@ -11,7 +11,6 @@
<img alt="tweet" src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social"> </a>
</p>
<h3 align="center">
<a href="https://signoz.io/docs"><b>Dokumentation</b></a> &bull;
<a href="https://github.com/SigNoz/signoz/blob/develop/README.md"><b>Readme auf Englisch </b></a> &bull;
@ -40,12 +39,13 @@ SigNoz hilft Entwicklern, Anwendungen zu überwachen und Probleme in ihren berei
👉 Einfache Einrichtung von Benachrichtigungen mit dem selbst erstellbaren Abfrage-Builder.
##
### Anwendung Metriken
![application_metrics](https://user-images.githubusercontent.com/83692067/226637410-900dbc5e-6705-4b11-a10c-bd0faeb2a92f.png)
### Verteiltes Tracing
<img width="2068" alt="distributed_tracing_2 2" src="https://user-images.githubusercontent.com/83692067/226536447-bae58321-6a22-4ed3-af80-e3e964cb3489.png">
<img width="2068" alt="distributed_tracing_1" src="https://user-images.githubusercontent.com/83692067/226536462-939745b6-4f9d-45a6-8016-814837e7f7b4.png">
@ -62,22 +62,18 @@ SigNoz hilft Entwicklern, Anwendungen zu überwachen und Probleme in ihren berei
![exceptions_light](https://user-images.githubusercontent.com/83692067/226637967-4188d024-3ac9-4799-be95-f5ea9c45436f.png)
### Alarme
<img width="2068" alt="alerts_management" src="https://user-images.githubusercontent.com/83692067/226536548-2c81e2e8-c12d-47e8-bad7-c6be79055def.png">
<br /><br />
## Werde Teil unserer Slack Community
Sag Hi zu uns auf [Slack](https://signoz.io/slack) 👋
<br /><br />
## Funktionen:
- Einheitliche Benutzeroberfläche für Metriken, Traces und Logs. Keine Notwendigkeit, zwischen Prometheus und Jaeger zu wechseln, um Probleme zu debuggen oder ein separates Log-Tool wie Elastic neben Ihrer Metriken- und Traces-Stack zu verwenden.
@ -93,7 +89,6 @@ Sag Hi zu uns auf [Slack](https://signoz.io/slack) 👋
<br /><br />
## Wieso SigNoz?
Als Entwickler fanden wir es anstrengend, uns für jede kleine Funktion, die wir haben wollten, auf Closed Source SaaS Anbieter verlassen zu müssen. Closed Source Anbieter überraschen ihre Kunden zum Monatsende oft mit hohen Rechnungen, die keine Transparenz bzgl. der Kostenaufteilung bieten.
@ -116,12 +111,10 @@ Wir unterstützen [OpenTelemetry](https://opentelemetry.io) als Bibliothek, mit
- Elixir
- Rust
Hier findest du die vollständige Liste von unterstützten Programmiersprachen - https://opentelemetry.io/docs/
<br /><br />
## Erste Schritte mit SigNoz
### Bereitstellung mit Docker
@ -138,7 +131,6 @@ Bitte folge den [hier](https://signoz.io/docs/deployment/helm_chart) aufgelistet
<br /><br />
## Vergleiche mit bekannten Tools
### SigNoz vs Prometheus
@ -179,7 +171,6 @@ Wir haben Benchmarks veröffentlicht, die Loki mit SigNoz vergleichen. Schauen S
<br /><br />
## Zum Projekt beitragen
Wir ❤️ Beiträge zum Projekt, egal ob große oder kleine. Bitte lies dir zuerst die [CONTRIBUTING.md](CONTRIBUTING.md), durch, bevor du anfängst, Beiträge zu SigNoz zu machen.
@ -197,6 +188,8 @@ Du bist dir nicht sicher, wie du anfangen sollst? Schreib uns einfach auf dem #c
#### Frontend
- [Palash Gupta](https://github.com/palashgdev)
- [Yunus M](https://github.com/YounixM)
- [Rajat Dabade](https://github.com/Rajat-Dabade)
#### DevOps
@ -204,16 +197,12 @@ Du bist dir nicht sicher, wie du anfangen sollst? Schreib uns einfach auf dem #c
<br /><br />
## Dokumentation
Du findest unsere Dokumentation unter https://signoz.io/docs/. Falls etwas unverständlich ist oder fehlt, öffne gerne ein Github Issue mit dem Label `documentation` oder schreib uns über den Community Slack Channel.
<br /><br />
## Gemeinschaft
Werde Teil der [slack community](https://signoz.io/slack) um mehr über verteilte Einzelschritt-Fehlersuche, Messung von Systemzuständen oder SigNoz zu erfahren und sich mit anderen Nutzern und Mitwirkenden in Verbindung zu setzen.

View File

@ -19,7 +19,7 @@
<a href="https://twitter.com/SigNozHq"><b>Twitter</b></a>
</h3>
##
##
SigNoz 帮助开发人员监控应用并排查已部署应用的问题。你可以使用 SigNoz 实现如下能力:
@ -67,7 +67,7 @@ SigNoz 帮助开发人员监控应用并排查已部署应用的问题。你可
## 加入我们 Slack 社区
来 [Slack](https://signoz.io/slack) 和我们打招呼吧 👋
来 [Slack](https://signoz.io/slack) 和我们打招呼吧 👋
<br /><br />
@ -83,7 +83,7 @@ SigNoz 帮助开发人员监控应用并排查已部署应用的问题。你可
- 通过 服务名、操作方式、延迟、错误、标签/注释 过滤 traces 数据
- 通过聚合 trace 数据而获得业务相关的 metrics。 比如你可以通过 `customer_type: gold` 或者 `deployment_version: v2` 或者 `external_call: paypal` 获取错误率和 P99 延迟数据
- 通过聚合 trace 数据而获得业务相关的 metrics。 比如你可以通过 `customer_type: gold` 或者 `deployment_version: v2` 或者 `external_call: paypal` 获取错误率和 P99 延迟数据
- 原生支持 OpenTelemetry 日志,高级日志查询,自动收集 k8s 相关日志
@ -101,7 +101,7 @@ SigNoz 帮助开发人员监控应用并排查已部署应用的问题。你可
我们想做一个自托管并且可开源的工具,像 DataDog 和 NewRelic 那样, 为那些担心数据隐私和安全的公司提供第三方服务。
作为开源的项目,你完全可以自己掌控你的配置、样本和更新。你同样可以基于 SigNoz 拓展特定的业务模块。
作为开源的项目,你完全可以自己掌控你的配置、样本和更新。你同样可以基于 SigNoz 拓展特定的业务模块。
### 支持的编程语言:
@ -153,9 +153,9 @@ Jaeger 仅仅是一个分布式追踪系统。 但是 SigNoz 可以提供 metric
而且, SigNoz 相较于 Jaeger 拥有更对的高级功能:
- Jaegar UI 不能提供任何基于 traces 的 metrics 查询和过滤。
- Jaegar UI 不能提供任何基于 traces 的 metrics 查询和过滤。
- Jaeger 不能针对过滤的 traces 做聚合。 比如, p99 延迟的请求有个标签是 customer_type='premium'。 而这些在 SigNoz 可以轻松做到。
- Jaeger 不能针对过滤的 traces 做聚合。 比如, p99 延迟的请求有个标签是 customer_type='premium'。 而这些在 SigNoz 可以轻松做到。
<p>&nbsp </p>
@ -185,7 +185,7 @@ Jaeger 仅仅是一个分布式追踪系统。 但是 SigNoz 可以提供 metric
我们 ❤️ 你的贡献,无论大小。 请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 再开始给 SigNoz 做贡献。
如果你不知道如何开始? 只需要在 [slack 社区](https://signoz.io/slack) 通过 `#contributing` 频道联系我们。
如果你不知道如何开始? 只需要在 [slack 社区](https://signoz.io/slack) 通过 `#contributing` 频道联系我们。
### 项目维护人员
@ -199,6 +199,8 @@ Jaeger 仅仅是一个分布式追踪系统。 但是 SigNoz 可以提供 metric
#### 前端
- [Palash Gupta](https://github.com/palashgdev)
- [Yunus M](https://github.com/YounixM)
- [Rajat Dabade](https://github.com/Rajat-Dabade)
#### 运维开发

View File

@ -146,11 +146,11 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.36.2
image: signoz/query-service:0.37.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.37.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.8
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.8
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:

View File

@ -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]

View File

@ -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]

View File

@ -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.8}
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.8
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

View File

@ -25,7 +25,7 @@ services:
command:
[
"-config=/root/config/prometheus.yml",
"--prefer-delta=true"
# "--prefer-delta=true"
]
ports:
- "6060:6060"

View File

@ -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.37.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.37.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.8}
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.8}
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

View File

@ -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]

View File

@ -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]

View File

@ -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{"*"},

View File

@ -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",

View 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

View File

@ -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",

View File

@ -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'),
);

View File

@ -23,6 +23,7 @@ import {
LogsIndexToFields,
MySettings,
NewDashboardPage,
OldLogsExplorer,
Onboarding,
OrganizationSettings,
PasswordReset,
@ -246,6 +247,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,

View File

@ -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}`);

View File

@ -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);
};

View File

@ -0,0 +1,90 @@
.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;
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);
}
}
}

View File

@ -0,0 +1,240 @@
/* 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="timeSelection-input-container"
placement="bottomRight"
getPopupContainer={popupContainer}
content={content}
arrow={false}
open={open}
onOpenChange={handleOpenChange}
trigger={['click']}
style={{
padding: 0,
}}
>
<Input
className="timeSelection-input"
type="text"
style={{
minWidth: '120px',
width: '100%',
}}
status={inputValue && inputStatus === 'error' ? 'error' : ''}
allowClear={!isInputFocused && selectedTime === 'custom'}
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;

View File

@ -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'};

View File

@ -43,7 +43,7 @@ function DynamicColumnTable({
: undefined,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [columns]);
}, [columns, dynamicColumns]);
const onToggleHandler = (index: number) => (
checked: boolean,

View File

@ -13,6 +13,7 @@ export const Container = styled.div`
&&& {
display: flex;
justify-content: center;
gap: 16px;
align-items: center;
min-height: 100vh;

View File

@ -1,2 +1,4 @@
const MAX_RPS_LIMIT = 100;
export { MAX_RPS_LIMIT };
export const LEGEND = 'legend';

View File

@ -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',

View File

@ -1,5 +1,6 @@
const themeColors = {
chartcolors: {
robin: '#3F5ECC',
dodgerBlue: '#2F80ED',
mediumOrchid: '#BB6BD9',
seaBuckthorn: '#F2994A',

View File

@ -15,6 +15,7 @@ export const ButtonContainer = styled.div`
align-items: center;
margin-top: 1rem;
margin-bottom: 1rem;
padding-right: 1rem;
}
`;

View File

@ -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(

View 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;
}
}

View File

@ -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>
);
}

View File

@ -13,6 +13,7 @@ export const Layout = styled(LayoutComponent)`
export const LayoutContent = styled(LayoutComponent.Content)`
overflow-y: auto;
height: 100%;
`;
export const ChildrenContainer = styled.div`

View File

@ -150,6 +150,8 @@ function ChartPreview({
thresholdUnit: alertDef?.condition.targetUnit,
},
],
softMax: null,
softMin: null,
}),
[
yAxisUnit,

View File

@ -0,0 +1,37 @@
.full-view-header-container {
display: flex;
justify-content: flex-end;
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;
}
}
}

View File

@ -0,0 +1,28 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './FullViewHeader.styles.scss';
import history from 'lib/history';
export default function FullViewHeader({
overrideRoute,
}: {
overrideRoute?: string;
}): React.ReactElement {
const handleLogoClick = (): void => {
history.push(overrideRoute || '/');
};
return (
<div className="full-view-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>
);
}
FullViewHeader.defaultProps = {
overrideRoute: '/',
};

View File

@ -132,6 +132,8 @@ function FullView({
thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
softMax: widget.softMax === undefined ? null : widget.softMax,
softMin: widget.softMin === undefined ? null : widget.softMin,
});
setChartOptions(newChartOptions);

View File

@ -46,6 +46,7 @@ function WidgetGraphComponent({
data,
options,
onDragSelect,
graphVisibility,
}: WidgetGraphComponentProps): JSX.Element {
const [deleteModal, setDeleteModal] = useState(false);
const [hovered, setHovered] = useState(false);
@ -83,6 +84,10 @@ function WidgetGraphComponent({
setGraphsVisibilityStates(localStoredVisibilityStates);
}, [localStoredVisibilityStates]);
graphVisibility?.forEach((state, index) => {
lineChartRef.current?.toggleGraph(index, state);
});
const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard();
const featureResponse = useSelector<AppState, AppReducer['featureResponse']>(

View File

@ -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,17 @@ function GridCardGraph({
? headerMenuList.filter((menu) => menu !== MenuItemKeys.CreateAlerts)
: headerMenuList;
const [graphVisibility, setGraphVisibility] = useState<boolean[]>(
Array(queryResponse.data?.payload?.data.result.length || 0).fill(true),
);
useEffect(() => {
setGraphVisibility([
true,
...Array(queryResponse.data?.payload?.data.result.length).fill(true),
]);
}, [queryResponse.data?.payload?.data.result.length]);
const options = useMemo(
() =>
getUPlotChartOptions({
@ -135,11 +187,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 +205,8 @@ function GridCardGraph({
onClickHandler,
minTimeScale,
maxTimeScale,
graphVisibility,
setGraphVisibility,
],
);
@ -167,6 +227,7 @@ function GridCardGraph({
threshold={threshold}
headerMenuList={menuList}
onClickHandler={onClickHandler}
graphVisibility={graphVisibility}
/>
)}
</div>

View File

@ -28,6 +28,7 @@ export interface WidgetGraphComponentProps extends UplotProps {
threshold?: ReactNode;
headerMenuList: MenuItemKeys[];
isWarning: boolean;
graphVisibility?: boolean[];
}
export interface GridCardGraphProps {

View File

@ -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(layouts);
const updateDashboardMutation = useUpdateDashboard();
const { notifications } = useNotifications();
@ -78,7 +84,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 +96,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 +111,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 +149,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 +171,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);

View File

@ -8,6 +8,8 @@
box-sizing: border-box;
font-size: 14px;
font-weight: 600;
cursor: move;
}
.widget-header-title {

View 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[];

View File

@ -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>

View File

@ -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;
`;

View File

@ -0,0 +1,20 @@
import { Dashboard } from 'types/api/dashboard/getAll';
export const filterDashboard = (
searchValue: string,
dashboardList: Dashboard[],
): any[] => {
// Convert the searchValue to lowercase for case-insensitive search
const searchValueLowerCase = searchValue.toLowerCase();
// Use the filter method to find matching objects
return dashboardList.filter((item: Dashboard) => {
// Convert each property value to lowercase for case-insensitive search
const itemValues = Object.values(item?.data).map((value) =>
value.toString().toLowerCase(),
);
// Check if any property value contains the searchValue
return itemValues.some((value) => value.includes(searchValueLowerCase));
});
};

View File

@ -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``}

View File

@ -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 />

View File

@ -3,7 +3,7 @@ import styled from 'styled-components';
export const Container = styled(Row)`
&&& {
margin-top: 2rem;
margin-top: 1rem;
min-height: 8vh;
}
`;

View File

@ -63,7 +63,7 @@ function LogDetailedView({
queryString,
);
history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`);
history.replace(`${ROUTES.OLD_LOGS_EXPLORER}?q=${updatedQueryString}`);
},
[history, queryString],
);

View File

@ -74,6 +74,7 @@ function LogsTopNav(): JSX.Element {
icon={<PlayCircleFilled />}
onClick={handleGoLive}
type="primary"
size="small"
>
Go Live
</LiveButtonStyled>

View File

@ -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``}

View File

@ -20,4 +20,6 @@ export const getWidgetQueryBuilder = ({
timePreferance: 'GLOBAL_TIME',
title,
yAxisUnit,
softMax: null,
softMin: null,
});

View File

@ -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 => {

View File

@ -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) {

View File

@ -0,0 +1,5 @@
.flexBtn {
display: flex;
align-items: center;
gap: 8px;
}

View File

@ -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>
);
}

View File

@ -0,0 +1,7 @@
.userInfo-label {
min-width: 150px;
}
.userInfo-value {
min-width: 20rem;
}

View File

@ -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;

View File

@ -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>
);
}

View File

@ -63,6 +63,8 @@ function DashboardGraphSlider(): JSX.Element {
panelTypes: name,
query: initialQueriesMap.metrics,
timePreferance: 'GLOBAL_TIME',
softMax: null,
softMin: null,
},
],
},

View File

@ -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',
};

View File

@ -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>

View File

@ -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}

View File

@ -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],

View File

@ -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);

View File

@ -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}
/>
);
}

View File

@ -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;

View File

@ -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>
);

View File

@ -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} />

View File

@ -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,

View File

@ -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 = {

View File

@ -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,
@ -317,6 +333,8 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
yAxisUnit={yAxisUnit}
thresholds={thresholds}
fillSpans={isFillSpans}
softMax={softMax}
softMin={softMin}
/>
</LeftContainerWrapper>
@ -343,6 +361,10 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
selectedWidget={selectedWidget}
isFillSpans={isFillSpans}
setIsFillSpans={setIsFillSpans}
softMin={softMin}
setSoftMin={setSoftMin}
softMax={softMax}
setSoftMax={setSoftMax}
/>
</RightContainerWrapper>
</PanelContainer>

View File

@ -13,4 +13,6 @@ export interface NewWidgetProps {
export interface WidgetGraphProps extends NewWidgetProps {
selectedTime: timePreferance;
thresholds: ThresholdProps[];
softMin: number | null;
softMax: number | null;
}

View File

@ -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).

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -32,12 +32,12 @@
.onboardingHeader {
text-align: center;
margin-top: 48px;
margin-bottom: 24px;
}
.onboardingHeader h1 {
font-size: 24px;
font-weight: 500;
margin: 0;
}
.modulesContainer {

View File

@ -6,6 +6,7 @@ import { ArrowRightOutlined } from '@ant-design/icons';
import { Button, Card, Typography } from 'antd';
import getIngestionData from 'api/settings/getIngestionData';
import cx from 'classnames';
import FullViewHeader from 'container/FullViewHeader/FullViewHeader';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useEffect, useState } from 'react';
@ -92,7 +93,7 @@ export default function Onboarding(): JSX.Element {
} = useOnboardingContext();
useEffectOnce(() => {
trackEvent('Onboarding Started');
trackEvent('Onboarding V2 Started');
});
const { status, data: ingestionData } = useQuery({
@ -179,20 +180,12 @@ export default function Onboarding(): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedModule, selectedDataSource, selectedEnvironment, selectedMethod]);
useEffect(() => {
// on select
trackEvent('Onboarding: Module Selected', {
selectedModule: selectedModule.id,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedModule]);
const handleNext = (): void => {
if (activeStep <= 3) {
const nextStep = activeStep + 1;
// on next
trackEvent('Onboarding: Get Started', {
trackEvent('Onboarding V2: Get Started', {
selectedModule: selectedModule.id,
nextStepId: nextStep,
});
@ -218,11 +211,10 @@ export default function Onboarding(): JSX.Element {
<div className={cx('container', isDarkMode ? 'darkMode' : 'lightMode')}>
{activeStep === 1 && (
<>
<FullViewHeader />
<div className="onboardingHeader">
<h1>Get Started with SigNoz</h1>
<div> Select a use-case to get started </div>
<h1> Select a use-case to get started</h1>
</div>
<div className="modulesContainer">
<div className="moduleContainerRowStyles">
{Object.keys(ModulesMap).map((module) => {
@ -261,7 +253,6 @@ export default function Onboarding(): JSX.Element {
})}
</div>
</div>
<div className="continue-to-next-step">
<Button type="primary" icon={<ArrowRightOutlined />} onClick={handleNext}>
Get Started

View File

@ -30,6 +30,9 @@ export default function ConnectionStatus(): JSX.Element {
const {
serviceName,
selectedDataSource,
selectedEnvironment,
activeStep,
selectedMethod,
selectedFramework,
} = useOnboardingContext();
const { queries } = useResourceAttribute();
@ -40,7 +43,7 @@ export default function ConnectionStatus(): JSX.Element {
const { trackEvent } = useAnalytics();
const [retryCount, setRetryCount] = useState(20); // Retry for 5 mins
const [retryCount, setRetryCount] = useState(20); // Retry for 3 mins 20s
const [loading, setLoading] = useState(true);
const [isReceivingData, setIsReceivingData] = useState(false);
const dispatch = useDispatch();
@ -122,7 +125,12 @@ export default function ConnectionStatus(): JSX.Element {
if (data || isError) {
setRetryCount(retryCount - 1);
if (retryCount < 0) {
trackEvent('❌ Onboarding: APM: Connection Status', {
trackEvent('Onboarding V2: Connection Status', {
dataSource: selectedDataSource?.id,
framework: selectedFramework,
environment: selectedEnvironment,
selectedMethod,
module: activeStep?.module?.id,
serviceName,
status: 'Failed',
});
@ -136,7 +144,12 @@ export default function ConnectionStatus(): JSX.Element {
setLoading(false);
setIsReceivingData(true);
trackEvent('✅ Onboarding: APM: Connection Status', {
trackEvent('Onboarding V2: Connection Status', {
dataSource: selectedDataSource?.id,
framework: selectedFramework,
environment: selectedEnvironment,
selectedMethod,
module: activeStep?.module?.id,
serviceName,
status: 'Successful',
});

View File

@ -11,7 +11,6 @@ import {
getSupportedFrameworks,
hasFrameworks,
} from 'container/OnboardingContainer/utils/dataSourceUtils';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useEffect, useState } from 'react';
import { popupContainer } from 'utils/selectPopupContainer';
@ -25,10 +24,7 @@ export interface DataSourceType {
export default function DataSource(): JSX.Element {
const [form] = Form.useForm();
const { trackEvent } = useAnalytics();
const {
activeStep,
serviceName,
selectedModule,
selectedDataSource,
@ -56,39 +52,6 @@ export default function DataSource(): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
// on language select
trackEvent('Onboarding: Data Source Selected', {
dataSource: selectedDataSource,
module: {
name: activeStep?.module?.title,
id: activeStep?.module?.id,
},
step: {
name: activeStep?.step?.title,
id: activeStep?.step?.id,
},
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedDataSource]);
useEffect(() => {
// on framework select
trackEvent('Onboarding: Framework Selected', {
dataSource: selectedDataSource,
framework: selectedFramework,
module: {
name: activeStep?.module?.title,
id: activeStep?.module?.id,
},
step: {
name: activeStep?.step?.title,
id: activeStep?.step?.id,
},
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedFramework]);
useEffect(() => {
if (selectedModule && selectedDataSource) {
const frameworks = hasFrameworks({
@ -125,8 +88,9 @@ export default function DataSource(): JSX.Element {
)}
key={dataSource.name}
onClick={(): void => {
updateSelectedFramework('');
updateSelectedFramework(null);
updateSelectedDataSource(dataSource);
form.setFieldsValue({ selectFramework: null });
}}
>
<div>
@ -152,6 +116,7 @@ export default function DataSource(): JSX.Element {
<Form
initialValues={{
serviceName,
selectFramework: selectedFramework,
}}
form={form}
onValuesChange={(): void => {
@ -165,7 +130,6 @@ export default function DataSource(): JSX.Element {
validateTrigger="onBlur"
>
<Form.Item
hasFeedback
name="serviceName"
label="Service Name"
rules={[{ required: true, message: 'Please enter service name' }]}
@ -178,13 +142,11 @@ export default function DataSource(): JSX.Element {
<div className="framework-selector">
<Form.Item
label="Select Framework"
name="select-framework"
hasFeedback
name="selectFramework"
rules={[{ required: true, message: 'Please select framework' }]}
validateTrigger=""
>
<Select
defaultValue={selectedFramework}
value={selectedFramework}
getPopupContainer={popupContainer}
style={{ minWidth: 120 }}
placeholder="Select Framework"

View File

@ -2,9 +2,7 @@ import { Card, Typography } from 'antd';
import cx from 'classnames';
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
import { useCases } from 'container/OnboardingContainer/OnboardingContainer';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { Server } from 'lucide-react';
import { useEffect } from 'react';
interface SupportedEnvironmentsProps {
name: string;
@ -36,9 +34,6 @@ const supportedEnvironments: SupportedEnvironmentsProps[] = [
export default function EnvironmentDetails(): JSX.Element {
const {
activeStep,
selectedDataSource,
selectedFramework,
selectedEnvironment,
updateSelectedEnvironment,
selectedModule,
@ -46,26 +41,6 @@ export default function EnvironmentDetails(): JSX.Element {
updateErrorDetails,
} = useOnboardingContext();
const { trackEvent } = useAnalytics();
useEffect(() => {
// on language select
trackEvent('Onboarding: Environment Selected', {
dataSource: selectedDataSource,
framework: selectedFramework,
environment: selectedEnvironment,
module: {
name: activeStep?.module?.title,
id: activeStep?.module?.id,
},
step: {
name: activeStep?.step?.title,
id: activeStep?.step?.id,
},
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedEnvironment]);
return (
<>
<Typography.Text className="environment-title">

View File

@ -26,7 +26,11 @@ const enum ApplicationLogsType {
export default function LogsConnectionStatus(): JSX.Element {
const [loading, setLoading] = useState(true);
const { selectedDataSource } = useOnboardingContext();
const {
selectedDataSource,
activeStep,
selectedEnvironment,
} = useOnboardingContext();
const { trackEvent } = useAnalytics();
const [isReceivingData, setIsReceivingData] = useState(false);
const [pollingInterval, setPollingInterval] = useState<number | false>(15000); // initial Polling interval of 15 secs , Set to false after 5 mins
@ -94,7 +98,10 @@ export default function LogsConnectionStatus(): JSX.Element {
setRetryCount(retryCount - 1);
if (retryCount < 0) {
trackEvent('❌ Onboarding: Logs Management: Connection Status', {
trackEvent('Onboarding V2: Connection Status', {
dataSource: selectedDataSource?.id,
environment: selectedEnvironment,
module: activeStep?.module?.id,
status: 'Failed',
});
@ -127,7 +134,10 @@ export default function LogsConnectionStatus(): JSX.Element {
setRetryCount(-1);
setPollingInterval(false);
trackEvent('✅ Onboarding: Logs Management: Connection Status', {
trackEvent('Onboarding V2: Connection Status', {
dataSource: selectedDataSource?.id,
environment: selectedEnvironment,
module: activeStep?.module?.id,
status: 'Successful',
});

View File

@ -8,7 +8,6 @@ import {
useOnboardingContext,
} from 'container/OnboardingContainer/context/OnboardingContext';
import { ModulesMap } from 'container/OnboardingContainer/OnboardingContainer';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useEffect, useState } from 'react';
export interface IngestionInfoProps {
@ -28,8 +27,6 @@ export default function MarkdownStep(): JSX.Element {
selectedMethod,
} = useOnboardingContext();
const { trackEvent } = useAnalytics();
const [markdownContent, setMarkdownContent] = useState('');
const { step } = activeStep;
@ -86,26 +83,6 @@ export default function MarkdownStep(): JSX.Element {
REGION: ingestionData?.REGION || 'region',
};
useEffect(() => {
trackEvent(
`Onboarding: ${activeStep?.module?.id}: ${selectedDataSource?.name}: ${activeStep?.step?.title}`,
{
dataSource: selectedDataSource,
framework: selectedFramework,
environment: selectedEnvironment,
module: {
name: activeStep?.module?.title,
id: activeStep?.module?.id,
},
step: {
name: activeStep?.step?.title,
id: activeStep?.step?.id,
},
},
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [step]);
return (
<div className="markdown-container">
<MarkdownRenderer markdownContent={markdownContent} variables={variables} />

View File

@ -3,45 +3,17 @@ import {
OnboardingMethods,
useOnboardingContext,
} from 'container/OnboardingContainer/context/OnboardingContext';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useEffect, useState } from 'react';
import { useState } from 'react';
export default function SelectMethod(): JSX.Element {
const {
activeStep,
selectedDataSource,
selectedFramework,
selectedEnvironment,
selectedMethod,
updateSelectedMethod,
} = useOnboardingContext();
const { selectedMethod, updateSelectedMethod } = useOnboardingContext();
const [value, setValue] = useState(selectedMethod);
const { trackEvent } = useAnalytics();
const onChange = (e: RadioChangeEvent): void => {
setValue(e.target.value);
updateSelectedMethod(e.target.value);
};
useEffect(() => {
// on language select
trackEvent('Onboarding: Environment Selected', {
dataSource: selectedDataSource,
framework: selectedFramework,
environment: selectedEnvironment,
module: {
name: activeStep?.module?.title,
id: activeStep?.module?.id,
},
step: {
name: activeStep?.step?.title,
id: activeStep?.step?.id,
},
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedMethod]);
return (
<div>
<Radio.Group onChange={onChange} value={value}>

View File

@ -39,6 +39,36 @@
.steps-container {
width: 20%;
height: 100%;
.steps-container-header {
display: flex;
align-items: center;
padding: 16px 0;
margin-bottom: 24px;
.brand-logo {
display: flex;
justify-content: center;
align-items: center;
gap: 16px;
cursor: pointer;
img {
height: 24px;
width: 24px;
}
.brand-logo-name {
font-family: 'Work Sans', sans-serif;
font-size: 18px;
font-style: normal;
font-weight: 500;
line-height: 18px;
color: #fff;
}
}
}
}
.selected-step-content {
@ -153,3 +183,18 @@
.error-container {
margin: 8px 0;
}
.lightMode {
.steps-container {
width: 20%;
height: 100%;
.steps-container-header {
.brand-logo {
.brand-logo-name {
color: black;
}
}
}
}
}

View File

@ -1,3 +1,6 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable react/jsx-no-comment-textnodes */
/* eslint-disable sonarjs/prefer-single-boolean-return */
import './ModuleStepsContainer.styles.scss';
@ -65,6 +68,7 @@ export default function ModuleStepsContainer({
selectedDataSource,
selectedEnvironment,
selectedFramework,
selectedMethod,
updateActiveStep,
updateErrorDetails,
resetProgress,
@ -75,6 +79,7 @@ export default function ModuleStepsContainer({
const [metaData, setMetaData] = useState<MetaDataProps[]>(defaultMetaData);
const lastStepIndex = selectedModuleSteps.length - 1;
// eslint-disable-next-line sonarjs/cognitive-complexity
const isValidForm = (): boolean => {
const { id: selectedModuleID } = selectedModule;
const dataSourceStep = stepsMap.dataSource;
@ -103,7 +108,10 @@ export default function ModuleStepsContainer({
dataSource: selectedDataSource,
});
if (doesHaveFrameworks && selectedFramework === '') {
if (
doesHaveFrameworks &&
(selectedFramework === null || selectedFramework === '')
) {
return false;
}
@ -128,14 +136,19 @@ export default function ModuleStepsContainer({
};
const redirectToModules = (): void => {
trackEvent('Onboarding Complete', {
trackEvent('Onboarding V2 Complete', {
module: selectedModule.id,
dataSource: selectedDataSource?.id,
framework: selectedFramework,
environment: selectedEnvironment,
selectedMethod,
serviceName,
});
if (selectedModule.id === ModulesMap.APM) {
history.push(ROUTES.APPLICATION);
} else if (selectedModule.id === ModulesMap.LogsManagement) {
history.push(ROUTES.LOGS);
history.push(ROUTES.LOGS_EXPLORER);
} else if (selectedModule.id === ModulesMap.InfrastructureMonitoring) {
history.push(ROUTES.APPLICATION);
}
@ -159,6 +172,101 @@ export default function ModuleStepsContainer({
module: selectedModule,
step: selectedModuleSteps[current + 1],
});
// on next step click track events
switch (selectedModuleSteps[current].id) {
case stepsMap.dataSource:
trackEvent('Onboarding V2: Data Source Selected', {
dataSource: selectedDataSource?.id,
framework: selectedFramework,
module: activeStep?.module?.id,
});
break;
case stepsMap.environmentDetails:
trackEvent('Onboarding V2: Environment Selected', {
dataSource: selectedDataSource?.id,
framework: selectedFramework,
environment: selectedEnvironment,
module: activeStep?.module?.id,
});
break;
case stepsMap.selectMethod:
trackEvent('Onboarding V2: Method Selected', {
dataSource: selectedDataSource?.id,
framework: selectedFramework,
environment: selectedEnvironment,
selectedMethod,
module: activeStep?.module?.id,
});
break;
case stepsMap.setupOtelCollector:
trackEvent('Onboarding V2: Setup Otel Collector', {
dataSource: selectedDataSource?.id,
framework: selectedFramework,
environment: selectedEnvironment,
selectedMethod,
module: activeStep?.module?.id,
});
break;
case stepsMap.instrumentApplication:
trackEvent('Onboarding V2: Instrument Application', {
dataSource: selectedDataSource?.id,
framework: selectedFramework,
environment: selectedEnvironment,
selectedMethod,
module: activeStep?.module?.id,
});
break;
case stepsMap.cloneRepository:
trackEvent('Onboarding V2: Clone Repository', {
dataSource: selectedDataSource?.id,
module: activeStep?.module?.id,
});
break;
case stepsMap.runApplication:
trackEvent('Onboarding V2: Run Application', {
dataSource: selectedDataSource?.id,
framework: selectedFramework,
environment: selectedEnvironment,
selectedMethod,
module: activeStep?.module?.id,
});
break;
case stepsMap.addHttpDrain:
trackEvent('Onboarding V2: Add HTTP Drain', {
dataSource: selectedDataSource?.id,
module: activeStep?.module?.id,
});
break;
case stepsMap.startContainer:
trackEvent('Onboarding V2: Start Container', {
dataSource: selectedDataSource?.id,
module: activeStep?.module?.id,
});
break;
case stepsMap.setupLogDrains:
trackEvent('Onboarding V2: Setup Log Drains', {
dataSource: selectedDataSource?.id,
module: activeStep?.module?.id,
});
break;
case stepsMap.configureReceiver:
trackEvent('Onboarding V2: Configure Receiver', {
dataSource: selectedDataSource?.id,
environment: selectedEnvironment,
module: activeStep?.module?.id,
});
break;
case stepsMap.configureAws:
trackEvent('Onboarding V2: Configure AWS', {
dataSource: selectedDataSource?.id,
environment: selectedEnvironment,
module: activeStep?.module?.id,
});
break;
default:
break;
}
}
// set meta data
@ -174,7 +282,7 @@ export default function ModuleStepsContainer({
},
{
name: 'Framework',
value: selectedFramework,
value: selectedFramework || '',
},
{
name: 'Environment',
@ -197,9 +305,21 @@ export default function ModuleStepsContainer({
}
};
const handleLogoClick = (): void => {
history.push('/');
};
return (
<div className="onboarding-module-steps">
<div className="steps-container">
<div className="steps-container-header">
<div className="brand-logo" onClick={handleLogoClick}>
<img src="/Logos/signoz-brand-logo.svg" alt="SigNoz" />
<div className="brand-logo-name">SigNoz</div>
</div>
</div>
<Space style={{ marginBottom: '24px' }}>
<Button
style={{ display: 'flex', alignItems: 'center' }}

View File

@ -14,7 +14,7 @@ interface OnboardingContextData {
ingestionData: any;
serviceName: string;
selectedEnvironment: string;
selectedFramework: string;
selectedFramework: string | null;
selectedModule: ModuleProps | null;
selectedMethod: any;
selectedDataSource: DataSourceType | null;
@ -51,7 +51,9 @@ function OnboardingContextProvider({
const [errorDetails, setErrorDetails] = useState(null);
const [selectedEnvironment, setSelectedEnvironment] = useState<string>('');
const [selectedFramework, setSelectedFramework] = useState<string>('');
const [selectedFramework, setSelectedFramework] = useState<string | null>(
null,
);
const [selectedMethod, setSelectedMethod] = useState(
OnboardingMethods.QUICK_START,

View File

@ -506,8 +506,9 @@ function PipelineListsView({
pagination={false}
/>
</DndProvider>
{showSaveButton && (
{isEditingActionMode && (
<SaveConfigButton
showSaveButton={Boolean(showSaveButton)}
onSaveConfigurationHandler={onSaveConfigurationHandler}
onCancelConfigurationHandler={onCancelConfigurationHandler}
/>

View File

@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import { SaveConfigWrapper } from './styles';
function SaveConfigButton({
showSaveButton,
onSaveConfigurationHandler,
onCancelConfigurationHandler,
}: SaveConfigButtonTypes): JSX.Element {
@ -11,14 +12,16 @@ function SaveConfigButton({
return (
<SaveConfigWrapper>
<Button
key="submit"
type="primary"
htmlType="submit"
onClick={onSaveConfigurationHandler}
>
{t('save_configuration')}
</Button>
{showSaveButton && (
<Button
key="submit"
type="primary"
htmlType="submit"
onClick={onSaveConfigurationHandler}
>
{t('save_configuration')}
</Button>
)}
<Button key="cancel" onClick={onCancelConfigurationHandler}>
{t('cancel')}
</Button>
@ -26,6 +29,7 @@ function SaveConfigButton({
);
}
export interface SaveConfigButtonTypes {
showSaveButton: boolean;
onSaveConfigurationHandler: VoidFunction;
onCancelConfigurationHandler: VoidFunction;
}

View File

@ -108,6 +108,7 @@ export const ModeAndConfigWrapper = styled.div`
export const SaveConfigWrapper = styled.div`
display: flex;
justify-content: flex-end;
gap: 0.938rem;
margin-top: 1.25rem;
`;

View File

@ -1,4 +1,5 @@
import { Col, Input, Row } from 'antd';
import { LEGEND } from 'constants/global';
// ** Components
import {
FilterLabel,
@ -13,6 +14,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { ChangeEvent, useCallback, useMemo } from 'react';
import { IBuilderFormula } from 'types/api/queryBuilder/queryBuilderData';
import { getFormatedLegend } from 'utils/getFormatedLegend';
import { AdditionalFiltersToggler } from '../AdditionalFiltersToggler';
// ** Types
@ -58,7 +60,7 @@ export function Formula({
const { name, value } = e.target;
const newFormula: IBuilderFormula = {
...formula,
[name]: value,
[name]: name === LEGEND ? getFormatedLegend(value) : value,
};
handleSetFormulaData(index, newFormula);

View File

@ -14,11 +14,11 @@ function OptionRenderer({
const optionType = getOptionType(label);
return (
<span>
<span className="option">
{optionType ? (
<SelectOptionContainer>
<div>{value}</div>
<div>
<div className="option-value">{value}</div>
<div className="option-meta-data-container">
<TagContainer>
<TagLabel>Type: </TagLabel>
<TagValue>{optionType}</TagValue>

View File

@ -18,17 +18,18 @@ export const StyledCheckOutlined = styled(CheckOutlined)`
export const SelectOptionContainer = styled.div`
display: flex;
gap: 8px;
justify-content: space-between;
align-items: center;
overflow-x: auto;
`;
export const TagContainer = styled(Tag)`
&&& {
border-radius: 0.25rem;
padding: 0.063rem 0.5rem;
font-weight: 600;
font-size: 0.75rem;
line-height: 1.25rem;
border-radius: 3px;
padding: 0.3rem 0.3rem;
font-weight: 400;
font-size: 0.6rem;
}
`;

View File

@ -0,0 +1,72 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { act } from 'react-dom/test-utils';
import ResetPassword from './index';
jest.mock('api/user/resetPassword', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.useFakeTimers();
describe('ResetPassword Component', () => {
beforeEach(() => {
userEvent.setup();
jest.clearAllMocks();
});
it('renders ResetPassword component correctly', () => {
render(<ResetPassword version="1.0" />);
expect(screen.getByText('Reset Your Password')).toBeInTheDocument();
expect(screen.getByLabelText('Password')).toBeInTheDocument();
// eslint-disable-next-line sonarjs/no-duplicate-string
expect(screen.getByLabelText('Confirm Password')).toBeInTheDocument();
expect(
// eslint-disable-next-line sonarjs/no-duplicate-string
screen.getByRole('button', { name: 'Get Started' }),
).toBeInTheDocument();
});
it('disables the "Get Started" button when password is invalid', async () => {
render(<ResetPassword version="1.0" />);
const passwordInput = screen.getByLabelText('Password');
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
const submitButton = screen.getByRole('button', { name: 'Get Started' });
act(() => {
// Set invalid password
fireEvent.change(passwordInput, { target: { value: 'password' } });
fireEvent.change(confirmPasswordInput, { target: { value: 'password' } });
});
await waitFor(() => {
// Expect the "Get Started" button to be disabled
expect(submitButton).toBeDisabled();
});
});
it('enables the "Get Started" button when password is valid', async () => {
render(<ResetPassword version="1.0" />);
const passwordInput = screen.getByLabelText('Password');
const confirmPasswordInput = screen.getByLabelText('Confirm Password');
const submitButton = screen.getByRole('button', { name: 'Get Started' });
act(() => {
fireEvent.change(passwordInput, { target: { value: 'newPassword' } });
fireEvent.change(confirmPasswordInput, { target: { value: 'newPassword' } });
});
act(() => {
jest.advanceTimersByTime(500);
});
await waitFor(() => {
// Expect the "Get Started" button to be enabled
expect(submitButton).toBeEnabled();
});
});
});

View File

@ -3,6 +3,7 @@ import resetPasswordApi from 'api/user/resetPassword';
import { Logout } from 'api/utils';
import WelcomeLeftContainer from 'components/WelcomeLeftContainer';
import ROUTES from 'constants/routes';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { Label } from 'pages/SignUp/styles';
@ -20,6 +21,8 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
const [confirmPasswordError, setConfirmPasswordError] = useState<boolean>(
false,
);
const [isValidPassword, setIsValidPassword] = useState(false);
const [loading, setLoading] = useState(false);
const { t } = useTranslation(['common']);
const { search } = useLocation();
@ -35,7 +38,7 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
}
}, [token]);
const handleSubmit: () => Promise<void> = async () => {
const handleFormSubmit: () => Promise<void> = async () => {
try {
setLoading(true);
const { password } = form.getFieldsValue();
@ -72,38 +75,88 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
});
}
};
const handleValuesChange: (changedValues: FormValues) => void = (
changedValues,
) => {
if ('confirmPassword' in changedValues) {
const { confirmPassword } = changedValues;
const isSamePassword = form.getFieldValue('password') === confirmPassword;
setConfirmPasswordError(!isSamePassword);
const validatePassword = (): boolean => {
const { password, confirmPassword } = form.getFieldsValue();
if (
password &&
confirmPassword &&
password.trim() &&
confirmPassword.trim() &&
password.length > 0 &&
confirmPassword.length > 0
) {
return password === confirmPassword;
}
return false;
};
const handleValuesChange = useDebouncedFn((): void => {
const { password, confirmPassword } = form.getFieldsValue();
if (!password || !confirmPassword) {
setIsValidPassword(false);
}
if (
password &&
confirmPassword &&
password.trim() &&
confirmPassword.trim()
) {
const isValid = validatePassword();
setIsValidPassword(isValid);
setConfirmPasswordError(!isValid);
}
}, 100);
const handleSubmit = (): void => {
const isValid = validatePassword();
setIsValidPassword(isValid);
if (token) {
handleFormSubmit();
}
};
return (
<WelcomeLeftContainer version={version}>
<FormWrapper>
<FormContainer
form={form}
onValuesChange={handleValuesChange}
onFinish={handleSubmit}
>
<FormContainer form={form} onFinish={handleSubmit}>
<Title level={4}>Reset Your Password</Title>
<div>
<Label htmlFor="Password">Password</Label>
<FormContainer.Item noStyle name="password">
<Input.Password required id="currentPassword" />
</FormContainer.Item>
<Label htmlFor="password">Password</Label>
<Form.Item
name="password"
validateTrigger="onBlur"
rules={[{ required: true, message: 'Please enter password!' }]}
>
<Input.Password
tabIndex={0}
onChange={handleValuesChange}
id="password"
data-testid="password"
/>
</Form.Item>
</div>
<div>
<Label htmlFor="ConfirmPassword">Confirm Password</Label>
<FormContainer.Item noStyle name="confirmPassword">
<Input.Password required id="UpdatePassword" />
</FormContainer.Item>
<Label htmlFor="confirmPassword">Confirm Password</Label>
<Form.Item
name="confirmPassword"
// validateTrigger="onChange"
validateTrigger="onBlur"
rules={[{ required: true, message: 'Please enter confirm password!' }]}
>
<Input.Password
onChange={handleValuesChange}
id="confirmPassword"
data-testid="confirmPassword"
/>
</Form.Item>
{confirmPasswordError && (
<Typography.Paragraph
@ -113,7 +166,8 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
marginTop: '0.50rem',
}}
>
Passwords dont match. Please try again
The passwords entered do not match. Please double-check and re-enter
your passwords.
</Typography.Paragraph>
)}
</div>
@ -124,13 +178,7 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
htmlType="submit"
data-attr="signup"
loading={loading}
disabled={
loading ||
!form.getFieldValue('password') ||
!form.getFieldValue('confirmPassword') ||
confirmPasswordError ||
token === null
}
disabled={!isValidPassword || loading}
>
Get Started
</Button>

View File

@ -4,8 +4,12 @@ import styled from 'styled-components';
export const FormWrapper = styled(Card)`
display: flex;
justify-content: center;
max-width: 432px;
width: 432px;
flex: 1;
.ant-card-body {
width: 100%;
}
`;
export const ButtonContainer = styled.div`

View File

@ -26,7 +26,11 @@ export const getColumnSearchProps = (
const queryString = getQueryString(avialableParams, urlParams);
return (
<Link to={`${ROUTES.APPLICATION}/${metrics}?${queryString.join('')}`}>
<Link
to={`${ROUTES.APPLICATION}/${encodeURIComponent(
metrics,
)}?${queryString.join('')}`}
>
<Name>{metrics}</Name>
</Link>
);

View File

@ -1,5 +1,4 @@
import { Typography } from 'antd';
import { themeColors } from 'constants/theme';
import styled from 'styled-components';
export const Container = styled.div`
@ -9,7 +8,7 @@ export const Container = styled.div`
export const Name = styled(Typography)`
&&& {
font-weight: 600;
color: ${themeColors.lightBlue};
color: #4e74f8;
cursor: pointer;
}
`;

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