Compare commits

..

61 Commits

Author SHA1 Message Date
Prashant Shahi
64d854ffa2 chore(signoz): 📌 pin versions: SigNoz 0.37.1
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-01-22 19:55:04 +05:45
Prashant Shahi
6b073280a4 Merge branch 'main' into release/v0.37.1 2024-01-22 19:54:38 +05:45
Vikrant Gupta
79e6699b37 fix: logs page crash when special chars present in the value of query (#4408) 2024-01-22 19:06:33 +05:30
Rajat Dabade
d563778479 [Fix]: Resolve glitch in graph due to variable and time stamp change (#4406)
* refactor: resolve glitch in variable and time stamp change

* refactor: removed unwanted code

* refactor: updated code
2024-01-22 16:36:03 +05:30
Vikrant Gupta
255b3dd3b0 fix: back btn navigation on first load (#4405)
* fix: back btn navigation on first load

* fix: remove console logs

* chore: refactor the logic to a function and added code comments
2024-01-22 14:47:25 +05:30
Vikrant Gupta
00e97fa401 fix: trial banner moving to left screen and breaking trace detail page (#4403)
* fix: trial banner moving to left screen and breaking trace detail page

* fix: trial banner moving to left screen and breaking trace detail page
2024-01-22 13:12:02 +05:30
Prashant Shahi
9755ba6b47 Merge pull request #4400 from SigNoz/release/v0.37.0
Release/v0.37.0
2024-01-20 02:03:04 +05:30
Nityananda Gohain
f3fdd2dd6c chore: add tenantId and orgName in usage (#4399)
* feat: add tenantId and orgName in usage

* fix: update regex

* fix: if else logic updated
2024-01-20 01:24:09 +05:30
Prashant Shahi
d4248fe933 chore(signoz): 📌 pin versions: SigNoz 0.37.0, SigNoz OtelCollector 0.88.8
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-01-20 00:19:42 +05:45
Vishal Sharma
a8d70206ab chore: update onboarding events (#4387)
* chore: update onboarding events

* chore: update retry count
2024-01-19 17:32:52 +05:30
Yunus M
7b344f7a75 Revert "fix: update time stamp on the first load in URL to help with back navigation" (#4398) 2024-01-19 17:01:52 +05:30
Vikrant Gupta
f0669a6dc1 fix: update time stamp on the first load in URL to help with back navigation (#4397)
* fix: update time stamp on the first load in URL to help with back navigation
2024-01-19 13:52:14 +05:30
Rajat Dabade
4a7d972c85 refactor: conditional based apdex on metrics and trace query range (#4395)
* refactor: conditional based apdex on metrics and trace query range

* chore: add invalid float conversion

* Revert "refactor: conditional based apdex on metrics and trace query range"

This reverts commit ca44a7aedd.

* refactor: added servicename to the query params

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-01-19 13:50:51 +05:30
Rajat Dabade
51c1f88593 Revert "[Feat]: added iscolumn in option rendering" (#4396) 2024-01-19 12:05:44 +05:30
Yunus M
c1b9049176 fix: allow workspace blocked users to extend trial (#4393)
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2024-01-19 11:11:29 +05:30
Rajat Dabade
46559014f7 [feat]: persistence of graph toggle on variable change (#4316)
* feat: persistence of graph toggle on variable change

* fix: reverted the previous changes

* chore: full view global time selector to right
2024-01-18 23:22:30 +05:30
Srikanth Chekuri
0c1a500142 chore: update onboarding docs to send logs from the end with a note for about beginning (#4374) 2024-01-18 20:02:25 +05:30
Vikrant Gupta
26d6a869c6 fix: button visibility in clickhouse and promQL headers (#4390) 2024-01-18 16:44:52 +05:30
Vikrant Gupta
f99da73098 fix: [GH-4383]: handle special characters in the services name (#4388)
* fix: [GH-4383]: handle special characters in the services name
2024-01-18 15:01:32 +05:30
Yunus M
4a1c48b72b fix: autosave layout to layout change (#4385)
* fix: autosave layout to layout change

* fix: autosave layout to layout change

* refactor: no update api call on opening dashboard

* refactor: removed extra put api call

* refactor: if condition changed

---------

Co-authored-by: Rajat-Dabade <rajat@signoz.io>
2024-01-18 15:01:10 +05:30
Rajat Dabade
1163c16506 [Feat]: added iscolumn in option rendering (#4334)
* feat: added iscolumn in option rendering

* chore: build failure

* fix: build failure
2024-01-18 14:14:10 +05:30
Vikrant Gupta
bb558dde8e Merge pull request #4380 from SigNoz/GH-4325
feat: [GH-4325]: update the URL time query params when zoom in and zoom out of charts
2024-01-17 18:15:47 +05:30
Vikrant Gupta
e89c000252 Merge branch 'develop' into GH-4325 2024-01-17 18:09:03 +05:30
Rajat Dabade
4e8e7745c1 [FE]: spaces in legend formatting (#4349)
* refactor: spaces in legend formatting

* refactor: spaces in legend formatting

* chore: string to const string

* refactor: replace string by const
2024-01-17 15:17:51 +05:30
Vikrant Gupta
512fcda33d fix: address review comments 2024-01-17 14:04:38 +05:30
Vikrant Gupta
6f43b085b0 Merge branch 'develop' into GH-4325 2024-01-17 13:57:25 +05:30
Vikrant Gupta
54038b8ddf feat: handle back btn changes 2024-01-17 13:57:05 +05:30
Yunus M
00c9ef50de fix: set max 6 months for user entered time (#4384)
* fix: set max 6 months for user entered time

* fix: set max 6 months for user entered time
2024-01-17 13:01:55 +05:30
Vikrant Gupta
52750e5248 Merge branch 'develop' into GH-4325 2024-01-16 17:40:28 +05:30
Vikrant Gupta
cbce1b1847 feat: [GH-4325]: update the URL time query params when zoom in and zoom out of charts 2024-01-16 17:35:07 +05:30
Srikanth Chekuri
abaf6126e5 chore: add /v4/query_range endpoint (#4361) 2024-01-16 16:56:20 +05:30
Yunus M
739b1bf387 feat: custom date time value (#4367)
* feat: custom date time value

* fix: update custom date picker

* fix: old placeholder value flicker

* fix: html semantics and move styles to css

* fix: remove console logs

---------

Co-authored-by: Vikrant Gupta <vikrant.thomso@gmail.com>
2024-01-16 01:13:52 +05:30
Vikrant Gupta
cbf150ef7b fix: update correct format in URL in case of custom date time (#4371) 2024-01-15 18:46:18 +05:30
Rajat Dabade
c28f367f46 refactor: updated height for date selector (#4331) 2024-01-11 16:22:20 +05:30
Yunus M
1e679a0d64 fix: empty string search doesn't return all exceptions (#4346)
* fix: empty string search doesn't return all exceptions

* fix: add type
2024-01-10 14:07:23 +05:30
Rajat Dabade
6f5f361a7e fix: soft min and soft max undefined issue (#4351) 2024-01-10 13:12:31 +05:30
Raj Kamal Singh
d65d75ef69 Fix: FE: pipelines: should be able to exit edit mode without making a change (#4335)
* fix: show cancel button on entering edit mode before any changes have been made

* chore: align pipeline page save/cancel buttons to the right
2024-01-10 11:26:25 +05:30
Srikanth Chekuri
722a38491e chore: add signozspanmetrics delta temporality pipeline (#3776) 2024-01-10 01:16:24 +05:30
Srikanth Chekuri
361efd3b52 chore: add querier v2 (#4170) 2024-01-09 22:19:03 +05:30
Rajat Dabade
7b46f86f7f [Refactor]: resolve the wrong payload in timeseries log explorer issue (#4345)
Co-authored-by: Vikrant Gupta <54737045+Vikrant2520@users.noreply.github.com>
2024-01-09 17:47:23 +05:30
Rajat Dabade
5b39dc36d6 [Feat]: soft min and soft max in uplot chart (#4287)
* feat: soft min and soft max in uplot chart

* fix: build pipeline

* fix: tsc

* refactor: added test case

* refactor: updated logic and added unit test

* refactor: updated logic

* chore: removed placeholder

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
Co-authored-by: Yunus M <myounis.ar@live.com>
2024-01-09 14:19:23 +05:30
Keshav Gupta
5fe7948be9 feat: preserve the sorting searching and pagination in dashboard page (#4319)
* feat: preserved the sorting searching and pagination

* fix: filter in dashboard data
2024-01-09 13:26:44 +05:30
Yunus M
a47a90b0f3 fix: validate password on paste, change (#4344)
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2024-01-09 12:55:24 +05:30
Srikanth Chekuri
be6bca3717 chore: add prepare query for cumulative/unspecified table (#4169) 2024-01-08 20:04:21 +05:30
Srikanth Chekuri
92717774a2 fix: throw error when response for value panel returns more than one series (#4332) 2024-01-08 18:11:22 +05:30
Srikanth Chekuri
e7fabca38e chore: fix the query range cache gaps (#4283) 2024-01-08 18:00:42 +05:30
Srikanth Chekuri
525dea343c chore: add prepare query for delta/unspecified table (#4168) 2024-01-08 01:33:04 +05:30
Yunus M
7d960b79dd feat: update sidebar and base theme styles (#4272)
* feat: update sidebar and base theme styles

* feat: update sidebar items and styles

* feat: wire up logs navigation and update user settings page

* feat: update styles to handle light mode, add full view header

* feat: update onboarding header and styles

* feat: remove unused routes

* feat: handle sidebar collapse

* feat: show pointer on logo hover

* feat: fix logs module navigations

* feat: update logo click route

* feat: update entity name color to primary in application and dashboard tables

* feat: update sidebar item styles

* feat: update collapse icon and styles

* fix: name not updated in menu on change

* fix: show invite members nav item

* fix: open invite members modal on invite team member nav item click
2024-01-05 11:15:31 +05:30
Yunus M
bdd7778e58 update readme.md (#3814)
* chore: update read.me - fe maintainers

* chore: update code owner for frontend codebase
2024-01-02 17:42:36 +05:30
Srikanth Chekuri
105216de3e chore: add prepare query for delta/unspecified timeseries (#4167)
* chore: update BuilderQuery struct and add PrepareTimeseriesFilterQuery

* chore: add prepare query for cumulative/unspecified timeseries

* chore: add prepare query for delta/unspecified timeseries

* chore: update group by to work with 23.11+

* chore: fix test

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2023-12-30 22:53:09 +05:30
Ankit Nayan
3072b7eb01 merging main 2023-12-29 22:29:15 +05:30
Raj Kamal Singh
fd9a502012 fix: opamp server: do not panic if config generation fails (#4307) 2023-12-29 21:55:38 +05:30
Prashant Shahi
cf6dc827cc Merge pull request #4306 from SigNoz/release/v0.36.2
Release/v0.36.2
2023-12-29 19:21:12 +05:30
Prashant Shahi
6530873994 Merge branch 'main' into release/v0.36.2 2023-12-29 19:12:41 +05:30
Prashant Shahi
c9c0bd38be chore(signoz): 📌 pin versions: SigNoz 0.36.2
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-12-29 19:24:12 +05:45
Prashant Shahi
9ac22fcb10 Merge branch 'develop' into release/v0.36.1 2023-12-29 19:06:53 +05:45
Raj Kamal Singh
86ff865842 Fix: logs pipelines: ignore coalesce op when generating nil check for add value expressions (#4305)
* chore: panic if agent config recommendation can't be generated

* chore: add case with coalesce op in field nil check generation tests

* fix: ignore expr ast member nodes that contain coalesce op in them
2023-12-29 18:21:01 +05:30
Rajat Dabade
e792c48f6d [Refactor]: css fixes (#4248) 2023-12-29 16:05:46 +05:30
Rajat Dabade
8ee92516ca [Refactor]: updated css for height of Panel in LeftContainer (#4030)
* refactor: updated css

* refactor: updated the css

* refactor: removed overflow hidden
2023-12-29 15:52:35 +05:30
Prashant Shahi
79c05d8fa8 Merge pull request #4304 from SigNoz/release/v0.36.1
Release/v0.36.1
2023-12-29 15:39:38 +05:30
Prashant Shahi
019bc8c1df chore(signoz): 📌 pin versions: SigNoz 0.36.1, SigNoz OtelCollector 0.88.6
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-12-29 15:44:27 +05:45
193 changed files with 5493 additions and 1977 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.0
image: signoz/query-service:0.37.1
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.0
image: signoz/frontend:0.37.1
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.4
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.4
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.4
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.4}
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.4
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.4
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.0}
image: signoz/query-service:${DOCKER_TAG:-0.37.1}
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.0}
image: signoz/frontend:${DOCKER_TAG:-0.37.1}
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.4}
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.4}
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.4}
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

@@ -193,7 +193,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
// start the usagemanager
usageManager, err := usage.New("sqlite", localDB, lm.GetRepo(), reader.GetConn())
usageManager, err := usage.New("sqlite", modelDao, lm.GetRepo(), reader.GetConn())
if err != nil {
return nil, err
}
@@ -331,6 +331,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
apiHandler.RegisterMetricsRoutes(r, am)
apiHandler.RegisterLogsRoutes(r, am)
apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am)
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},

View File

@@ -20,6 +20,8 @@ type Usage struct {
TimeStamp time.Time `json:"timestamp"`
Count int64 `json:"count"`
Size int64 `json:"size"`
OrgName string `json:"orgName"`
TenantId string `json:"tenantId"`
}
type UsageDB struct {

View File

@@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"regexp"
"strings"
"sync/atomic"
"time"
@@ -11,10 +13,10 @@ import (
"github.com/ClickHouse/clickhouse-go/v2"
"github.com/go-co-op/gocron"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
"go.signoz.io/signoz/ee/query-service/dao"
licenseserver "go.signoz.io/signoz/ee/query-service/integrations/signozio"
"go.signoz.io/signoz/ee/query-service/license"
"go.signoz.io/signoz/ee/query-service/model"
@@ -38,15 +40,29 @@ type Manager struct {
licenseRepo *license.Repo
scheduler *gocron.Scheduler
modelDao dao.ModelDao
tenantID string
}
func New(dbType string, db *sqlx.DB, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn) (*Manager, error) {
func New(dbType string, modelDao dao.ModelDao, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn) (*Manager, error) {
hostNameRegex := regexp.MustCompile(`tcp://(?P<hostname>.*):`)
hostNameRegexMatches := hostNameRegex.FindStringSubmatch(os.Getenv("ClickHouseUrl"))
tenantID := ""
if len(hostNameRegexMatches) == 2 {
tenantID = hostNameRegexMatches[1]
tenantID = strings.TrimRight(tenantID, "-clickhouse")
}
m := &Manager{
// repository: repo,
clickhouseConn: clickhouseConn,
licenseRepo: licenseRepo,
scheduler: gocron.NewScheduler(time.UTC).Every(1).Day().At("00:00"), // send usage every at 00:00 UTC
modelDao: modelDao,
tenantID: tenantID,
}
return m, nil
}
@@ -123,6 +139,19 @@ func (lm *Manager) UploadUsage() {
zap.S().Info("uploading usage data")
// Try to get the org name
orgName := ""
orgNames, err := lm.modelDao.GetOrgs(ctx)
if err != nil {
zap.S().Errorf("failed to get org data: %v", zap.Error(err))
} else {
if len(orgNames) != 1 {
zap.S().Errorf("expected one org but got %d orgs", len(orgNames))
} else {
orgName = orgNames[0].Name
}
}
usagesPayload := []model.Usage{}
for _, usage := range usages {
usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data))
@@ -142,6 +171,8 @@ func (lm *Manager) UploadUsage() {
usageData.ExporterID = usage.ExporterID
usageData.Type = usage.Type
usageData.Tenant = usage.Tenant
usageData.OrgName = orgName
usageData.TenantId = lm.tenantID
usagesPayload = append(usagesPayload, usageData)
}

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

@@ -8,11 +8,13 @@ export const Layout = styled(LayoutComponent)`
min-height: calc(100vh - 8rem);
overflow: hidden;
height: 100%;
flex-direction: column !important;
}
`;
export const LayoutContent = styled(LayoutComponent.Content)`
overflow-y: auto;
height: 100%;
`;
export const ChildrenContainer = styled.div`

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

@@ -18,13 +18,14 @@ import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariab
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
import { getGraphVisibilityStateOnDataChange } from '../utils';
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
import GraphManager from './GraphManager';
// import GraphManager from './GraphManager';
@@ -36,13 +37,14 @@ function FullView({
fullViewOptions = true,
onClickHandler,
name,
originalName,
yAxisUnit,
options,
onDragSelect,
isDependedDataLoaded = false,
graphsVisibilityStates,
onToggleModelHandler,
parentChartRef,
setGraphsVisibilityStates,
parentGraphVisibilityState,
}: FullViewProps): JSX.Element {
const { selectedTime: globalSelectedTime } = useSelector<
AppState,
@@ -55,6 +57,20 @@ function FullView({
const { selectedDashboard, isDashboardLocked } = useDashboard();
const { graphVisibilityStates: localStoredVisibilityStates } = useMemo(
() =>
getGraphVisibilityStateOnDataChange({
options,
isExpandedName: false,
name: originalName,
}),
[options, originalName],
);
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState(
localStoredVisibilityStates,
);
const getSelectedTime = useCallback(
() =>
timeItems.find((e) => e.enum === (widget?.timePreferance || 'GLOBAL_TIME')),
@@ -132,6 +148,8 @@ function FullView({
thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
softMax: widget.softMax === undefined ? null : widget.softMax,
softMin: widget.softMin === undefined ? null : widget.softMin,
});
setChartOptions(newChartOptions);
@@ -142,9 +160,9 @@ function FullView({
useEffect(() => {
graphsVisibilityStates?.forEach((e, i) => {
fullViewChartRef?.current?.toggleGraph(i, e);
parentChartRef?.current?.toggleGraph(i, e);
});
}, [graphsVisibilityStates, parentChartRef]);
parentGraphVisibilityState(graphsVisibilityStates);
}, [graphsVisibilityStates, parentGraphVisibilityState]);
if (response.isFetching) {
return <Spinner height="100%" size="large" tip="Loading..." />;
@@ -204,7 +222,7 @@ function FullView({
{canModifyChart && chartOptions && !isDashboardLocked && (
<GraphManager
data={chartData}
name={name}
name={originalName}
options={chartOptions}
yAxisUnit={yAxisUnit}
onToggleModelHandler={onToggleModelHandler}

View File

@@ -50,13 +50,14 @@ export interface FullViewProps {
fullViewOptions?: boolean;
onClickHandler?: OnClickPluginOpts['onClick'];
name: string;
originalName: string;
options: uPlot.Options;
yAxisUnit?: string;
onDragSelect: (start: number, end: number) => void;
isDependedDataLoaded?: boolean;
graphsVisibilityStates?: boolean[];
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
setGraphsVisibilityStates: Dispatch<SetStateAction<boolean[]>>;
parentChartRef: GraphManagerProps['lineChartRef'];
parentGraphVisibilityState: Dispatch<SetStateAction<boolean[]>>;
}
export interface GraphManagerProps extends UplotProps {
@@ -64,8 +65,8 @@ export interface GraphManagerProps extends UplotProps {
yAxisUnit?: string;
onToggleModelHandler?: () => void;
options: uPlot.Options;
setGraphsVisibilityStates: FullViewProps['setGraphsVisibilityStates'];
graphsVisibilityStates: FullViewProps['graphsVisibilityStates'];
graphsVisibilityStates?: boolean[];
setGraphsVisibilityStates: Dispatch<SetStateAction<boolean[]>>;
lineChartRef?: MutableRefObject<ToggleGraphProps | undefined>;
parentChartRef?: MutableRefObject<ToggleGraphProps | undefined>;
}

View File

@@ -17,7 +17,6 @@ import {
SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
@@ -39,13 +38,15 @@ function WidgetGraphComponent({
queryResponse,
errorMessage,
name,
onClickHandler,
threshold,
headerMenuList,
isWarning,
data,
options,
graphVisibiltyState,
onClickHandler,
onDragSelect,
setGraphVisibility,
}: WidgetGraphComponentProps): JSX.Element {
const [deleteModal, setDeleteModal] = useState(false);
const [hovered, setHovered] = useState(false);
@@ -59,29 +60,28 @@ function WidgetGraphComponent({
const lineChartRef = useRef<ToggleGraphProps>();
const graphRef = useRef<HTMLDivElement>(null);
const { graphVisibilityStates: localStoredVisibilityStates } = useMemo(
() =>
getGraphVisibilityStateOnDataChange({
useEffect(() => {
if (queryResponse.isSuccess) {
const {
graphVisibilityStates: localStoredVisibilityState,
} = getGraphVisibilityStateOnDataChange({
options,
isExpandedName: true,
isExpandedName: false,
name,
}),
[options, name],
);
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState<
boolean[]
>(localStoredVisibilityStates);
});
setGraphVisibility(localStoredVisibilityState);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [queryResponse.isSuccess]);
useEffect(() => {
setGraphsVisibilityStates(localStoredVisibilityStates);
if (!lineChartRef.current) return;
localStoredVisibilityStates.forEach((state, index) => {
graphVisibiltyState.forEach((state, index) => {
lineChartRef.current?.toggleGraph(index, state);
});
setGraphsVisibilityStates(localStoredVisibilityStates);
}, [localStoredVisibilityStates]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard();
@@ -274,13 +274,14 @@ function WidgetGraphComponent({
>
<FullView
name={`${name}expanded`}
originalName={name}
widget={widget}
yAxisUnit={widget.yAxisUnit}
onToggleModelHandler={onToggleModelHandler}
parentChartRef={lineChartRef}
parentGraphVisibilityState={setGraphVisibility}
onDragSelect={onDragSelect}
setGraphsVisibilityStates={setGraphsVisibilityStates}
graphsVisibilityStates={graphsVisibilityStates}
options={options}
/>
</Modal>

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,10 @@ function GridCardGraph({
? headerMenuList.filter((menu) => menu !== MenuItemKeys.CreateAlerts)
: headerMenuList;
const [graphVisibility, setGraphVisibility] = useState<boolean[]>(
Array(queryResponse.data?.payload?.data.result.length || 0).fill(true),
);
const options = useMemo(
() =>
getUPlotChartOptions({
@@ -135,11 +180,17 @@ function GridCardGraph({
thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
softMax: widget.softMax === undefined ? null : widget.softMax,
softMin: widget.softMin === undefined ? null : widget.softMin,
graphsVisibilityStates: graphVisibility,
setGraphsVisibilityStates: setGraphVisibility,
}),
[
widget?.id,
widget?.yAxisUnit,
widget.thresholds,
widget.softMax,
widget.softMin,
queryResponse.data?.payload,
containerDimensions,
isDarkMode,
@@ -147,6 +198,8 @@ function GridCardGraph({
onClickHandler,
minTimeScale,
maxTimeScale,
graphVisibility,
setGraphVisibility,
],
);
@@ -167,6 +220,8 @@ function GridCardGraph({
threshold={threshold}
headerMenuList={menuList}
onClickHandler={onClickHandler}
graphVisibiltyState={graphVisibility}
setGraphVisibility={setGraphVisibility}
/>
)}
</div>

View File

@@ -1,7 +1,7 @@
import { ToggleGraphProps } from 'components/Graph/types';
import { UplotProps } from 'components/Uplot/Uplot';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { MutableRefObject, ReactNode } from 'react';
import { Dispatch, MutableRefObject, ReactNode, SetStateAction } from 'react';
import { UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
@@ -28,6 +28,8 @@ export interface WidgetGraphComponentProps extends UplotProps {
threshold?: ReactNode;
headerMenuList: MenuItemKeys[];
isWarning: boolean;
graphVisibiltyState: boolean[];
setGraphVisibility: Dispatch<SetStateAction<boolean[]>>;
}
export interface GridCardGraphProps {

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

@@ -1,7 +1,7 @@
import styled from 'styled-components';
export const WrapperStyled = styled.div`
height: 100%;
height: 95%;
overflow: hidden;
& .ant-table-wrapper {

View File

@@ -1,106 +0,0 @@
import { act } from 'react-dom/test-utils';
import { fireEvent, render, screen, within } from 'tests/test-utils';
import ListAlertRules from '.';
describe('ListAlertRules', () => {
test('Should render the table', async () => {
act(() => {
render(<ListAlertRules />);
});
const newAlert = await screen.findByRole('button', {
name: /plus new alert/i,
});
expect(newAlert).toBeInTheDocument();
const status = await screen.findByText(/status/i);
expect(status).toBeInTheDocument();
const alertName = await screen.findByText(/alert name/i);
expect(alertName).toBeInTheDocument();
const severity = await screen.findByText(/severity/i);
expect(severity).toBeInTheDocument();
const label = await screen.findByText(/label/i);
expect(label).toBeInTheDocument();
const action = await screen.findByText(/action/i);
expect(action).toBeInTheDocument();
});
test('Should render the table data', async () => {
act(() => {
render(<ListAlertRules />);
});
const status = await screen.findByText(/status/i);
expect(status).toBeInTheDocument();
const disabledRow = await screen.findByRole('row', {
name: /disabled Test Rule 1 warning details: https:\/\/stagi\.\.\. hello: world region: us \+1 ellipsis/i,
});
expect(disabledRow).toBeInTheDocument();
const actionButton = within(disabledRow).getByRole('button', {
name: /ellipsis/i,
});
expect(actionButton).toBeInTheDocument();
fireEvent.mouseOver(actionButton);
const enabled = await screen.findByRole('menuitem', {
name: /enable/i,
});
expect(enabled).toBeInTheDocument();
});
test('Should render enabled for disabled alert in menu', async () => {
act(() => {
render(<ListAlertRules />);
});
const disabledRow = await screen.findByRole('row', {
name: /disabled Test Rule 1 warning details: https:\/\/stagi\.\.\. hello: world region: us \+1 ellipsis/i,
});
expect(disabledRow).toBeInTheDocument();
const actionButton = within(disabledRow).getByRole('button', {
name: /ellipsis/i,
});
expect(actionButton).toBeInTheDocument();
fireEvent.mouseOver(actionButton);
const enabled = await screen.findByRole('menuitem', {
name: /enable/i,
});
expect(enabled).toBeInTheDocument();
});
test('Should render disabled for Ok alert in menu', async () => {
act(() => {
render(<ListAlertRules />);
});
const enabledRow = await screen.findByRole('row', {
name: /ok test rule 2 warning - ellipsis/i,
});
expect(enabledRow).toBeInTheDocument();
const actionButton = within(enabledRow).getByRole('button', {
name: /ellipsis/i,
});
expect(actionButton).toBeInTheDocument();
fireEvent.mouseOver(actionButton);
const disabled = await screen.findByRole('menuitem', {
name: /disable/i,
});
expect(disabled).toBeInTheDocument();
});
});

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

@@ -12,9 +12,9 @@ export const Container = styled(Card)<Props>`
}
.ant-card-body {
padding: 8px;
padding: ${({ $panelType }): string =>
$panelType === PANEL_TYPES.TABLE ? '0 0' : '1.5rem 0'};
height: 57vh;
overflow: auto;
display: flex;
flex-direction: column;
}

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

@@ -27,6 +27,10 @@ import CustomColor from './CustomColor';
import ShowCaseValue from './ShowCaseValue';
import { ThresholdProps } from './types';
const wrapStyle = {
flexWrap: 'wrap',
} as React.CSSProperties;
function Threshold({
index,
thresholdOperator = '>',
@@ -220,7 +224,7 @@ function Threshold({
}
>
{selectedGraph === PANEL_TYPES.TIME_SERIES && (
<>
<Space style={wrapStyle}>
<Typography.Text>Label</Typography.Text>
{isEditMode ? (
<Input
@@ -232,7 +236,7 @@ function Threshold({
) : (
<ShowCaseValue width="180px" value={label || 'none'} />
)}
</>
</Space>
)}
{(selectedGraph === PANEL_TYPES.VALUE ||
selectedGraph === PANEL_TYPES.TABLE) && (
@@ -243,7 +247,7 @@ function Threshold({
{isEditMode ? (
<>
{selectedGraph === PANEL_TYPES.TABLE && (
<Space>
<Space style={wrapStyle}>
<Select
style={{
minWidth: '150px',
@@ -270,7 +274,7 @@ function Threshold({
) : (
<>
{selectedGraph === PANEL_TYPES.TABLE && (
<Space>
<Space style={wrapStyle}>
<ShowCaseValue width="150px" value={tableSelectedOption} />
<Typography.Text>is</Typography.Text>
</Space>
@@ -283,7 +287,7 @@ function Threshold({
</Space>
</div>
<div className="threshold-units-selector">
<Space>
<Space style={wrapStyle}>
{isEditMode ? (
<InputNumber
style={{ backgroundColor }}
@@ -311,7 +315,7 @@ function Threshold({
<div>
<Space direction="vertical">
<Typography.Text>Show with</Typography.Text>
<Space>
<Space style={wrapStyle}>
{isEditMode ? (
<>
<ColorSelector setColor={setColor} thresholdColor={color} />

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

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