Compare commits

...

132 Commits

Author SHA1 Message Date
Prashant Shahi
eb6f038db5 Merge branch 'main' into release/v0.45 2024-05-08 21:21:45 +05:30
Prashant Shahi
47dcd994f0 chore(signoz): 📌 pin versions: SigNoz 0.45.0, SigNoz OtelCollector 0.88.22
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-05-08 20:03:12 +05:30
Vikrant Gupta
211fe4fdd5 fix: prevent page from crashing in case items in filters is null (#4964)
* fix: prevent page from crashing in case items in filters is null

* fix: added null check for filters as well
2024-05-06 19:18:27 +05:30
Vikrant Gupta
e2992b42c1 fix: make integrations available for the ee cloud user (#4963) 2024-05-06 19:17:50 +05:30
Nityananda Gohain
3957d91a9b fix: add read-in-order config (#4918) 2024-05-06 15:01:53 +05:30
Vishal Sharma
967aa16f21 feat: sort tags and events in trace detail (#4962) 2024-05-05 09:03:31 +05:30
Vikrant Gupta
08b1a87cb5 Revert "fix: step interval not getting updated on time range change (#4944)" (#4955)
This reverts commit 5c1c09c790.
2024-05-01 22:46:32 +05:30
SagarRajput-7
03ddcdd20e feat: added test cases for pipeline pages (#4872)
* feat: added test cases for pipeline pages

* feat: added test cases for changeHistory

* chore: change history table test case added

* chore: added create pipeline button test cases

* chore: updated useAnalytics mocking
2024-05-01 18:49:04 +05:30
SagarRajput-7
1aec7f3ca6 feat: added tooltips for facing issue btn (#4948) 2024-05-01 18:36:56 +05:30
Vikrant Gupta
241edcb88a fix: text change for saved views in traces (#4953) 2024-05-01 18:28:05 +05:30
Srikanth Chekuri
27d12871af chore: disallow small step intervals for large durations (#4950) 2024-05-01 17:03:46 +05:30
Yunus M
e78e1d4b63 fix: add safety checks to handle null response from query range API (#4939) 2024-05-01 15:49:30 +05:30
Yunus M
64bf580323 feat: show milliseconds in timestamp in logs views (#4949)
* feat: show milliseconds in timestamp in logs views

* fix: remove console log

---------

Co-authored-by: Vikrant Gupta <vikrant.thomso@gmail.com>
2024-05-01 15:27:48 +05:30
SagarRajput-7
152aa4b518 fix: fixed facing issue btn alignment issue (#4936)
* fix: fixed facing issue btn alignment issue

* fix: fixed facing issue btn alignment issue

* fix: moved intercom help messages to util file
2024-05-01 14:49:42 +05:30
Vikrant Gupta
b3d5831574 fix: ch queries sending builder as query type in query range api for exceptions alerts (#4941)
* fix: ch queries sending builder as query type in query range api for exceptions alerts

* fix: ch queries sending builder as query type in query range api for exceptions alerts

* fix: alerts routing from logs explorer and dashboards
2024-05-01 14:39:39 +05:30
Vikrant Gupta
b85b9f42ed fix: time interval not getting updated in case of edit dashboard (#4940) 2024-05-01 13:00:18 +05:30
Vikrant Gupta
5c1c09c790 fix: step interval not getting updated on time range change (#4944) 2024-05-01 12:47:33 +05:30
Vishal Sharma
33960b05fd chore: update facing issues text (#4942) 2024-04-30 23:38:15 +05:30
Vikrant Gupta
191d9b0648 feat: introducing collapsable rows for dashboards (#4806)
* feat: dashboard panel grouping initial setup

* feat: added panel map to the dashboard response and subsequent types for the same

* feat: added panel map to the dashboard response and subsequent types for the same

* feat: added settings modal

* feat: handle panel collapse and open changes

* feat: handle creating panel map when dashboard layout changes

* feat: handle creating panel map when dashboard layout changes

* feat: refactor code

* feat: handle multiple collapsable rows

* fix: type issues

* feat: handle row collapse button and scroll

* feat: handle y axis movement for rows

* feat: handle delete row

* feat: handle settings name change

* feat: disable collapse/uncollapse when dashboard loading to avoid async states

* feat: decrease the height of the grouping row

* fix: row height management

* fix: handle empty row case

* feat: remove resize handle from the row

* feat: handle re-arrangement of panels

* feat: increase height of default new widget

* feat: added safety checks
2024-04-30 14:36:47 +05:30
Srikanth Chekuri
7d81bc3417 fix: value panel restriction should be on enabled queries (#4934) 2024-04-30 09:53:03 +05:30
Srikanth Chekuri
506916661d fix: metric limit works with cache (#4935) 2024-04-30 01:25:50 +05:30
Nityananda Gohain
5326f2d23b fix: dont enrich if non empty keys are not same (#4930)
* fix: dont enrich if non empty keys are not same

* fix: update if any of the type and dataType is empty but other is matching
2024-04-29 22:40:40 +05:30
dependabot[bot]
dfaa344dce chore(deps): bump express from 4.18.2 to 4.19.2 in /frontend (#4840)
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 21:29:27 +05:30
SagarRajput-7
882b540a0b chore: [SIG-583]: Jest coverage collection config (#4920)
* chore: [SIG-583]: Jest coverage collection config

* fix: added missing attribute
2024-04-27 11:31:37 +05:30
Ankit Nayan
1c4b579c3d fix: frontend/package.json & frontend/yarn.lock to reduce vulnerabilities (#4341)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-AXIOS-6144788

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
Co-authored-by: Prashant Shahi <prashant@signoz.io>
2024-04-26 15:00:06 +05:30
Yunus M
706f25cc5d fix: frontend/Dockerfile to reduce vulnerabilities (#4913)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE318-EXPAT-6241039
- https://snyk.io/vuln/SNYK-ALPINE318-LIBX11-6042398
- https://snyk.io/vuln/SNYK-ALPINE318-LIBXML2-6245694
- https://snyk.io/vuln/SNYK-ALPINE318-OPENSSL-6032386
- https://snyk.io/vuln/SNYK-ALPINE318-OPENSSL-6032386

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
Co-authored-by: Prashant Shahi <prashant@signoz.io>
2024-04-26 11:41:53 +05:30
Raj Kamal Singh
e6e0a59f5f Feat: integrations: clickhouse (#4879)
* chore: get built-in clickhouse integration started

* chore: update config pre-requisites for clickhouse integration

* chore: add details of metrics data collected for clickhouse integration

* chore: clickhouse integration: move list of data-collected to its own file

* chore: clickhouse integration: get overview dashboard started

* chore: start with logs collection instructions for clickhouse

* chore: regex parsing for clickhouse text logs

* chore: timestamp parsing for clickhouse logs

* chore: severity parsing for clickhouse logs

* chore: clickhouse logs parsing: move parsed message to body if available

* chore: update pre-reqs for collecting from system.query_log table

* feat: add instructions for collecting from system.query_log table

* feat: add logs attribs collected

* chore: some cleanup of clickhouse overview dashboard

* feat: finish up with clickhouse overview dashboard for clickhouse integration
2024-04-26 09:45:57 +05:30
Prashant Shahi
b2c170c752 Merge pull request #4919 from SigNoz/release/v0.44
post-release: sync release changes in develop
2024-04-25 22:13:44 +05:30
Prashant Shahi
ee421af95c Merge pull request #4917 from SigNoz/release/v0.44
Release/v0.44
2024-04-25 19:26:25 +05:30
Prashant Shahi
453be9074d Merge branch 'main' into release/v0.44 2024-04-25 18:07:31 +05:30
Prashant Shahi
3272444e13 chore(signoz): 📌 pin versions: SigNoz 0.44.0
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-04-25 18:01:10 +05:30
Vishal Sharma
71b3e6d522 fix: rate in table panel (#4916)
* fix: rate in table panel

* test: added test cases for rate operation in table panel
2024-04-25 14:15:33 +05:30
Prashant Shahi
6cf7cc9f4f chore: bump SigNoz/prometheus from v1.10.1 to v1.11.0 (#4912)
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-04-25 13:12:30 +05:30
Prashant Shahi
5ec2f17d09 chore: pin SigNoz OtelCollector 0.88.21 and update ClickHouse dsn (#4909)
* chore: 📌 pin versions: SigNoz OtelCollector 0.88.21
* chore(clickhouse): update dsn as per the new parsing logic

---------

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-04-24 21:15:07 +05:30
Prashant Shahi
a45fb8ec0c fix(clickhouse): update endpoint of the healthcheck in deployment files (#4908)
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-04-24 19:06:05 +05:30
SagarRajput-7
bd148bbd5a fix: restrict visibility of facing-issue button to only cloud users with intercom setup (#4907)
* fix: restrict visibilty of facing-issue button to only cloud users with intercom setup

* fix: restrict visibilty of facing-issue button to only cloud users with intercom setup

* fix: added a comment

* fix: added chat support feature flag condition

* fix: added a comment

* fix: changed folder structure
2024-04-24 18:56:19 +05:30
SagarRajput-7
1306e99ca8 fix: alert threshold form is resetting to default query option on stage & run (#4876)
* fix: alert threshold form is resetting to default query option on stage & run

* fix: alert threshold - added safety check when the queryOption is deleted
2024-04-24 15:58:59 +05:30
SagarRajput-7
1a8f063b4b feat: [SIG-585]: Added facingIssueBtn at Dashboard list, detail and alert list, detail etc. pages (#4899)
* feat: [SIG-585]: Added facingIssueBtn at Dashboard list, detail and alert list, detail etc. pages

* feat: [SIG-585]: Added facingIssueBtn for dashboard & alert listing

* feat: [SIG-585]: Added facingIssueBtn for dashboard & alert detail and dashboard panel edit

* feat: [SIG-585]: Code cleanup

* feat: [SIG-585]: Changed logEvent attribute and event content

* feat: [SIG-585]: Changed alignment of button and button text

* feat: [SIG-585]: Changed button color to amber

* feat: [SIG-585]: Code cleanup
2024-04-24 15:48:48 +05:30
SagarRajput-7
c7668b9a78 chore: added jest coverage setup (#4871)
* chore: added jest coverage setup

* chore: added sample test changes

* chore: revert sample code change

* chore: changed script to add yarn install handle and removed checkouts to develop

* chore: added fix for checkout issue

* chore: added fix for checkout issue

* chore: added fix for checkout issue

* chore: added fix for checkout issue

* chore: added sample test changes

* chore: revert sample test cases

* chore: adding coverage threshold

* chore: added coverage threshold and sample test code

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: testing fetch and checkout

* chore: code cleanup and threshold adjustment

* chore: testing fetch and checkout
2024-04-23 20:06:02 +05:30
Vikrant Gupta
5e3dba2587 fix: do not save dashboard panel on creating a new panel if discard is pressed (#4884)
* fix: do not save dashboard panel on creating a new panel if discard is pressed

* fix: remove console log
2024-04-23 19:39:41 +05:30
Vikrant Gupta
374f30e0cd fix: billing container scroll issue when trial banner present (#4893) 2024-04-22 19:31:54 +05:30
Vikrant Gupta
38d2833931 fix: handle the old variables by name instead of id (#4890) 2024-04-20 17:54:29 +05:30
SagarRajput-7
731eacbbca feat: [SIG-584]: moved facing issue btn tracking from trackEvent to logEvent (#4888)
* feat: [SIG-584]: moved facing issue btn tracking from trackEvent to logEvent

* feat: [SIG-584]: removed logEvent from useAnalytic hook
2024-04-20 16:40:47 +05:30
dependabot[bot]
a63bb139bf chore(deps): bump golang.org/x/net from 0.21.0 to 0.23.0 (#4889)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.21.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.21.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-20 07:52:44 +05:30
Vikrant Gupta
a140bef0e6 fix: handle the case where the functions are recieved as undefined in the query response (#4880) 2024-04-18 11:17:28 +05:30
Vikrant Gupta
48e5436167 fix: handle the edge cases for alerts create form (#4875) 2024-04-17 16:40:21 +05:30
Yunus M
0fc664a387 feat: show timestamp in list view of trace explorer (#4860)
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2024-04-16 12:41:13 +05:30
Prashant Shahi
5817d50652 Merge pull request #4853 from SigNoz/release/v0.43.x
Release/v0.43.x
2024-04-15 20:07:57 +05:45
Prashant Shahi
bb318cf52a Merge pull request #4852 from SigNoz/release/v0.43.x
chore(pre-release): 📌 pin versions: SigNoz 0.43.0, OtelCollector 0.88.20
2024-04-15 19:50:20 +05:45
Prashant Shahi
ec0185da61 Merge branch 'develop' into release/v0.43.x 2024-04-15 18:24:21 +05:45
Srikanth Chekuri
fc2bdb610f chore: make send resolved notifs configurable (#4833) 2024-04-15 13:46:12 +05:30
Srikanth Chekuri
a9464de62d chore: use last 1day data for apdex latency metric meta (#4846) 2024-04-15 13:37:08 +05:30
Yunus M
57bfdedfe1 feat: send event if users click in facing issues button in get started (#4859)
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2024-04-15 13:26:20 +05:30
SagarRajput-7
7bdc9c0cb0 fix: fixed sidenav alignment with and without get-started (#4829) 2024-04-15 11:40:40 +05:30
SagarRajput-7
0d5934d56b chore: added test cases for Logs (#4828)
* chore: add test cases for Logs

* chore: add test cases for Logs - explorer

* chore: add test cases for Logs - toolbarAction

* chore: add test cases for Logs - list and table view

* chore: add test cases for Logs - list and table view

* chore: code fix
2024-04-15 11:30:25 +05:30
Vikrant Gupta
3a5a61aff9 fix: wrong payload being sent in the dashboard payload (#4854)
* fix: wrong payload being sent in the dashboard payload

* fix: sync the update set dashboard function

* fix: syncronise the var updates

* fix: jest test cases

* fix: added review comments

* fix: do not make query range API call until the queue is empty

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-04-15 11:11:14 +05:30
Rajat Dabade
a54b7baa7d feat: add support for pie chart panel type (#4751) 2024-04-13 09:55:02 +05:30
Prashant Shahi
cd63dd972d chore(pre-release): 📌 pin versions: SigNoz 0.43.0, SigNoz OtelCollector 0.88.20
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-04-10 23:55:39 +05:45
Nityananda Gohain
389058b9b4 feat: allow query restrictions for log queries (#4778)
* feat: allow query restrictions for log queries

* fix: error check

* fix: set default only if not present

* chore: add error log for query restriction error

* fix: add limtations for traces

* fix: fix wrapper

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-04-10 17:25:57 +05:30
Rajat Dabade
27e412d1ee refactor: removed the error body (#4850)
* refactor: removed the error body

* refactor: check for error response body condition

* refactor: empty object check
2024-04-10 15:19:01 +05:30
Vikrant Gupta
03dccb0101 fix: trace page breaking (#4844)
* fix: trace page breaking

* fix: crash on reload

* fix: hide update button for viewer role in collapsed mode
2024-04-09 19:20:08 +05:30
Rajat Dabade
25b74b48a5 [Fix]: selected time in right container to be used in query-range (#4842) 2024-04-09 13:48:54 +05:30
Vikrant Gupta
6815a96d29 feat: support of changing panel type in dashboards (#4759)
* feat: support of changing panel type in dashboards

* feat: add handle query change function

* feat: last bit of minor change

* feat: apply current query updates to superset query

* feat: pr cleanup

* feat: handle list type change

* fix: build issues

* fix: changes required due to refactor

* fix: handle offset and page size for list queries

* feat: handle functions propagation

* feat: handle the spaceAggregation value to retain

* fix: handle list panel type changes

* feat: handle removing the graph list from the side selection in case of metrics

* feat: handle list type qb changes

* feat: handle page breaking

* feat: pick dataSource from newQUeryItem

* feat: handle page reload
2024-04-09 13:36:19 +05:30
SagarRajput-7
e9bb05cc5d fix: added billing page condition to not show when its not either cloud or enterprise (#4827) 2024-04-09 12:05:39 +05:30
dependabot[bot]
31c0b94ae6 chore(deps): bump webpack-dev-middleware in /frontend (#4838)
Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.3 to 5.3.4.
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4)

---
updated-dependencies:
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-09 11:58:39 +05:30
Vikrant Gupta
59c242961f feat: revamp integration flow (#4832)
* feat: revamp integration flow

* feat: final design changes

* feat: make test connection button grey
2024-04-09 11:29:54 +05:30
Yunus M
872ed9e963 fix: time preference from the panel should be used to fetch data (#4836) 2024-04-09 11:05:49 +05:30
Yunus M
d6cd155988 feat: update styles for toolbar (#4824) 2024-04-08 10:52:58 +05:30
Prashant Shahi
7f4a61ffb1 Merge pull request #4822 from SigNoz/fix/heartbeat-ratelimit
fix: avoid rate limiting heartbeat events
2024-04-06 01:24:36 +05:45
makeavish
7737d513a7 fix: avoid rate limiting heartbeat events 2024-04-06 00:54:14 +05:30
Vishal Sharma
2bd666efae fix: query range API validation (#4821) 2024-04-05 23:57:11 +05:30
Srikanth Chekuri
d98265f345 chore: add log comment for prom queries (#4819) 2024-04-05 21:11:53 +05:30
Vikrant Gupta
b480ff1e48 revert: prevent stage and run query to apply legends (#4816) 2024-04-05 02:58:09 +05:30
Vishal Sharma
af353b9340 fix: sidebar jitter (#4815) 2024-04-04 20:53:36 +05:30
Raj Kamal Singh
96e7505922 Chore: logs pipelines cover all available processors with tests (#4454)
* chore: add explicit happy case test for regex parsing processor

* chore: add explicit happy case test for grok parsing processor

* chore: add explicit happy case test for JSON parsing processor

* chore: log pipelines: move trace parser test to processors_test.go

* chore: add explicit happy case test for ADD processor

* chore: add explicit happy case test for remove processor

* chore: add explicit happy case test for copy processor

* chore: add explicit happy case test for move processor

* fix: fix broken grok parser test: change test int value to int64

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2024-04-04 18:48:01 +05:30
Yunus M
8f6f2f0018 feat: views toolbar visibility, light mode (#4813) 2024-04-04 18:47:41 +05:30
Yunus M
1f25d386df feat: retry on chunk load error (#4803)
* feat: retry on chunk load error

* feat: consistent return

* feat: use lazyRetry for help tooltip

* feat: enable sourcemaps
2024-04-04 18:00:50 +05:30
SagarRajput-7
2d7a3733da fix: [SIG-578]: changed color of function block connector (#4800) 2024-04-04 16:45:59 +05:30
SagarRajput-7
ff2a3bc4b0 fix: [SIG-577]: QB functions - states not clearing (#4810) 2024-04-04 16:45:15 +05:30
Vikrant Gupta
33383a4503 fix: alerts tag popup vibrating (#4812) 2024-04-04 13:58:26 +05:30
CheetoDa
f05b94c01e Merge pull request #4799 from SigNoz/java-instructions-update
chore: fixed java instructions
2024-04-04 13:14:18 +05:30
CheetoDa
fd632f9952 Merge pull request #4798 from SigNoz/otel-version-change
feat: updated otel version number
2024-04-04 13:14:04 +05:30
CheetoDa
fd84d7b492 Merge pull request #4797 from SigNoz/docker-instructions
Docker instructions
2024-04-04 13:13:48 +05:30
Vikrant Gupta
e4808e585a fix: app content overlapping sidenav (#4811) 2024-04-04 12:46:33 +05:30
CheetoDa
5cfeb56f9c Created a variable for OTel version 2024-04-04 12:13:19 +05:30
CheetoDa
b947f823d7 chore: minor fix 2024-04-04 12:02:52 +05:30
CheetoDa
1520c1c57d Merge branch 'develop' into otel-version-change 2024-04-04 12:00:09 +05:30
CheetoDa
f8477981d8 chore: fixed review comments 2024-04-04 11:53:46 +05:30
Vishal Sharma
9b1d596816 chore: update events (#4808)
* chore: update events

* Update ee/query-service/app/server.go

Co-authored-by: Prashant Shahi <prashant@signoz.io>

---------

Co-authored-by: Prashant Shahi <prashant@signoz.io>
2024-04-04 11:31:27 +05:30
Yunus M
6a4aa9a956 QB - Logs - Enable TimeShift function (#4792)
* feat: qb - logs - enable time shift function

* feat: qb - logs - enable time shift function

* feat: show functions for logs in v3 version too
2024-04-04 11:05:58 +05:30
Nityananda Gohain
a7b0ef55ad fix: querier v2 synced and tablePanel result processor updated (#4807) 2024-04-03 17:52:45 +05:30
Srikanth Chekuri
87534b6fb6 fix: incorrect error rate query (#4805) 2024-04-03 16:42:00 +05:30
Vikrant Gupta
c76cef47ba fix: remove integrations page view and add event for the same (#4802) 2024-04-03 12:31:38 +05:30
CheetoDa
3276dfa03e chore: fixed java instructions 2024-04-03 04:42:19 +05:30
CheetoDa
1a14cc305c feat: updated otel version number 2024-04-03 04:28:15 +05:30
CheetoDa
0c7e63d735 chore: saved unsaved files 2024-04-03 04:08:46 +05:30
CheetoDa
eb74cb4c5e feat: docker completed 2024-04-03 04:03:00 +05:30
hulk
a47d3289d0 fix: typo in the log message (#4769) 2024-04-02 20:39:05 +05:30
Yunus M
8ad827130e feat: docked sidebar (#4794)
* feat: docked sidebar

* feat: update styles
2024-04-02 19:43:03 +05:30
CheetoDa
93bdfd3d83 chore: merged latest develop 2024-04-02 18:19:20 +05:30
CheetoDa
22d8889a07 feat: added docker instructions 2024-04-02 18:05:39 +05:30
Vikrant Gupta
7c93944d40 fix: chunk load webpack error (#4795) 2024-04-02 17:55:36 +05:30
Rajat Dabade
ec9dbb6853 Dashboard Clean up and list view improvement. (#4675)
* refactor: initial setup

* refactor: created panelWrapper to separate panel data

* fix: type error

* fix: the dimension issue for graphs

* refactor: done with table value uplot panels

* refactor: done with logs panel component

* refactor: updated props for log panel component

* fix: query range duplicate issue for logs

* refactor: trace list view done

* fix: full view support

* refactor: done with edit mode for panels

* refactor: type and props

* refactor: reduce an extra api call on edit for list view

* refactor: done with full graph visibility handler

* refactor: removed commented code

* refactor: removed commented code

* fix: build failure

* refactor: updated service layer graphs

* refactor: updated top level oparation query key

* refactor: added drag select

* refactor: done with drag select in chart

* refactor: code cleanup

* refactor: legend should not need stage and run query
2024-04-02 16:40:41 +05:30
Vikrant Gupta
7a7d814288 fix: sidenav items overlapping in small screens (#4789) 2024-04-02 12:38:10 +05:30
Vikrant Gupta
3babce3ecf fix: added dashboard and QB shortcuts to the sidenav (#4791) 2024-04-02 11:31:42 +05:30
Yunus M
1610b95b84 feat: onboarding flow - enable users to submit request for a new data… (#4786)
* feat: onboarding flow - enable users to submit request for a new data source , environment

* chore: request data source to be available for all modules

* chore: remove hardcoded value
2024-04-01 19:09:16 +05:30
Vishal Sharma
8c02f8ec31 chore: rate limit param (#4785) 2024-04-01 15:06:38 +05:30
Nityananda Gohain
5e0e9da6c4 fix: hotfix bug in enhance query (#4783) 2024-04-01 14:51:40 +05:30
Vikrant Gupta
51abe71421 fix: do not move to next step if env not selected in onboarding (#4784) 2024-04-01 13:56:59 +05:30
Vikrant Gupta
00d74bfebb feat: add integrations to the side-nav for cloud users (#4756)
* feat: add integrations to the side-nav for cloud users

* feat: change the route from integrations/installed to /integrations

* feat: light mode table color

* feat: increase the width of the integrations panel by 25 percent

* feat: added telemetry constants and page view

* feat: added telemetry events for integrations

* feat: address review comments
2024-04-01 12:40:15 +05:30
Raj Kamal Singh
39e0ef68ca chore: integration instructions: add typical log file locations on macOS (#4779) 2024-04-01 12:06:08 +05:30
Ankit Nayan
cff20f88cd merging main 2024-03-30 18:30:46 +01:00
Prashant Shahi
4fbb71484d Merge pull request #4755 from SigNoz/release/v0.42.0
Release/v0.42.0
2024-03-27 23:40:08 +05:45
Prashant Shahi
f8e8132b58 chore(signoz): 📌 pin versions: SigNoz 0.42.0, SigNoz OtelCollector 0.88.17
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-03-27 23:23:15 +05:45
Prashant Shahi
a1dd170641 Merge branch 'main' into release/v0.42.0 2024-03-27 23:17:30 +05:45
Prashant Shahi
fe2ddf9d60 Merge branch 'develop' into main-public 2024-03-27 23:15:57 +05:45
Prashant Shahi
dfc99a7756 Revert "Revert "Explorer Toolbar maximised and minimised (#4656)" (#4705)"
This reverts commit c04d0e9419.
2024-03-27 23:15:40 +05:45
Prashant Shahi
c2556facc2 Merge branch 'main' into release/v0.42.0 2024-03-27 22:49:48 +05:45
Prashant Shahi
2a7ad596a1 Merge pull request #4707 from SigNoz/release/v0.41.1
chore(pre-release): 📌 pin versions: SigNoz 0.41.1
2024-03-15 02:19:23 +05:30
Prashant Shahi
6c455ab5ce Merge branch 'main' into release/v0.41.1 2024-03-15 02:11:34 +05:30
Prashant Shahi
7c062163a1 chore(release): 📌 pin versions: SigNoz 0.41.1
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-03-15 02:24:09 +05:45
Prashant Shahi
d6a256247c Merge pull request #4706 from SigNoz/release/v0.41.1
Release/v0.41.1
2024-03-15 02:00:37 +05:30
Prashant Shahi
0e2c699518 Merge branch 'main' into release/v0.41.1 2024-03-15 01:46:02 +05:30
Vikrant Gupta
c04d0e9419 Revert "Explorer Toolbar maximised and minimised (#4656)" (#4705)
This reverts commit aadb962b6c.

(cherry picked from commit cf22039562)
2024-03-15 01:57:03 +05:45
Prashant Shahi
d0feff00a7 Merge pull request #4690 from SigNoz/release/v0.41.0
Release/v0.41.0
2024-03-14 00:50:36 +05:30
Prashant Shahi
6c2a3d5d43 chore(signoz): 📌 pin versions: SigNoz 0.41.0, SigNoz OtelCollector 0.88.15
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-03-14 00:23:51 +05:45
Prashant Shahi
914b035b3f Merge branch 'main' into release/v0.41.0 2024-03-14 00:23:02 +05:45
Prashant Shahi
71c4fcc382 Merge pull request #4616 from SigNoz/release/v0.40.0
Release/v0.40.0
2024-02-28 19:46:06 +05:30
Prashant Shahi
9af1c2320b chore(signoz): 📌 pin versions: SigNoz 0.40.0, SigNoz OtelCollector 0.88.14
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-02-28 19:13:23 +05:45
Prashant Shahi
cdabf9060e Merge branch 'main' into release/v0.40.0 2024-02-28 19:11:34 +05:45
Prashant Shahi
f069ecdb76 Merge pull request #4582 from SigNoz/release/v0.39.1
Release/v0.39.1
2024-02-21 17:53:50 +05:30
Prashant Shahi
493aef0241 chore(signoz): 📌 pin versions: SigNoz 0.39.1, SigNoz OtelCollector 0.88.13
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-02-21 16:22:48 +05:45
510 changed files with 30475 additions and 4199 deletions

View File

@@ -0,0 +1,31 @@
name: Jest Coverage - changed files
on:
pull_request:
branches: develop
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: "refs/heads/develop"
token: ${{ secrets.GITHUB_TOKEN }} # Provide the GitHub token for authentication
- name: Fetch branch
run: git fetch origin ${{ github.event.pull_request.head.ref }}
- run: |
git checkout ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: cd frontend && npm install -g yarn && yarn
- name: npm run test:changedsince
run: cd frontend && npm run i18n:generate-hash && npm run test:changedsince

View File

@@ -22,7 +22,7 @@ x-clickhouse-defaults: &clickhouse-defaults
"wget",
"--spider",
"-q",
"localhost:8123/ping"
"0.0.0.0:8123/ping"
]
interval: 30s
timeout: 5s
@@ -146,7 +146,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.39.0
image: signoz/query-service:0.45.0
command:
[
"-config=/root/config/prometheus.yml",
@@ -186,7 +186,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:0.39.0
image: signoz/frontend:0.45.0
deploy:
restart_policy:
condition: on-failure
@@ -199,7 +199,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:0.88.12
image: signoz/signoz-otel-collector:0.88.22
command:
[
"--config=/etc/otel-collector-config.yaml",
@@ -237,7 +237,7 @@ services:
- query-service
otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.88.12
image: signoz/signoz-schema-migrator:0.88.22
deploy:
restart_policy:
condition: on-failure

View File

@@ -111,18 +111,18 @@ processors:
exporters:
clickhousetraces:
datasource: tcp://clickhouse:9000/?database=signoz_traces
datasource: tcp://clickhouse:9000/signoz_traces
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
endpoint: tcp://clickhouse:9000/signoz_metrics
resource_to_telemetry_conversion:
enabled: true
clickhousemetricswrite/prometheus:
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
endpoint: tcp://clickhouse:9000/signoz_metrics
# logging: {}
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/
dsn: tcp://clickhouse:9000/signoz_logs
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
timeout: 10s
extensions:

View File

@@ -22,4 +22,4 @@ rule_files:
scrape_configs: []
remote_read:
- url: tcp://clickhouse:9000/?database=signoz_metrics
- url: tcp://clickhouse:9000/signoz_metrics

View File

@@ -46,7 +46,7 @@ services:
"wget",
"--spider",
"-q",
"localhost:8123/ping"
"0.0.0.0:8123/ping"
]
interval: 30s
timeout: 5s
@@ -66,7 +66,7 @@ services:
- --storage.path=/data
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.12}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.22}
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.12
image: signoz/signoz-otel-collector:0.88.22
command:
[
"--config=/etc/otel-collector-config.yaml",

View File

@@ -21,7 +21,7 @@ x-clickhouse-defaults: &clickhouse-defaults
"wget",
"--spider",
"-q",
"localhost:8123/ping"
"0.0.0.0:8123/ping"
]
interval: 30s
timeout: 5s
@@ -164,7 +164,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.39.0}
image: signoz/query-service:${DOCKER_TAG:-0.45.0}
container_name: signoz-query-service
command:
[
@@ -203,7 +203,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.39.0}
image: signoz/frontend:${DOCKER_TAG:-0.45.0}
container_name: signoz-frontend
restart: on-failure
depends_on:
@@ -215,7 +215,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.12}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.22}
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.12}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.22}
container_name: signoz-otel-collector
command:
[

View File

@@ -0,0 +1,64 @@
<clickhouse>
<logger>
<!-- Possible levels [1]:
- none (turns off logging)
- fatal
- critical
- error
- warning
- notice
- information
- debug
- trace
[1]: https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/Logger.h#L105-L114
-->
<level>information</level>
<log>/var/log/clickhouse-keeper/clickhouse-keeper.log</log>
<errorlog>/var/log/clickhouse-keeper/clickhouse-keeper.err.log</errorlog>
<!-- Rotation policy
See https://github.com/pocoproject/poco/blob/poco-1.9.4-release/Foundation/include/Poco/FileChannel.h#L54-L85
-->
<size>1000M</size>
<count>10</count>
<!-- <console>1</console> --> <!-- Default behavior is autodetection (log to console if not daemon mode and is tty) -->
</logger>
<listen_host>0.0.0.0</listen_host>
<max_connections>4096</max_connections>
<keeper_server>
<tcp_port>9181</tcp_port>
<!-- Must be unique among all keeper serves -->
<server_id>1</server_id>
<log_storage_path>/var/lib/clickhouse/coordination/logs</log_storage_path>
<snapshot_storage_path>/var/lib/clickhouse/coordination/snapshots</snapshot_storage_path>
<coordination_settings>
<operation_timeout_ms>10000</operation_timeout_ms>
<min_session_timeout_ms>10000</min_session_timeout_ms>
<session_timeout_ms>100000</session_timeout_ms>
<raft_logs_level>information</raft_logs_level>
<compress_logs>false</compress_logs>
<!-- All settings listed in https://github.com/ClickHouse/ClickHouse/blob/master/src/Coordination/CoordinationSettings.h -->
</coordination_settings>
<!-- enable sanity hostname checks for cluster configuration (e.g. if localhost is used with remote endpoints) -->
<hostname_checks_enabled>true</hostname_checks_enabled>
<raft_configuration>
<server>
<id>1</id>
<!-- Internal port and hostname -->
<hostname>clickhouses-keeper-1</hostname>
<port>9234</port>
</server>
<!-- Add more servers here -->
</raft_configuration>
</keeper_server>
</clickhouse>

View File

@@ -122,21 +122,20 @@ extensions:
exporters:
clickhousetraces:
datasource: tcp://clickhouse:9000/?database=signoz_traces
datasource: tcp://clickhouse:9000/signoz_traces
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
low_cardinal_exception_grouping: ${LOW_CARDINAL_EXCEPTION_GROUPING}
clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
endpoint: tcp://clickhouse:9000/signoz_metrics
resource_to_telemetry_conversion:
enabled: true
clickhousemetricswrite/prometheus:
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
# logging: {}
endpoint: tcp://clickhouse:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://clickhouse:9000/
dsn: tcp://clickhouse:9000/signoz_logs
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
timeout: 10s
# logging: {}
service:
telemetry:

View File

@@ -22,4 +22,4 @@ rule_files:
scrape_configs: []
remote_read:
- url: tcp://clickhouse:9000/?database=signoz_metrics
- url: tcp://clickhouse:9000/signoz_metrics

View File

@@ -152,7 +152,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
router.HandleFunc("/api/v1/register", am.OpenAccess(ah.registerUser)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/login", am.OpenAccess(ah.loginUser)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/traces/{traceId}", am.ViewAccess(ah.searchTraces)).Methods(http.MethodGet)
router.HandleFunc("/api/v2/metrics/query_range", am.ViewAccess(ah.queryRangeMetricsV2)).Methods(http.MethodPost)
// PAT APIs
router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.createPAT)).Methods(http.MethodPost)

View File

@@ -1,236 +0,0 @@
package api
import (
"bytes"
"fmt"
"net/http"
"sync"
"text/template"
"time"
"go.signoz.io/signoz/pkg/query-service/app/metrics"
"go.signoz.io/signoz/pkg/query-service/app/parser"
"go.signoz.io/signoz/pkg/query-service/constants"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
querytemplate "go.signoz.io/signoz/pkg/query-service/utils/queryTemplate"
"go.uber.org/zap"
)
func (ah *APIHandler) queryRangeMetricsV2(w http.ResponseWriter, r *http.Request) {
if !ah.CheckFeature(basemodel.CustomMetricsFunction) {
zap.L().Info("CustomMetricsFunction feature is not enabled in this plan")
ah.APIHandler.QueryRangeMetricsV2(w, r)
return
}
metricsQueryRangeParams, apiErrorObj := parser.ParseMetricQueryRangeParams(r)
if apiErrorObj != nil {
zap.L().Error("Error in parsing metric query params", zap.Error(apiErrorObj.Err))
RespondError(w, apiErrorObj, nil)
return
}
// prometheus instant query needs same timestamp
if metricsQueryRangeParams.CompositeMetricQuery.PanelType == basemodel.QUERY_VALUE &&
metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.PROM {
metricsQueryRangeParams.Start = metricsQueryRangeParams.End
}
// round up the end to nearest multiple
if metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.QUERY_BUILDER {
end := (metricsQueryRangeParams.End) / 1000
step := metricsQueryRangeParams.Step
metricsQueryRangeParams.End = (end / step * step) * 1000
}
type channelResult struct {
Series []*basemodel.Series
TableName string
Err error
Name string
Query string
}
execClickHouseQueries := func(queries map[string]string) ([]*basemodel.Series, []string, error, map[string]string) {
var seriesList []*basemodel.Series
var tableName []string
ch := make(chan channelResult, len(queries))
var wg sync.WaitGroup
for name, query := range queries {
wg.Add(1)
go func(name, query string) {
defer wg.Done()
seriesList, tableName, err := ah.opts.DataConnector.GetMetricResultEE(r.Context(), query)
for _, series := range seriesList {
series.QueryName = name
}
if err != nil {
ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err), Name: name, Query: query}
return
}
ch <- channelResult{Series: seriesList, TableName: tableName}
}(name, query)
}
wg.Wait()
close(ch)
var errs []error
errQuriesByName := make(map[string]string)
// read values from the channel
for r := range ch {
if r.Err != nil {
errs = append(errs, r.Err)
errQuriesByName[r.Name] = r.Query
continue
}
seriesList = append(seriesList, r.Series...)
tableName = append(tableName, r.TableName)
}
if len(errs) != 0 {
return nil, nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")), errQuriesByName
}
return seriesList, tableName, nil, nil
}
execPromQueries := func(metricsQueryRangeParams *basemodel.QueryRangeParamsV2) ([]*basemodel.Series, error, map[string]string) {
var seriesList []*basemodel.Series
ch := make(chan channelResult, len(metricsQueryRangeParams.CompositeMetricQuery.PromQueries))
var wg sync.WaitGroup
for name, query := range metricsQueryRangeParams.CompositeMetricQuery.PromQueries {
if query.Disabled {
continue
}
wg.Add(1)
go func(name string, query *basemodel.PromQuery) {
var seriesList []*basemodel.Series
defer wg.Done()
tmpl := template.New("promql-query")
tmpl, tmplErr := tmpl.Parse(query.Query)
if tmplErr != nil {
ch <- channelResult{Err: fmt.Errorf("error in parsing query-%s: %v", name, tmplErr), Name: name, Query: query.Query}
return
}
var queryBuf bytes.Buffer
tmplErr = tmpl.Execute(&queryBuf, metricsQueryRangeParams.Variables)
if tmplErr != nil {
ch <- channelResult{Err: fmt.Errorf("error in parsing query-%s: %v", name, tmplErr), Name: name, Query: query.Query}
return
}
query.Query = queryBuf.String()
queryModel := basemodel.QueryRangeParams{
Start: time.UnixMilli(metricsQueryRangeParams.Start),
End: time.UnixMilli(metricsQueryRangeParams.End),
Step: time.Duration(metricsQueryRangeParams.Step * int64(time.Second)),
Query: query.Query,
}
promResult, _, err := ah.opts.DataConnector.GetQueryRangeResult(r.Context(), &queryModel)
if err != nil {
ch <- channelResult{Err: fmt.Errorf("error in query-%s: %v", name, err), Name: name, Query: query.Query}
return
}
matrix, _ := promResult.Matrix()
for _, v := range matrix {
var s basemodel.Series
s.QueryName = name
s.Labels = v.Metric.Copy().Map()
for _, p := range v.Floats {
s.Points = append(s.Points, basemodel.MetricPoint{Timestamp: p.T, Value: p.F})
}
seriesList = append(seriesList, &s)
}
ch <- channelResult{Series: seriesList}
}(name, query)
}
wg.Wait()
close(ch)
var errs []error
errQuriesByName := make(map[string]string)
// read values from the channel
for r := range ch {
if r.Err != nil {
errs = append(errs, r.Err)
errQuriesByName[r.Name] = r.Query
continue
}
seriesList = append(seriesList, r.Series...)
}
if len(errs) != 0 {
return nil, fmt.Errorf("encountered multiple errors: %s", metrics.FormatErrs(errs, "\n")), errQuriesByName
}
return seriesList, nil, nil
}
var seriesList []*basemodel.Series
var tableName []string
var err error
var errQuriesByName map[string]string
switch metricsQueryRangeParams.CompositeMetricQuery.QueryType {
case basemodel.QUERY_BUILDER:
runQueries := metrics.PrepareBuilderMetricQueries(metricsQueryRangeParams, constants.SIGNOZ_TIMESERIES_TABLENAME)
if runQueries.Err != nil {
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: runQueries.Err}, nil)
return
}
seriesList, tableName, err, errQuriesByName = execClickHouseQueries(runQueries.Queries)
case basemodel.CLICKHOUSE:
queries := make(map[string]string)
for name, chQuery := range metricsQueryRangeParams.CompositeMetricQuery.ClickHouseQueries {
if chQuery.Disabled {
continue
}
tmpl := template.New("clickhouse-query")
tmpl, err := tmpl.Parse(chQuery.Query)
if err != nil {
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}, nil)
return
}
var query bytes.Buffer
// replace go template variables
querytemplate.AssignReservedVars(metricsQueryRangeParams)
err = tmpl.Execute(&query, metricsQueryRangeParams.Variables)
if err != nil {
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}, nil)
return
}
queries[name] = query.String()
}
seriesList, tableName, err, errQuriesByName = execClickHouseQueries(queries)
case basemodel.PROM:
seriesList, err, errQuriesByName = execPromQueries(metricsQueryRangeParams)
default:
err = fmt.Errorf("invalid query type")
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}, errQuriesByName)
return
}
if err != nil {
apiErrObj := &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: err}
RespondError(w, apiErrObj, errQuriesByName)
return
}
if metricsQueryRangeParams.CompositeMetricQuery.PanelType == basemodel.QUERY_VALUE &&
len(seriesList) > 1 &&
(metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.QUERY_BUILDER ||
metricsQueryRangeParams.CompositeMetricQuery.QueryType == basemodel.CLICKHOUSE) {
RespondError(w, &basemodel.ApiError{Typ: basemodel.ErrorBadData, Err: fmt.Errorf("invalid: query resulted in more than one series for value type")}, nil)
return
}
type ResponseFormat struct {
ResultType string `json:"resultType"`
Result []*basemodel.Series `json:"result"`
TableName []string `json:"tableName"`
}
resp := ResponseFormat{ResultType: "matrix", Result: seriesList, TableName: tableName}
ah.Respond(w, resp)
}

View File

@@ -10,6 +10,7 @@ import (
"net/http"
_ "net/http/pprof" // http profiler
"os"
"regexp"
"time"
"github.com/gorilla/handlers"
@@ -328,7 +329,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
r.Use(loggingMiddleware)
apiHandler.RegisterRoutes(r, am)
apiHandler.RegisterMetricsRoutes(r, am)
apiHandler.RegisterLogsRoutes(r, am)
apiHandler.RegisterIntegrationRoutes(r, am)
apiHandler.RegisterQueryRangeV3Routes(r, am)
@@ -393,13 +393,14 @@ func (lrw *loggingResponseWriter) Flush() {
lrw.ResponseWriter.(http.Flusher).Flush()
}
func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface{}, bool) {
pathToExtractBodyFrom := "/api/v3/query_range"
func extractQueryRangeData(path string, r *http.Request) (map[string]interface{}, bool) {
pathToExtractBodyFromV3 := "/api/v3/query_range"
pathToExtractBodyFromV4 := "/api/v4/query_range"
data := map[string]interface{}{}
var postData *v3.QueryRangeParamsV3
if path == pathToExtractBodyFrom && (r.Method == "POST") {
if (r.Method == "POST") && ((path == pathToExtractBodyFromV3) || (path == pathToExtractBodyFromV4)) {
if r.Body != nil {
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
@@ -417,6 +418,25 @@ func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface
return nil, false
}
referrer := r.Header.Get("Referer")
dashboardMatched, err := regexp.MatchString(`/dashboard/[a-zA-Z0-9\-]+/(new|edit)(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the referrer", zap.Error(err))
}
alertMatched, err := regexp.MatchString(`/alerts/(new|edit)(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the alert: ", zap.Error(err))
}
logsExplorerMatched, err := regexp.MatchString(`/logs/logs-explorer(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the logs explorer: ", zap.Error(err))
}
traceExplorerMatched, err := regexp.MatchString(`/traces-explorer(?:\?.*)?$`, referrer)
if err != nil {
zap.L().Error("error while matching the trace explorer: ", zap.Error(err))
}
signozMetricsUsed := false
signozLogsUsed := false
signozTracesUsed := false
@@ -445,6 +465,20 @@ func extractQueryRangeV3Data(path string, r *http.Request) (map[string]interface
data["tracesUsed"] = signozTracesUsed
userEmail, err := baseauth.GetEmailFromJwt(r.Context())
if err == nil {
// switch case to set data["screen"] based on the referrer
switch {
case dashboardMatched:
data["screen"] = "panel"
case alertMatched:
data["screen"] = "alert"
case logsExplorerMatched:
data["screen"] = "logs-explorer"
case traceExplorerMatched:
data["screen"] = "traces-explorer"
default:
data["screen"] = "unknown"
return data, true
}
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_QUERY_RANGE_API, data, userEmail, true, false)
}
}
@@ -472,7 +506,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
route := mux.CurrentRoute(r)
path, _ := route.GetPathTemplate()
queryRangeV3data, metadataExists := extractQueryRangeV3Data(path, r)
queryRangeData, metadataExists := extractQueryRangeData(path, r)
getActiveLogs(path, r)
lrw := NewLoggingResponseWriter(w)
@@ -480,7 +514,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
data := map[string]interface{}{"path": path, "statusCode": lrw.statusCode}
if metadataExists {
for key, value := range queryRangeV3data {
for key, value := range queryRangeData {
data[key] = value
}
}

3
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Sentry Config File
.env.sentry-build-plugin

View File

@@ -1,4 +1,4 @@
FROM nginx:1.25.2-alpine
FROM nginx:1.26-alpine
# Add Maintainer Info
LABEL maintainer="signoz"

View File

@@ -4,6 +4,7 @@ const config: Config.InitialOptions = {
clearMocks: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'cobertura', 'html', 'json-summary'],
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
modulePathIgnorePatterns: ['dist'],
moduleNameMapper: {
@@ -35,6 +36,14 @@ const config: Config.InitialOptions = {
browsers: ['chromium', 'firefox', 'webkit'],
},
},
coverageThreshold: {
global: {
statements: 80,
branches: 65,
functions: 80,
lines: 80,
},
},
};
export default config;

View File

@@ -21,7 +21,9 @@
"playwright:codegen:local": "playwright codegen http://localhost:3301",
"playwright:codegen:local:auth": "yarn playwright:codegen:local --load-storage=tests/auth.json",
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
"commitlint": "commitlint --edit $1"
"commitlint": "commitlint --edit $1",
"test": "jest --coverage",
"test:changedsince": "jest --changedSince=develop --coverage --silent"
},
"engines": {
"node": ">=16.15.0"
@@ -41,14 +43,17 @@
"@radix-ui/react-tabs": "1.0.4",
"@radix-ui/react-tooltip": "1.0.7",
"@sentry/react": "7.102.1",
"@sentry/webpack-plugin": "2.14.2",
"@sentry/webpack-plugin": "2.16.0",
"@signozhq/design-tokens": "0.0.8",
"@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0",
"@visx/shape": "3.5.0",
"@visx/tooltip": "3.3.0",
"@xstate/react": "^3.0.0",
"ansi-to-html": "0.7.2",
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
"axios": "1.6.2",
"axios": "1.6.4",
"babel-eslint": "^10.1.0",
"babel-jest": "^29.6.4",
"babel-loader": "9.1.3",
@@ -121,6 +126,7 @@
"web-vitals": "^0.2.4",
"webpack": "5.88.2",
"webpack-dev-server": "^4.15.1",
"webpack-retry-chunk-load-plugin": "3.1.1",
"xstate": "^4.31.0"
},
"browserslist": {

View File

@@ -15,6 +15,7 @@
"button_test_channel": "Test",
"button_return": "Back",
"field_channel_name": "Name",
"field_send_resolved": "Send resolved alerts",
"field_channel_type": "Type",
"field_webhook_url": "Webhook URL",
"field_slack_recipient": "Recipient",

View File

@@ -16,6 +16,7 @@
"new_dashboard_title": "Sample Title",
"layout_saved_successfully": "Layout saved successfully",
"add_panel": "Add Panel",
"add_row": "Add Row",
"save_layout": "Save Layout",
"variable_updated_successfully": "Variable updated successfully",
"error_while_updating_variable": "Error while updating variable",

View File

@@ -15,6 +15,7 @@
"button_test_channel": "Test",
"button_return": "Back",
"field_channel_name": "Name",
"field_send_resolved": "Send resolved alerts",
"field_channel_type": "Type",
"field_webhook_url": "Webhook URL",
"field_slack_recipient": "Recipient",

View File

@@ -16,6 +16,7 @@
"new_dashboard_title": "Sample Title",
"layout_saved_successfully": "Layout saved successfully",
"add_panel": "Add Panel",
"add_row": "Add Row",
"save_layout": "Save Layout",
"full_view": "Full Screen View",
"variable_updated_successfully": "Variable updated successfully",

View File

@@ -48,5 +48,5 @@
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
"DEFAULT": "Open source Observability Platform | SigNoz",
"SHORTCUTS": "SigNoz | Shortcuts",
"INTEGRATIONS_INSTALLED": "SigNoz | Integrations"
"INTEGRATIONS": "SigNoz | Integrations"
}

View File

@@ -147,7 +147,11 @@ function App(): JSX.Element {
}
}
if (isOnBasicPlan || (isLoggedInState && role && role !== 'ADMIN')) {
if (
isOnBasicPlan ||
(isLoggedInState && role && role !== 'ADMIN') ||
!(isCloudUserVal || isEECloudUser())
) {
const newRoutes = routes.filter((route) => route?.path !== ROUTES.BILLING);
setRoutes(newRoutes);
}

View File

@@ -197,11 +197,3 @@ export const InstalledIntegrations = Loadable(
/* webpackChunkName: "InstalledIntegrations" */ 'pages/IntegrationsModulePage'
),
);
export const IntegrationsMarketPlace = Loadable(
// eslint-disable-next-line sonarjs/no-identical-functions
() =>
import(
/* webpackChunkName: "IntegrationsMarketPlace" */ 'pages/IntegrationsModulePage'
),
);

View File

@@ -15,7 +15,6 @@ import {
ErrorDetails,
IngestionSettings,
InstalledIntegrations,
IntegrationsMarketPlace,
LicensePage,
ListAllALertsPage,
LiveLogs,
@@ -338,18 +337,11 @@ const routes: AppRoutes[] = [
key: 'SHORTCUTS',
},
{
path: ROUTES.INTEGRATIONS_INSTALLED,
path: ROUTES.INTEGRATIONS,
exact: true,
component: InstalledIntegrations,
isPrivate: true,
key: 'INTEGRATIONS_INSTALLED',
},
{
path: ROUTES.INTEGRATIONS_MARKETPLACE,
exact: true,
component: IntegrationsMarketPlace,
isPrivate: true,
key: 'INTEGRATIONS_MARKETPLACE',
key: 'INTEGRATIONS',
},
];

View File

@@ -12,7 +12,7 @@ const create = async (
name: props.name,
email_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
to: props.to,
html: props.html,
headers: props.headers,

View File

@@ -12,7 +12,7 @@ const create = async (
name: props.name,
msteams_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
webhook_url: props.webhook_url,
title: props.title,
text: props.text,

View File

@@ -12,7 +12,7 @@ const create = async (
name: props.name,
pagerduty_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
routing_key: props.routing_key,
client: props.client,
client_url: props.client_url,

View File

@@ -12,7 +12,7 @@ const create = async (
name: props.name,
slack_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
api_url: props.api_url,
channel: props.channel,
title: props.title,

View File

@@ -30,7 +30,7 @@ const create = async (
name: props.name,
webhook_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
url: props.api_url,
http_config: httpConfig,
},

View File

@@ -12,7 +12,7 @@ const editEmail = async (
name: props.name,
email_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
to: props.to,
html: props.html,
headers: props.headers,

View File

@@ -12,7 +12,7 @@ const editMsTeams = async (
name: props.name,
msteams_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
webhook_url: props.webhook_url,
title: props.title,
text: props.text,

View File

@@ -12,7 +12,7 @@ const editOpsgenie = async (
name: props.name,
opsgenie_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
api_key: props.api_key,
description: props.description,
priority: props.priority,

View File

@@ -12,7 +12,7 @@ const editPager = async (
name: props.name,
pagerduty_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
routing_key: props.routing_key,
client: props.client,
client_url: props.client_url,

View File

@@ -12,7 +12,7 @@ const editSlack = async (
name: props.name,
slack_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
api_url: props.api_url,
channel: props.channel,
title: props.title,

View File

@@ -29,7 +29,7 @@ const editWebhook = async (
name: props.name,
webhook_configs: [
{
send_resolved: true,
send_resolved: props.send_resolved,
url: props.api_url,
http_config: httpConfig,
},

View File

@@ -0,0 +1,28 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { EventSuccessPayloadProps } from 'types/api/events/types';
const logEvent = async (
eventName: string,
attributes: Record<string, unknown>,
): Promise<SuccessResponse<EventSuccessPayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/event', {
eventName,
attributes,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default logEvent;

View File

@@ -1,4 +1,4 @@
import axios from 'api';
import { ApiV4Instance } from 'api';
import { AxiosResponse } from 'axios';
import { MetricMetaProps } from 'types/api/metrics/getApDex';
@@ -6,4 +6,6 @@ export const getMetricMeta = (
metricName: string,
servicename: string,
): Promise<AxiosResponse<MetricMetaProps>> =>
axios.get(`/metric_meta?metricName=${metricName}&serviceName=${servicename}`);
ApiV4Instance.get(
`/metric/metric_metadata?metricName=${metricName}&serviceName=${servicename}`,
);

View File

@@ -1,27 +0,0 @@
import { ApiV2Instance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MetricNameProps,
MetricNamesPayloadProps,
} from 'types/api/metrics/getMetricName';
export const getMetricName = async (
props: MetricNameProps,
): Promise<SuccessResponse<MetricNamesPayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/metrics/autocomplete/list?match=${props || ''}`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,6 +1,7 @@
import { ApiV2Instance as axios } from 'api';
import { ApiV3Instance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
TagKeyProps,
@@ -8,15 +9,19 @@ import {
TagValueProps,
TagValuesPayloadProps,
} from 'types/api/metrics/getResourceAttributes';
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
export const getResourceAttributesTagKeys = async (
props: TagKeyProps,
): Promise<SuccessResponse<TagKeysPayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/metrics/autocomplete/tagKey?metricName=${props.metricName}${
props.match ? `&match=${props.match}` : ''
}`,
`/autocomplete/attribute_keys?${createQueryParams({
aggregateOperator: MetricAggregateOperator.RATE,
searchText: props.match,
dataSource: DataSource.METRICS,
aggregateAttribute: props.metricName,
})}`,
);
return {
@@ -35,7 +40,13 @@ export const getResourceAttributesTagValues = async (
): Promise<SuccessResponse<TagValuesPayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/metrics/autocomplete/tagValue?metricName=${props.metricName}&tagKey=${props.tagKey}`,
`/autocomplete/attribute_values?${createQueryParams({
aggregateOperator: MetricAggregateOperator.RATE,
dataSource: DataSource.METRICS,
aggregateAttribute: props.metricName,
attributeKey: props.tagKey,
searchText: '',
})}`,
);
return {

View File

@@ -1,9 +1,10 @@
import { ComponentType, lazy, LazyExoticComponent } from 'react';
import { lazyRetry } from 'utils/lazyWithRetries';
function Loadable(importPath: {
(): LoadableProps;
}): LazyExoticComponent<LazyComponent> {
return lazy(() => importPath());
return lazy(() => lazyRetry(() => importPath()));
}
type LazyComponent = ComponentType<Record<string, unknown>>;

View File

@@ -157,8 +157,8 @@ function ListLogView({
const timestampValue = useMemo(
() =>
typeof flattenLogData.timestamp === 'string'
? dayjs(flattenLogData.timestamp).format()
: dayjs(flattenLogData.timestamp / 1e6).format(),
? dayjs(flattenLogData.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(flattenLogData.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'),
[flattenLogData.timestamp],
);

View File

@@ -90,12 +90,12 @@ function RawLogView({
const text = useMemo(
() =>
typeof data.timestamp === 'string'
? `${dayjs(data.timestamp).format()} | ${attributesText} ${severityText} ${
data.body
}`
: `${dayjs(
data.timestamp / 1e6,
).format()} | ${attributesText} ${severityText} ${data.body}`,
? `${dayjs(data.timestamp).format(
'YYYY-MM-DD HH:mm:ss.SSS',
)} | ${attributesText} ${severityText} ${data.body}`
: `${dayjs(data.timestamp / 1e6).format(
'YYYY-MM-DD HH:mm:ss.SSS',
)} | ${attributesText} ${severityText} ${data.body}`,
[data.timestamp, data.body, severityText, attributesText],
);

View File

@@ -76,8 +76,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
render: (field, item): ColumnTypeRender<Record<string, unknown>> => {
const date =
typeof field === 'string'
? dayjs(field).format()
: dayjs(field / 1e6).format();
? dayjs(field).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(field / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
return {
children: (
<div className="table-timestamp">

View File

@@ -1,8 +1,9 @@
/* eslint-disable react/jsx-props-no-spreading */
import './DynamicColumnTable.syles.scss';
import { Button, Dropdown, MenuProps, Switch } from 'antd';
import { Button, Dropdown, Flex, MenuProps, Switch } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { SlidersHorizontal } from 'lucide-react';
import { memo, useEffect, useState } from 'react';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -20,6 +21,7 @@ function DynamicColumnTable({
columns,
dynamicColumns,
onDragColumn,
facingIssueBtn,
...restProps
}: DynamicColumnTableProps): JSX.Element {
const [columnsData, setColumnsData] = useState<ColumnsType | undefined>(
@@ -83,19 +85,22 @@ function DynamicColumnTable({
return (
<div className="DynamicColumnTable">
{dynamicColumns && (
<Dropdown
getPopupContainer={popupContainer}
menu={{ items }}
trigger={['click']}
>
<Button
className="dynamicColumnTable-button filter-btn"
size="middle"
icon={<SlidersHorizontal size={14} />}
/>
</Dropdown>
)}
<Flex justify="flex-end" align="center" gap={8}>
{facingIssueBtn && <FacingIssueBtn {...facingIssueBtn} />}
{dynamicColumns && (
<Dropdown
getPopupContainer={popupContainer}
menu={{ items }}
trigger={['click']}
>
<Button
className="dynamicColumnTable-button filter-btn"
size="middle"
icon={<SlidersHorizontal size={14} />}
/>
</Dropdown>
)}
</Flex>
<ResizeTable
columns={columnsData}

View File

@@ -2,6 +2,7 @@
import { TableProps } from 'antd';
import { ColumnsType } from 'antd/es/table';
import { ColumnGroupType, ColumnType } from 'antd/lib/table';
import { FacingIssueBtnProps } from 'components/facingIssueBtn/FacingIssueBtn';
import { TableDataSource } from './contants';
@@ -12,6 +13,7 @@ export interface DynamicColumnTableProps extends TableProps<any> {
tablesource: typeof TableDataSource[keyof typeof TableDataSource];
dynamicColumns: TableProps<any>['columns'];
onDragColumn?: (fromIndex: number, toIndex: number) => void;
facingIssueBtn?: FacingIssueBtnProps;
}
export type GetVisibleColumnsFunction = (

View File

@@ -1,10 +1,10 @@
import './LabelColumn.styles.scss';
import { Popover, Tag } from 'antd';
import { popupContainer } from 'utils/selectPopupContainer';
import { LabelColumnProps } from './TableRenderer.types';
import TagWithToolTip from './TagWithToolTip';
import { getLabelAndValueContent } from './utils';
function LabelColumn({ labels, value, color }: LabelColumnProps): JSX.Element {
const newLabels = labels.length > 3 ? labels.slice(0, 3) : labels;
@@ -19,19 +19,17 @@ function LabelColumn({ labels, value, color }: LabelColumnProps): JSX.Element {
)}
{remainingLabels.length > 0 && (
<Popover
getPopupContainer={popupContainer}
placement="bottomRight"
showArrow={false}
content={
<div>
{labels.map(
(label: string): JSX.Element => (
<TagWithToolTip
key={label}
label={label}
color={color}
value={value}
/>
<div key={label}>
<Tag className="label-column--tag" color={color}>
{getLabelAndValueContent(label, value && value[label])}
</Tag>
</div>
),
)}
</div>

View File

@@ -38,6 +38,16 @@ export const getLabelRenderingValue = (
return label;
};
export const getLabelAndValueContent = (
label: string,
value?: string,
): string => {
if (value) {
return `${label}: ${value}`;
}
return `${label}`;
};
interface GeneratorResizeTableColumnsProp<T> {
baseColumnOptions: ColumnsType<T>;
dynamicColumnOption: { key: string; columnOption: ColumnType<T> }[];

View File

@@ -0,0 +1,9 @@
.facing-issue-button {
color: var(--bg-amber-500);
border-color: var(--bg-amber-500);
.ant-btn:hover {
color: var(--bg-amber-400) !important;
border-color: var(--bg-amber-300) !important;
}
}

View File

@@ -0,0 +1,62 @@
import './FacingIssueBtn.style.scss';
import { Button, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { FeatureKeys } from 'constants/features';
import useFeatureFlags from 'hooks/useFeatureFlag';
import { defaultTo } from 'lodash-es';
import { HelpCircle } from 'lucide-react';
import { isCloudUser } from 'utils/app';
export interface FacingIssueBtnProps {
eventName: string;
attributes: Record<string, unknown>;
message?: string;
buttonText?: string;
className?: string;
onHoverText?: string;
}
function FacingIssueBtn({
attributes,
eventName,
message = '',
buttonText = '',
className = '',
onHoverText = '',
}: FacingIssueBtnProps): JSX.Element | null {
const handleFacingIssuesClick = (): void => {
logEvent(eventName, attributes);
if (window.Intercom) {
window.Intercom('showNewMessage', defaultTo(message, ''));
}
};
const isChatSupportEnabled = useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active;
const isCloudUserVal = isCloudUser();
return isCloudUserVal && isChatSupportEnabled ? ( // Note: we would need to move this condition to license based in future
<div className="facing-issue-button">
<Tooltip title={onHoverText} autoAdjustOverflow>
<Button
className={cx('periscope-btn', 'facing-issue-button', className)}
onClick={handleFacingIssuesClick}
icon={<HelpCircle size={14} />}
>
{buttonText || 'Facing issues?'}
</Button>
</Tooltip>
</div>
) : null;
}
FacingIssueBtn.defaultProps = {
message: '',
buttonText: '',
className: '',
onHoverText: '',
};
export default FacingIssueBtn;

View File

@@ -0,0 +1,57 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { AlertDef } from 'types/api/alerts/def';
import { Dashboard, DashboardData } from 'types/api/dashboard/getAll';
export const chartHelpMessage = (
selectedDashboard: Dashboard | undefined,
graphType: PANEL_TYPES,
): string => `
Hi Team,
I need help in creating this chart. Here are my dashboard details
Name: ${selectedDashboard?.data.title || ''}
Panel type: ${graphType}
Dashboard Id: ${selectedDashboard?.uuid || ''}
Thanks`;
export const dashboardHelpMessage = (
data: DashboardData | undefined,
selectedDashboard: Dashboard | undefined,
): string => `
Hi Team,
I need help with this dashboard. Here are my dashboard details
Name: ${data?.title || ''}
Dashboard Id: ${selectedDashboard?.uuid || ''}
Thanks`;
export const dashboardListMessage = `Hi Team,
I need help with dashboards.
Thanks`;
export const listAlertMessage = `Hi Team,
I need help with managing alerts.
Thanks`;
export const alertHelpMessage = (
alertDef: AlertDef,
ruleId: number,
): string => `
Hi Team,
I need help in configuring this alert. Here are my alert rule details
Name: ${alertDef?.alert || ''}
Alert Type: ${alertDef?.alertType || ''}
State: ${(alertDef as any)?.state || ''}
Alert Id: ${ruleId}
Thanks`;

View File

@@ -29,6 +29,7 @@ export const getComponentForPanelType = (
[PANEL_TYPES.LIST]:
dataSource === DataSource.LOGS ? LogsPanelComponent : TracesTableComponent,
[PANEL_TYPES.BAR]: Uplot,
[PANEL_TYPES.PIE]: null,
[PANEL_TYPES.EMPTY_WIDGET]: null,
};

View File

@@ -30,4 +30,5 @@ export enum QueryParams {
integration = 'integration',
pagination = 'pagination',
relativeTime = 'relativeTime',
alertType = 'alertType',
}

View File

@@ -285,9 +285,15 @@ export enum PANEL_TYPES {
LIST = 'list',
TRACE = 'trace',
BAR = 'bar',
PIE = 'pie',
EMPTY_WIDGET = 'EMPTY_WIDGET',
}
// eslint-disable-next-line @typescript-eslint/naming-convention
export enum PANEL_GROUP_TYPES {
ROW = 'row',
}
// eslint-disable-next-line @typescript-eslint/naming-convention
export enum ATTRIBUTE_TYPES {
SUM = 'Sum',

View File

@@ -2,7 +2,7 @@
import { QueryFunctionsTypes } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
export const queryFunctionOptions: SelectOption<string, string>[] = [
export const metricQueryFunctionOptions: SelectOption<string, string>[] = [
{
value: QueryFunctionsTypes.CUTOFF_MIN,
label: 'Cut Off Min',
@@ -65,6 +65,12 @@ export const queryFunctionOptions: SelectOption<string, string>[] = [
},
];
export const logsQueryFunctionOptions: SelectOption<string, string>[] = [
{
value: QueryFunctionsTypes.TIME_SHIFT,
label: 'Time Shift',
},
];
interface QueryFunctionConfigType {
[key: string]: {
showInput: boolean;

View File

@@ -51,9 +51,7 @@ const ROUTES = {
TRACES_SAVE_VIEWS: '/traces/saved-views',
WORKSPACE_LOCKED: '/workspace-locked',
SHORTCUTS: '/shortcuts',
INTEGRATIONS_BASE: '/integrations',
INTEGRATIONS_INSTALLED: '/integrations/installed',
INTEGRATIONS_MARKETPLACE: '/integrations/marketplace',
INTEGRATIONS: '/integrations',
} as const;
export default ROUTES;

View File

@@ -0,0 +1,3 @@
export enum SESSIONSTORAGE {
RETRY_LAZY_REFRESHED = 'retry-lazy-refreshed',
}

View File

@@ -9,9 +9,10 @@ export const DashboardShortcuts = {
export const DashboardShortcutsName = {
SaveChanges: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+s`,
DiscardChanges: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+d`,
};
export const DashboardShortcutsDescription = {
SaveChanges: 'Save Changes',
DiscardChanges: 'Discard Changes',
SaveChanges: 'Save Changes for panel',
DiscardChanges: 'Discard Changes for panel',
};

View File

@@ -13,5 +13,5 @@ export const QBShortcutsName = {
};
export const QBShortcutsDescription = {
StageAndRunQuery: 'Stage and Run the query',
StageAndRunQuery: 'Stage and Run the current query',
};

View File

@@ -4,8 +4,9 @@
width: 100%;
.app-content {
width: 100%;
width: calc(100% - 64px);
overflow: auto;
z-index: 0;
.content-container {
position: relative;
@@ -16,6 +17,12 @@
width: 100%;
}
}
&.docked {
.app-content {
width: calc(100% - 240px);
}
}
}
.isDarkMode {

View File

@@ -311,7 +311,13 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</div>
)}
<Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}>
<Flex
className={cx(
'app-layout',
isDarkMode ? 'darkMode' : 'lightMode',
!collapsed && !renderFullScreen ? 'docked' : '',
)}
>
{isToDisplayLayout && !renderFullScreen && (
<SideNav
licenseData={licenseData}

View File

@@ -1,4 +1,5 @@
.billing-container {
margin-bottom: 40px;
padding-top: 36px;
width: 65%;

View File

@@ -3,6 +3,7 @@ import Spinner from 'components/Spinner';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { lazy, Suspense, useMemo } from 'react';
import { ConfigProps } from 'types/api/dynamicConfigs/getDynamicConfigs';
import { lazyRetry } from 'utils/lazyWithRetries';
import ErrorLink from './ErrorLink';
import LinkContainer from './Link';
@@ -17,8 +18,9 @@ function HelpToolTip({ config }: HelpToolTipProps): JSX.Element {
const items = sortedConfig.map((item) => {
const iconName = `${isDarkMode ? item.darkIcon : item.lightIcon}`;
const Component = lazy(
() => import(`@ant-design/icons/es/icons/${iconName}.js`),
const Component = lazy(() =>
lazyRetry(() => import(`@ant-design/icons/es/icons/${iconName}.js`)),
);
return {
key: item.text + item.href,

View File

@@ -53,6 +53,7 @@ function CreateAlertChannels({
EmailChannel
>
>({
send_resolved: true,
text: `{{ range .Alerts -}}
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
@@ -119,7 +120,7 @@ function CreateAlertChannels({
api_url: selectedConfig?.api_url || '',
channel: selectedConfig?.channel || '',
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
}),
@@ -158,7 +159,7 @@ function CreateAlertChannels({
let request: WebhookChannel = {
api_url: selectedConfig?.api_url || '',
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
};
if (selectedConfig?.username !== '' || selectedConfig?.password !== '') {
@@ -226,7 +227,7 @@ function CreateAlertChannels({
return {
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
routing_key: selectedConfig?.routing_key || '',
client: selectedConfig?.client || '',
client_url: selectedConfig?.client_url || '',
@@ -274,7 +275,7 @@ function CreateAlertChannels({
() => ({
api_key: selectedConfig?.api_key || '',
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
description: selectedConfig?.description || '',
message: selectedConfig?.message || '',
priority: selectedConfig?.priority || '',
@@ -312,7 +313,7 @@ function CreateAlertChannels({
const prepareEmailRequest = useCallback(
() => ({
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
to: selectedConfig?.to || '',
html: selectedConfig?.html || '',
headers: selectedConfig?.headers || {},
@@ -350,7 +351,7 @@ function CreateAlertChannels({
() => ({
webhook_url: selectedConfig?.webhook_url || '',
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
}),

View File

@@ -1,7 +1,9 @@
import { Form, Row } from 'antd';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { QueryParams } from 'constants/query';
import FormAlertRules from 'container/FormAlertRules';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import history from 'lib/history';
import { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { AlertTypes } from 'types/api/alerts/alertTypes';
@@ -18,15 +20,25 @@ import SelectAlertType from './SelectAlertType';
function CreateRules(): JSX.Element {
const [initValues, setInitValues] = useState<AlertDef | null>(null);
const [alertType, setAlertType] = useState<AlertTypes>(
AlertTypes.METRICS_BASED_ALERT,
);
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const version = queryParams.get('version');
const alertTypeFromParams = queryParams.get(QueryParams.alertType);
const compositeQuery = useGetCompositeQueryParam();
function getAlertTypeFromDataSource(): AlertTypes | null {
if (!compositeQuery) {
return null;
}
const dataSource = compositeQuery?.builder?.queryData[0]?.dataSource;
return ALERT_TYPE_VS_SOURCE_MAPPING[dataSource];
}
const [alertType, setAlertType] = useState<AlertTypes>(
(alertTypeFromParams as AlertTypes) || getAlertTypeFromDataSource(),
);
const [formInstance] = Form.useForm();
@@ -48,21 +60,17 @@ function CreateRules(): JSX.Element {
version: version || ENTITY_VERSION_V4,
});
}
queryParams.set(QueryParams.alertType, typ);
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
history.replace(generatedUrl);
};
useEffect(() => {
if (!compositeQuery) {
return;
}
const dataSource = compositeQuery?.builder?.queryData[0]?.dataSource;
const alertType = ALERT_TYPE_VS_SOURCE_MAPPING[dataSource];
if (alertType) {
onSelectType(alertType);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [compositeQuery]);
}, [alertType]);
if (!initValues) {
return (

View File

@@ -72,7 +72,7 @@ function EditAlertChannels({
api_url: selectedConfig?.api_url || '',
channel: selectedConfig?.channel || '',
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
id,
@@ -115,7 +115,7 @@ function EditAlertChannels({
return {
api_url: selectedConfig?.api_url || '',
name: name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
username,
password,
id,
@@ -284,7 +284,7 @@ function EditAlertChannels({
() => ({
webhook_url: selectedConfig?.webhook_url || '',
name: selectedConfig?.name || '',
send_resolved: true,
send_resolved: selectedConfig?.send_resolved || false,
text: selectedConfig?.text || '',
title: selectedConfig?.title || '',
id,

View File

@@ -1,10 +1,10 @@
.hide-update {
left: calc(50% - 41px) !important;
left: calc(50% - 72px) !important;
}
.explorer-update {
position: fixed;
bottom: 24px;
left: calc(50% - 250px);
left: calc(50% - 352px);
display: flex;
align-items: center;
gap: 12px;
@@ -37,7 +37,6 @@
}
}
.explorer-options {
position: fixed;
bottom: 24px;
@@ -78,11 +77,9 @@
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
border: none;
border: 1px solid #1d2023;
color: #c0c1c3;
background-color: #161922;
box-shadow: none !important;

View File

@@ -373,34 +373,42 @@ function ExplorerOptions({
onClick={handleSaveViewModalToggle}
className={isEditDeleteSupported ? '' : 'hidden'}
disabled={viewsIsLoading || isRefetching}
icon={<Disc3 size={16} />}
>
<Disc3 size={16} /> Save this view
Save this view
</Button>
</div>
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
<Tooltip title="Create Alerts">
<Button
disabled={disabled}
shape="round"
onClick={onCreateAlertsHandler}
icon={<ConciergeBell size={16} />}
>
Create an Alert
</Button>
<Button
type="primary"
disabled={disabled}
shape="round"
onClick={onAddToDashboard}
icon={<Plus size={16} />}
>
Add to Dashboard
</Button>
</div>
<div className="actions">
<Tooltip title="Hide">
<Button
disabled={disabled}
shape="circle"
onClick={onCreateAlertsHandler}
>
<ConciergeBell size={16} />
</Button>
</Tooltip>
<Tooltip title="Add to Dashboard">
<Button disabled={disabled} shape="circle" onClick={onAddToDashboard}>
<Plus size={16} />
</Button>
</Tooltip>
<Tooltip title="Hide">
<Button disabled={disabled} shape="circle" onClick={hideToolbar}>
<PanelBottomClose size={16} />
</Button>
onClick={hideToolbar}
icon={<PanelBottomClose size={16} />}
/>
</Tooltip>
</div>
</div>
@@ -413,6 +421,7 @@ function ExplorerOptions({
isQueryUpdated={isQueryUpdated}
handleClearSelect={handleClearSelect}
onUpdateQueryHandler={onUpdateQueryHandler}
isEditDeleteSupported={isEditDeleteSupported}
/>
<Modal

View File

@@ -16,6 +16,7 @@ interface DroppableAreaProps {
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
handleClearSelect: () => void;
onUpdateQueryHandler: () => void;
isEditDeleteSupported: boolean;
}
function ExplorerOptionsHideArea({
@@ -25,6 +26,7 @@ function ExplorerOptionsHideArea({
setIsExplorerOptionHidden,
handleClearSelect,
onUpdateQueryHandler,
isEditDeleteSupported,
}: DroppableAreaProps): JSX.Element {
const handleShowExplorerOption = (): void => {
if (setIsExplorerOptionHidden) {
@@ -47,14 +49,16 @@ function ExplorerOptionsHideArea({
icon={<X size={14} color={Color.BG_INK_500} />}
/>
</Tooltip>
<Tooltip title="Update this View">
<Button
onClick={onUpdateQueryHandler}
className="action-btn"
style={{ background: Color.BG_ROBIN_500 }}
icon={<Disc3 size={14} color={Color.BG_INK_500} />}
/>
</Tooltip>
{isEditDeleteSupported && (
<Tooltip title="Update this View">
<Button
onClick={onUpdateQueryHandler}
className="action-btn"
style={{ background: Color.BG_ROBIN_500 }}
icon={<Disc3 size={14} color={Color.BG_INK_500} />}
/>
</Tooltip>
)}
</div>
)}
<Button

View File

@@ -1,4 +1,4 @@
import { Form, FormInstance, Input, Select, Typography } from 'antd';
import { Form, FormInstance, Input, Select, Switch, Typography } from 'antd';
import { Store } from 'antd/lib/form/interface';
import UpgradePrompt from 'components/Upgrade/UpgradePrompt';
import { FeatureKeys } from 'constants/features';
@@ -95,6 +95,22 @@ function FormAlertChannels({
/>
</Form.Item>
<Form.Item
label={t('field_send_resolved')}
labelAlign="left"
name="send_resolved"
>
<Switch
defaultChecked={initialValue?.send_resolved}
onChange={(value): void => {
setSelectedConfig((state) => ({
...state,
send_resolved: value,
}));
}}
/>
</Form.Item>
<Form.Item label={t('field_channel_type')} labelAlign="left" name="type">
<Select disabled={editing} onChange={onTypeChangeHandler} value={type}>
<Select.Option value="slack" key="slack">

View File

@@ -1,45 +1,50 @@
.create-alert-modal {
.ant-modal-content {
background-color: var(--bg-ink-300);
.ant-modal-confirm-title {
color: var(--bg-vanilla-100);
}
.ant-modal-content {
background-color: var(--bg-ink-300);
.ant-modal-confirm-title {
color: var(--bg-vanilla-100);
}
.ant-modal-confirm-content {
.ant-typography {
color: var(--bg-vanilla-100);
}
}
.ant-modal-confirm-content {
.ant-typography {
color: var(--bg-vanilla-100);
}
}
.ant-modal-confirm-btns {
button:nth-of-type(1) {
background-color: var(--bg-slate-400);
border: none;
color: var(--bg-vanilla-100);
}
}
}
.ant-modal-confirm-btns {
button:nth-of-type(1) {
background-color: var(--bg-slate-400);
border: none;
color: var(--bg-vanilla-100);
}
}
}
}
.lightMode {
.ant-modal-content {
background-color: var(--bg-vanilla-100);
.ant-modal-confirm-title {
color: var(--bg-ink-500);
}
.ant-modal-content {
background-color: var(--bg-vanilla-100);
.ant-modal-confirm-title {
color: var(--bg-ink-500);
}
.ant-modal-confirm-content {
.ant-typography {
color: var(--bg-ink-500);
}
}
.ant-modal-confirm-content {
.ant-typography {
color: var(--bg-ink-500);
}
}
.ant-modal-confirm-btns {
button:nth-of-type(1) {
background-color: var(--bg-vanilla-300);
border: none;
color: var(--bg-ink-500);
}
}
}
}
.ant-modal-confirm-btns {
button:nth-of-type(1) {
background-color: var(--bg-vanilla-300);
border: none;
color: var(--bg-ink-500);
}
}
}
}
.facing-issue-btn {
margin-top: 20px;
width: 100%;
}

View File

@@ -56,8 +56,9 @@ function QuerySection({
initialDataSource: ALERTS_DATA_SOURCE_MAP[alertType],
}}
showFunctions={
alertType === AlertTypes.METRICS_BASED_ALERT &&
alertDef.version === ENTITY_VERSION_V4
(alertType === AlertTypes.METRICS_BASED_ALERT &&
alertDef.version === ENTITY_VERSION_V4) ||
alertType === AlertTypes.LOGS_BASED_ALERT
}
version={alertDef.version || 'v3'}
/>

View File

@@ -11,6 +11,8 @@ import {
} from 'antd';
import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { alertHelpMessage } from 'components/facingIssueBtn/util';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -138,15 +140,21 @@ function FormAlertRules({
useEffect(() => {
// Set selectedQueryName based on the length of queryOptions
setAlertDef((def) => ({
...def,
condition: {
...def.condition,
selectedQueryName:
queryOptions.length > 0 ? String(queryOptions[0].value) : undefined,
},
}));
}, [currentQuery?.queryType, queryOptions]);
const selectedQueryName = alertDef?.condition?.selectedQueryName;
if (
!selectedQueryName ||
!queryOptions.some((option) => option.value === selectedQueryName)
) {
setAlertDef((def) => ({
...def,
condition: {
...def.condition,
selectedQueryName:
queryOptions.length > 0 ? String(queryOptions[0].value) : undefined,
},
}));
}
}, [alertDef, currentQuery?.queryType, queryOptions]);
const onCancelHandler = useCallback(() => {
history.replace(ROUTES.LIST_ALL_ALERT);
@@ -482,6 +490,8 @@ function FormAlertRules({
alertDef?.broadcastToAll ||
(alertDef.preferredChannels && alertDef.preferredChannels.length > 0);
const isRuleCreated = !ruleId || ruleId === 0;
return (
<>
{Element}
@@ -514,6 +524,7 @@ function FormAlertRules({
runQuery={handleRunQuery}
alertDef={alertDef}
panelType={panelType || PANEL_TYPES.TIME_SERIES}
key={currentQuery.queryType}
/>
<RuleOptions
@@ -563,6 +574,22 @@ function FormAlertRules({
</StyledLeftContainer>
<Col flex="1 1 300px">
<UserGuide queryType={currentQuery.queryType} />
<FacingIssueBtn
attributes={{
alert: alertDef?.alert,
alertType: alertDef?.alertType,
id: ruleId,
ruleType: alertDef?.ruleType,
state: (alertDef as any)?.state,
panelType,
screen: isRuleCreated ? 'Edit Alert' : 'New Alert',
}}
className="facing-issue-btn"
eventName="Alert: Facing Issues in alert"
buttonText="Need help with this alert?"
message={alertHelpMessage(alertDef, ruleId)}
onHoverText="Click here to get help with this alert"
/>
</Col>
</PanelContainer>
</>

View File

@@ -26,5 +26,6 @@ export const PANEL_TYPES_VS_FULL_VIEW_TABLE: PanelTypeAndGraphManagerVisibilityP
LIST: false,
TRACE: false,
BAR: true,
PIE: false,
EMPTY_WIDGET: false,
};

View File

@@ -1,62 +1,60 @@
import './WidgetFullView.styles.scss';
import { SyncOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { LoadingOutlined, SyncOutlined } from '@ant-design/icons';
import { Button, Spin } from 'antd';
import cx from 'classnames';
import { ToggleGraphProps } from 'components/Graph/types';
import Spinner from 'components/Spinner';
import TimePreference from 'components/TimePreferenceDropDown';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import GridPanelSwitch from 'container/GridPanelSwitch';
import {
timeItems,
timePreferance,
} from 'container/NewWidget/RightContainer/timeItems';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { useChartMutable } from 'hooks/useChartMutable';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import GetMinMax from 'lib/getMinMax';
import history from 'lib/history';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
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';
import uPlot from 'uplot';
import { getGraphType } from 'utils/getGraphType';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange';
import { getLocalStorageGraphVisibilityState } from '../utils';
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
import GraphManager from './GraphManager';
import { GraphContainer, TimeContainer } from './styles';
import { FullViewProps } from './types';
function FullView({
widget,
fullViewOptions = true,
onClickHandler,
name,
version,
originalName,
yAxisUnit,
onDragSelect,
isDependedDataLoaded = false,
onToggleModelHandler,
parentChartRef,
}: FullViewProps): JSX.Element {
const { selectedTime: globalSelectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const dispatch = useDispatch();
const urlQuery = useUrlQuery();
const location = useLocation();
const fullViewRef = useRef<HTMLDivElement>(null);
const [chartOptions, setChartOptions] = useState<uPlot.Options>();
const { selectedDashboard, isDashboardLocked } = useDashboard();
const getSelectedTime = useCallback(
@@ -74,24 +72,70 @@ function FullView({
const updatedQuery = useStepInterval(widget?.query);
const response = useGetQueryRange(
{
selectedTime: selectedTime.enum,
graphType:
widget.panelTypes === PANEL_TYPES.BAR
? PANEL_TYPES.TIME_SERIES
: widget.panelTypes,
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
if (widget.panelTypes !== PANEL_TYPES.LIST) {
return {
selectedTime: selectedTime.enum,
graphType: getGraphType(widget.panelTypes),
query: updatedQuery,
globalSelectedInterval: globalSelectedTime,
variables: getDashboardVariables(selectedDashboard?.data.variables),
};
}
updatedQuery.builder.queryData[0].pageSize = 10;
return {
query: updatedQuery,
graphType: PANEL_TYPES.LIST,
selectedTime: widget?.timePreferance || 'GLOBAL_TIME',
globalSelectedInterval: globalSelectedTime,
variables: getDashboardVariables(selectedDashboard?.data.variables),
},
tableParams: {
pagination: {
offset: 0,
limit: updatedQuery.builder.queryData[0].limit || 0,
},
},
};
});
useEffect(() => {
setRequestData((prev) => ({
...prev,
selectedTime: selectedTime.enum,
}));
}, [selectedTime]);
const response = useGetQueryRange(
requestData,
selectedDashboard?.data?.version || version || DEFAULT_ENTITY_VERSION,
{
queryKey: `FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}-${widget.id}`,
enabled: !isDependedDataLoaded && widget.panelTypes !== PANEL_TYPES.LIST, // Internally both the list view panel has it's own query range api call, so we don't need to call it again
queryKey: [widget?.query, widget?.panelTypes, requestData, version],
enabled: !isDependedDataLoaded,
keepPreviousData: true,
},
);
const onDragSelect = useCallback(
(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,
]);
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
},
[dispatch, location.pathname, urlQuery],
);
const [graphsVisibilityStates, setGraphsVisibilityStates] = useState<
boolean[]
>(Array(response.data?.payload.data.result.length).fill(true));
@@ -118,60 +162,6 @@ function FullView({
response.data.payload.data.result = sortedSeriesData;
}
const chartData = getUPlotChartData(response?.data?.payload, widget.fillSpans);
const isDarkMode = useIsDarkMode();
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
useEffect((): void => {
const { startTime, endTime } = getTimeRange(response);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [maxTime, minTime, globalSelectedInterval, response]);
useEffect(() => {
if (!response.isFetching && fullViewRef.current) {
const width = fullViewRef.current?.clientWidth
? fullViewRef.current.clientWidth - 45
: 700;
const height = fullViewRef.current?.clientWidth
? fullViewRef.current.clientHeight
: 300;
const newChartOptions = getUPlotChartOptions({
id: originalName,
yAxisUnit: yAxisUnit || '',
apiResponse: response.data?.payload,
dimensions: {
height,
width,
},
isDarkMode,
onDragSelect,
graphsVisibilityStates,
setGraphsVisibilityStates,
thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
softMax: widget.softMax === undefined ? null : widget.softMax,
softMin: widget.softMin === undefined ? null : widget.softMin,
panelType: widget.panelTypes,
});
setChartOptions(newChartOptions);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [response.isFetching, graphsVisibilityStates, fullViewRef.current]);
useEffect(() => {
graphsVisibilityStates?.forEach((e, i) => {
fullViewChartRef?.current?.toggleGraph(i, e);
@@ -180,7 +170,7 @@ function FullView({
const isListView = widget.panelTypes === PANEL_TYPES.LIST;
if (response.isFetching) {
if (response.isLoading && widget.panelTypes !== PANEL_TYPES.LIST) {
return <Spinner height="100%" size="large" tip="Loading..." />;
}
@@ -189,6 +179,9 @@ function FullView({
<div className="full-view-header-container">
{fullViewOptions && (
<TimeContainer $panelType={widget.panelTypes}>
{response.isFetching && (
<Spin spinning indicator={<LoadingOutlined spin />} />
)}
<TimePreference
selectedTime={selectedTime}
setSelectedTime={setSelectedTime}
@@ -214,47 +207,24 @@ function FullView({
})}
ref={fullViewRef}
>
{chartOptions && (
<GraphContainer
style={{
height: isListView ? '100%' : '90%',
}}
isGraphLegendToggleAvailable={canModifyChart}
>
<GridPanelSwitch
panelType={widget.panelTypes}
data={chartData}
options={chartOptions}
onClickHandler={onClickHandler}
name={name}
yAxisUnit={yAxisUnit}
onDragSelect={onDragSelect}
panelData={response.data?.payload.data.newResult.data.result || []}
query={widget.query}
ref={fullViewChartRef}
thresholds={widget.thresholds}
selectedLogFields={widget.selectedLogFields}
dataSource={widget.query.builder.queryData[0].dataSource}
selectedTracesFields={widget.selectedTracesFields}
selectedTime={selectedTime}
/>
</GraphContainer>
)}
<GraphContainer
style={{
height: isListView ? '100%' : '90%',
}}
isGraphLegendToggleAvailable={canModifyChart}
>
<PanelWrapper
queryResponse={response}
widget={widget}
setRequestData={setRequestData}
isFullViewMode
onToggleModelHandler={onToggleModelHandler}
setGraphVisibility={setGraphsVisibilityStates}
graphVisibility={graphsVisibilityStates}
onDragSelect={onDragSelect}
/>
</GraphContainer>
</div>
{canModifyChart && chartOptions && !isDashboardLocked && (
<GraphManager
data={chartData}
name={originalName}
options={chartOptions}
yAxisUnit={yAxisUnit}
onToggleModelHandler={onToggleModelHandler}
setGraphsVisibilityStates={setGraphsVisibilityStates}
graphsVisibilityStates={graphsVisibilityStates}
lineChartRef={fullViewChartRef}
parentChartRef={parentChartRef}
/>
)}
</div>
);
}

View File

@@ -18,6 +18,7 @@ export const NotFoundContainer = styled.div`
export const TimeContainer = styled.div<Props>`
display: flex;
justify-content: flex-end;
align-items: center;
${({ $panelType }): FlattenSimpleInterpolation =>
$panelType === PANEL_TYPES.TABLE
? css`

View File

@@ -53,10 +53,8 @@ export interface FullViewProps {
version?: string;
originalName: string;
yAxisUnit?: string;
onDragSelect: (start: number, end: number) => void;
isDependedDataLoaded?: boolean;
onToggleModelHandler?: GraphManagerProps['onToggleModelHandler'];
parentChartRef: GraphManagerProps['lineChartRef'];
}
export interface GraphManagerProps extends UplotProps {

View File

@@ -6,7 +6,7 @@ import { ToggleGraphProps } from 'components/Graph/types';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import GridPanelSwitch from 'container/GridPanelSwitch';
import PanelWrapper from 'container/PanelWrapper/PanelWrapper';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
@@ -33,23 +33,20 @@ import FullView from './FullView';
import { Modal } from './styles';
import { WidgetGraphComponentProps } from './types';
import { getLocalStorageGraphVisibilityState } from './utils';
// import { getLocalStorageGraphVisibilityState } from './utils';
function WidgetGraphComponent({
widget,
queryResponse,
errorMessage,
name,
version,
threshold,
headerMenuList,
isWarning,
data,
options,
graphVisibiltyState,
isFetchingResponse,
setRequestData,
onClickHandler,
onDragSelect,
setGraphVisibility,
isFetchingResponse,
}: WidgetGraphComponentProps): JSX.Element {
const [deleteModal, setDeleteModal] = useState(false);
const [hovered, setHovered] = useState(false);
@@ -61,12 +58,15 @@ function WidgetGraphComponent({
const isFullViewOpen = params.get(QueryParams.expandedWidgetId) === widget.id;
const lineChartRef = useRef<ToggleGraphProps>();
const [graphVisibility, setGraphVisibility] = useState<boolean[]>(
Array(queryResponse.data?.payload?.data?.result?.length || 0).fill(true),
);
const graphRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!lineChartRef.current) return;
graphVisibiltyState.forEach((state, index) => {
graphVisibility.forEach((state, index) => {
lineChartRef.current?.toggleGraph(index, state);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -135,7 +135,7 @@ function WidgetGraphComponent({
i: uuid,
w: 6,
x: 0,
h: 3,
h: 6,
y: 0,
},
];
@@ -210,7 +210,7 @@ function WidgetGraphComponent({
graphVisibilityStates: localStoredVisibilityState,
} = getLocalStorageGraphVisibilityState({
apiResponse: queryResponse.data.payload.data.result,
name,
name: widget.id,
});
setGraphVisibility(localStoredVisibilityState);
}
@@ -252,7 +252,7 @@ function WidgetGraphComponent({
onBlur={(): void => {
setHovered(false);
}}
id={name}
id={widget.id}
>
<Modal
destroyOnClose
@@ -278,14 +278,12 @@ function WidgetGraphComponent({
className="widget-full-view"
>
<FullView
name={`${name}expanded`}
name={`${widget.id}expanded`}
version={version}
originalName={name}
originalName={widget.id}
widget={widget}
yAxisUnit={widget.yAxisUnit}
onToggleModelHandler={onToggleModelHandler}
parentChartRef={lineChartRef}
onDragSelect={onDragSelect}
/>
</Modal>
@@ -305,26 +303,22 @@ function WidgetGraphComponent({
isFetchingResponse={isFetchingResponse}
/>
</div>
{queryResponse.isLoading && <Skeleton />}
{queryResponse.isLoading && widget.panelTypes !== PANEL_TYPES.LIST && (
<Skeleton />
)}
{(queryResponse.isSuccess || widget.panelTypes === PANEL_TYPES.LIST) && (
<div
className={cx('widget-graph-container', widget.panelTypes)}
ref={graphRef}
>
<GridPanelSwitch
panelType={widget.panelTypes}
data={data}
name={name}
ref={lineChartRef}
options={options}
yAxisUnit={widget.yAxisUnit}
<PanelWrapper
widget={widget}
queryResponse={queryResponse}
setRequestData={setRequestData}
setGraphVisibility={setGraphVisibility}
graphVisibility={graphVisibility}
onClickHandler={onClickHandler}
panelData={queryResponse.data?.payload?.data.newResult.data.result || []}
query={widget.query}
thresholds={widget.thresholds}
selectedLogFields={widget.selectedLogFields}
dataSource={widget.query.builder?.queryData[0]?.dataSource}
selectedTracesFields={widget.selectedTracesFields}
onDragSelect={onDragSelect}
/>
</div>
)}

View File

@@ -4,80 +4,47 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
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 { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
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';
import _noop from 'lodash-es/noop';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { memo, useEffect, 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';
import { getGraphType } from 'utils/getGraphType';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange';
import EmptyWidget from '../EmptyWidget';
import { MenuItemKeys } from '../WidgetHeader/contants';
import { GridCardGraphProps } from './types';
import { getLocalStorageGraphVisibilityState } from './utils';
import WidgetGraphComponent from './WidgetGraphComponent';
function GridCardGraph({
widget,
name,
onClickHandler = _noop,
headerMenuList = [MenuItemKeys.View],
isQueryEnabled,
threshold,
variables,
fillSpans = false,
version,
onClickHandler,
onDragSelect,
}: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>();
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const urlQuery = useUrlQuery();
const location = useLocation();
const {
toScrollWidgetId,
setToScrollWidgetId,
variablesToGetUpdated,
} = useDashboard();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const onDragSelect = useCallback(
(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,
]);
urlQuery.set(QueryParams.startTime, minTime.toString());
urlQuery.set(QueryParams.endTime, maxTime.toString());
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
},
[dispatch, location.pathname, urlQuery],
);
const handleBackNavigation = (): void => {
const searchParams = new URLSearchParams(window.location.search);
const startTime = searchParams.get(QueryParams.startTime);
@@ -131,15 +98,39 @@ function GridCardGraph({
isVisible &&
!isEmptyWidget &&
isQueryEnabled &&
widget.panelTypes !== PANEL_TYPES.LIST;
isEmpty(variablesToGetUpdated);
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
if (widget.panelTypes !== PANEL_TYPES.LIST) {
return {
selectedTime: widget?.timePreferance,
graphType: getGraphType(widget.panelTypes),
query: updatedQuery,
globalSelectedInterval,
variables: getDashboardVariables(variables),
};
}
updatedQuery.builder.queryData[0].pageSize = 10;
return {
query: updatedQuery,
graphType: PANEL_TYPES.LIST,
selectedTime: widget.timePreferance || 'GLOBAL_TIME',
globalSelectedInterval,
tableParams: {
pagination: {
offset: 0,
limit: updatedQuery.builder.queryData[0].limit || 0,
},
},
};
});
const queryResponse = useGetQueryRange(
{
selectedTime: widget?.timePreferance,
graphType: getGraphType(widget.panelTypes),
query: updatedQuery,
globalSelectedInterval,
...requestData,
variables: getDashboardVariables(variables),
selectedTime: widget.timePreferance || 'GLOBAL_TIME',
globalSelectedInterval,
},
version || DEFAULT_ENTITY_VERSION,
{
@@ -151,6 +142,7 @@ function GridCardGraph({
widget?.query,
widget?.panelTypes,
widget.timePreferance,
requestData,
],
retry(failureCount, error): boolean {
if (
@@ -173,15 +165,6 @@ function GridCardGraph({
const isEmptyLayout = widget?.id === PANEL_TYPES.EMPTY_WIDGET;
const containerDimensions = useResizeObserver(graphRef);
useEffect((): void => {
const { startTime, endTime } = getTimeRange(queryResponse);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [maxTime, minTime, globalSelectedInterval, queryResponse]);
if (queryResponse.data && widget.panelTypes === PANEL_TYPES.BAR) {
const sortedSeriesData = getSortedSeriesData(
queryResponse.data?.payload.data.result,
@@ -189,89 +172,30 @@ function GridCardGraph({
queryResponse.data.payload.data.result = sortedSeriesData;
}
const chartData = getUPlotChartData(queryResponse?.data?.payload, fillSpans);
const isDarkMode = useIsDarkMode();
const menuList =
widget.panelTypes === PANEL_TYPES.TABLE ||
widget.panelTypes === PANEL_TYPES.LIST
widget.panelTypes === PANEL_TYPES.LIST ||
widget.panelTypes === PANEL_TYPES.PIE
? headerMenuList.filter((menu) => menu !== MenuItemKeys.CreateAlerts)
: headerMenuList;
const [graphVisibility, setGraphVisibility] = useState<boolean[]>(
Array(queryResponse.data?.payload?.data.result.length || 0).fill(true),
);
useEffect(() => {
const {
graphVisibilityStates: localStoredVisibilityState,
} = getLocalStorageGraphVisibilityState({
apiResponse: queryResponse.data?.payload.data.result || [],
name,
});
setGraphVisibility(localStoredVisibilityState);
}, [name, queryResponse.data?.payload.data.result]);
const options = useMemo(
() =>
getUPlotChartOptions({
id: widget?.id,
apiResponse: queryResponse.data?.payload,
dimensions: containerDimensions,
isDarkMode,
onDragSelect,
yAxisUnit: widget?.yAxisUnit,
onClickHandler,
thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
softMax: widget.softMax === undefined ? null : widget.softMax,
softMin: widget.softMin === undefined ? null : widget.softMin,
graphsVisibilityStates: graphVisibility,
setGraphsVisibilityStates: setGraphVisibility,
panelType: widget.panelTypes,
}),
[
widget?.id,
widget?.yAxisUnit,
widget.thresholds,
widget.softMax,
widget.softMin,
queryResponse.data?.payload,
containerDimensions,
isDarkMode,
onDragSelect,
onClickHandler,
minTimeScale,
maxTimeScale,
graphVisibility,
setGraphVisibility,
widget.panelTypes,
],
);
return (
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
{isEmptyLayout ? (
<EmptyWidget />
) : (
<WidgetGraphComponent
data={chartData}
options={options}
widget={widget}
queryResponse={queryResponse}
errorMessage={errorMessage}
isWarning={false}
name={name}
version={version}
onDragSelect={onDragSelect}
threshold={threshold}
headerMenuList={menuList}
onClickHandler={onClickHandler}
graphVisibiltyState={graphVisibility}
setGraphVisibility={setGraphVisibility}
isFetchingResponse={queryResponse.isFetching}
setRequestData={setRequestData}
onClickHandler={onClickHandler}
onDragSelect={onDragSelect}
/>
)}
</div>

View File

@@ -1,9 +1,9 @@
import { ToggleGraphProps } from 'components/Graph/types';
import { UplotProps } from 'components/Uplot/Uplot';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { Dispatch, MutableRefObject, ReactNode, SetStateAction } from 'react';
import { UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { SuccessResponse } from 'types/api';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
@@ -16,35 +16,32 @@ export interface GraphVisibilityLegendEntryProps {
legendEntry: LegendEntryProps[];
}
export interface WidgetGraphComponentProps extends UplotProps {
export interface WidgetGraphComponentProps {
widget: Widgets;
queryResponse: UseQueryResult<
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
errorMessage: string | undefined;
name: string;
version?: string;
onDragSelect: (start: number, end: number) => void;
onClickHandler?: OnClickPluginOpts['onClick'];
threshold?: ReactNode;
headerMenuList: MenuItemKeys[];
isWarning: boolean;
graphVisibiltyState: boolean[];
setGraphVisibility: Dispatch<SetStateAction<boolean[]>>;
isFetchingResponse: boolean;
setRequestData?: Dispatch<SetStateAction<GetQueryResultsProps>>;
onClickHandler?: OnClickPluginOpts['onClick'];
onDragSelect: (start: number, end: number) => void;
}
export interface GridCardGraphProps {
widget: Widgets;
name: string;
onDragSelect?: (start: number, end: number) => void;
onClickHandler?: OnClickPluginOpts['onClick'];
threshold?: ReactNode;
headerMenuList?: WidgetGraphComponentProps['headerMenuList'];
onClickHandler?: OnClickPluginOpts['onClick'];
isQueryEnabled: boolean;
variables?: Dashboard['data']['variables'];
fillSpans?: boolean;
version?: string;
onDragSelect: (start: number, end: number) => void;
}
export interface GetGraphVisibilityStateOnLegendClickProps {

View File

@@ -5,6 +5,12 @@
.react-grid-layout {
border: none !important;
margin-top: 0;
.widget-graph-container {
&.graph {
height: 100%;
}
}
}
}
@@ -23,7 +29,7 @@
.ant-modal-header {
background-color: var(--bg-ink-400);
}
}
}
}
.lightMode {

View File

@@ -1,27 +1,46 @@
import './GridCardLayout.styles.scss';
import { PlusOutlined } from '@ant-design/icons';
import { Tooltip } from 'antd';
import { Flex, Form, Input, Modal, Tooltip, Typography } from 'antd';
import { useForm } from 'antd/es/form/Form';
import cx from 'classnames';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { dashboardHelpMessage } from 'components/facingIssueBtn/util';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QueryParams } from 'constants/query';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import { themeColors } from 'constants/theme';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { defaultTo } from 'lodash-es';
import isEqual from 'lodash-es/isEqual';
import { FullscreenIcon } from 'lucide-react';
import {
FullscreenIcon,
GripVertical,
MoveDown,
MoveUp,
Settings,
Trash2,
} from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useEffect, useState } from 'react';
import { sortLayout } from 'providers/Dashboard/util';
import { useCallback, useEffect, useState } from 'react';
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
import { Layout } from 'react-grid-layout';
import { ItemCallback, Layout } from 'react-grid-layout';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
import { v4 as uuid } from 'uuid';
import { EditMenuAction, ViewMenuAction } from './config';
import GridCard from './GridCard';
@@ -40,11 +59,15 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
selectedDashboard,
layouts,
setLayouts,
panelMap,
setPanelMap,
setSelectedDashboard,
isDashboardLocked,
} = useDashboard();
const { data } = selectedDashboard || {};
const handle = useFullScreenHandle();
const { pathname } = useLocation();
const dispatch = useDispatch();
const { widgets, variables } = data || {};
@@ -58,9 +81,30 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
const [dashboardLayout, setDashboardLayout] = useState<Layout[]>([]);
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState<boolean>(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState<boolean>(false);
const [currentSelectRowId, setCurrentSelectRowId] = useState<string | null>(
null,
);
const [currentPanelMap, setCurrentPanelMap] = useState<
Record<string, { widgets: Layout[]; collapsed: boolean }>
>({});
useEffect(() => {
setCurrentPanelMap(panelMap);
}, [panelMap]);
const [form] = useForm<{
title: string;
}>();
const updateDashboardMutation = useUpdateDashboard();
const { notifications } = useNotifications();
const urlQuery = useUrlQuery();
let permissions: ComponentTypes[] = ['save_layout', 'add_panel'];
@@ -79,7 +123,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
);
useEffect(() => {
setDashboardLayout(layouts);
setDashboardLayout(sortLayout(layouts));
}, [layouts]);
const onSaveHandler = (): void => {
@@ -89,6 +133,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
...selectedDashboard,
data: {
...selectedDashboard.data,
panelMap: { ...currentPanelMap },
layout: dashboardLayout.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET),
},
uuid: selectedDashboard.uuid,
@@ -98,8 +143,9 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
onSuccess: (updatedDashboard) => {
if (updatedDashboard.payload) {
if (updatedDashboard.payload.data.layout)
setLayouts(updatedDashboard.payload.data.layout);
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
setSelectedDashboard(updatedDashboard.payload);
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
}
featureResponse.refetch();
@@ -122,10 +168,28 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
dashboardLayout,
);
if (!isEqual(filterLayout, filterDashboardLayout)) {
setDashboardLayout(layout);
const updatedLayout = sortLayout(layout);
setDashboardLayout(updatedLayout);
}
};
const onDragSelect = useCallback(
(start: number, end: number) => {
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, pathname, urlQuery],
);
useEffect(() => {
if (
dashboardLayout &&
@@ -142,35 +206,335 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dashboardLayout]);
function handleAddRow(): void {
if (!selectedDashboard) return;
const id = uuid();
const newRowWidgetMap: { widgets: Layout[]; collapsed: boolean } = {
widgets: [],
collapsed: false,
};
const currentRowIdx = 0;
for (let j = currentRowIdx; j < dashboardLayout.length; j++) {
if (!currentPanelMap[dashboardLayout[j].i]) {
newRowWidgetMap.widgets.push(dashboardLayout[j]);
} else {
break;
}
}
const updatedDashboard: Dashboard = {
...selectedDashboard,
data: {
...selectedDashboard.data,
layout: [
{
i: id,
w: 12,
minW: 12,
minH: 1,
maxH: 1,
x: 0,
h: 1,
y: 0,
},
...dashboardLayout.filter((e) => e.i !== PANEL_TYPES.EMPTY_WIDGET),
],
panelMap: { ...currentPanelMap, [id]: newRowWidgetMap },
widgets: [
...(selectedDashboard.data.widgets || []),
{
id,
title: 'Sample Row',
description: '',
panelTypes: PANEL_GROUP_TYPES.ROW,
},
],
},
uuid: selectedDashboard.uuid,
};
updateDashboardMutation.mutate(updatedDashboard, {
// eslint-disable-next-line sonarjs/no-identical-functions
onSuccess: (updatedDashboard) => {
if (updatedDashboard.payload) {
if (updatedDashboard.payload.data.layout)
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
setSelectedDashboard(updatedDashboard.payload);
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
}
featureResponse.refetch();
},
// eslint-disable-next-line sonarjs/no-identical-functions
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
}
const handleRowSettingsClick = (id: string): void => {
setIsSettingsModalOpen(true);
setCurrentSelectRowId(id);
};
const onSettingsModalSubmit = (): void => {
const newTitle = form.getFieldValue('title');
if (!selectedDashboard) return;
if (!currentSelectRowId) return;
const currentWidget = selectedDashboard?.data?.widgets?.find(
(e) => e.id === currentSelectRowId,
);
if (!currentWidget) return;
currentWidget.title = newTitle;
const updatedWidgets = selectedDashboard?.data?.widgets?.filter(
(e) => e.id !== currentSelectRowId,
);
updatedWidgets?.push(currentWidget);
const updatedSelectedDashboard: Dashboard = {
...selectedDashboard,
data: {
...selectedDashboard.data,
widgets: updatedWidgets,
},
uuid: selectedDashboard.uuid,
};
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
onSuccess: (updatedDashboard) => {
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.payload) {
setSelectedDashboard(updatedDashboard.payload);
}
if (setPanelMap)
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
form.setFieldValue('title', '');
setIsSettingsModalOpen(false);
setCurrentSelectRowId(null);
featureResponse.refetch();
},
// eslint-disable-next-line sonarjs/no-identical-functions
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
};
// eslint-disable-next-line sonarjs/cognitive-complexity
const handleRowCollapse = (id: string): void => {
if (!selectedDashboard) return;
const rowProperties = { ...currentPanelMap[id] };
const updatedPanelMap = { ...currentPanelMap };
let updatedDashboardLayout = [...dashboardLayout];
if (rowProperties.collapsed === true) {
rowProperties.collapsed = false;
const widgetsInsideTheRow = rowProperties.widgets;
let maxY = 0;
widgetsInsideTheRow.forEach((w) => {
maxY = Math.max(maxY, w.y + w.h);
});
const currentRowWidget = dashboardLayout.find((w) => w.i === id);
if (currentRowWidget && widgetsInsideTheRow.length) {
maxY -= currentRowWidget.h + currentRowWidget.y;
}
const idxCurrentRow = dashboardLayout.findIndex((w) => w.i === id);
for (let j = idxCurrentRow + 1; j < dashboardLayout.length; j++) {
updatedDashboardLayout[j].y += maxY;
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
updatedDashboardLayout[j].i
// eslint-disable-next-line @typescript-eslint/no-loop-func
].widgets.map((w) => ({
...w,
y: w.y + maxY,
}));
}
}
updatedDashboardLayout = [...updatedDashboardLayout, ...widgetsInsideTheRow];
} else {
rowProperties.collapsed = true;
const currentIdx = dashboardLayout.findIndex((w) => w.i === id);
let widgetsInsideTheRow: Layout[] = [];
let isPanelMapUpdated = false;
for (let j = currentIdx + 1; j < dashboardLayout.length; j++) {
if (currentPanelMap[dashboardLayout[j].i]) {
rowProperties.widgets = widgetsInsideTheRow;
widgetsInsideTheRow = [];
isPanelMapUpdated = true;
break;
} else {
widgetsInsideTheRow.push(dashboardLayout[j]);
}
}
if (!isPanelMapUpdated) {
rowProperties.widgets = widgetsInsideTheRow;
}
let maxY = 0;
widgetsInsideTheRow.forEach((w) => {
maxY = Math.max(maxY, w.y + w.h);
});
const currentRowWidget = dashboardLayout[currentIdx];
if (currentRowWidget && widgetsInsideTheRow.length) {
maxY -= currentRowWidget.h + currentRowWidget.y;
}
for (let j = currentIdx + 1; j < updatedDashboardLayout.length; j++) {
updatedDashboardLayout[j].y += maxY;
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
updatedDashboardLayout[j].i
// eslint-disable-next-line @typescript-eslint/no-loop-func
].widgets.map((w) => ({
...w,
y: w.y + maxY,
}));
}
}
updatedDashboardLayout = updatedDashboardLayout.filter(
(widget) => !rowProperties.widgets.some((w: Layout) => w.i === widget.i),
);
}
setCurrentPanelMap((prev) => ({
...prev,
...updatedPanelMap,
[id]: {
...rowProperties,
},
}));
setDashboardLayout(sortLayout(updatedDashboardLayout));
};
const handleDragStop: ItemCallback = (_, oldItem, newItem): void => {
if (currentPanelMap[oldItem.i]) {
const differenceY = newItem.y - oldItem.y;
const widgetsInsideRow = currentPanelMap[oldItem.i].widgets.map((w) => ({
...w,
y: w.y + differenceY,
}));
setCurrentPanelMap((prev) => ({
...prev,
[oldItem.i]: {
...prev[oldItem.i],
widgets: widgetsInsideRow,
},
}));
}
};
const handleRowDelete = (): void => {
if (!selectedDashboard) return;
if (!currentSelectRowId) return;
const updatedWidgets = selectedDashboard?.data?.widgets?.filter(
(e) => e.id !== currentSelectRowId,
);
const updatedLayout =
selectedDashboard.data.layout?.filter((e) => e.i !== currentSelectRowId) ||
[];
const updatedPanelMap = { ...currentPanelMap };
delete updatedPanelMap[currentSelectRowId];
const updatedSelectedDashboard: Dashboard = {
...selectedDashboard,
data: {
...selectedDashboard.data,
widgets: updatedWidgets,
layout: updatedLayout,
panelMap: updatedPanelMap,
},
uuid: selectedDashboard.uuid,
};
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
onSuccess: (updatedDashboard) => {
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.payload) {
setSelectedDashboard(updatedDashboard.payload);
}
if (setPanelMap)
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
setIsDeleteModalOpen(false);
setCurrentSelectRowId(null);
featureResponse.refetch();
},
// eslint-disable-next-line sonarjs/no-identical-functions
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
};
return (
<>
<ButtonContainer>
<Tooltip title="Open in Full Screen">
<Button
className="periscope-btn"
loading={updateDashboardMutation.isLoading}
onClick={handle.enter}
icon={<FullscreenIcon size={16} />}
disabled={updateDashboardMutation.isLoading}
/>
</Tooltip>
<Flex justify="flex-end" gap={8} align="center">
<FacingIssueBtn
attributes={{
uuid: selectedDashboard?.uuid,
title: data?.title,
screen: 'Dashboard Details',
}}
eventName="Dashboard: Facing Issues in dashboard"
buttonText="Need help with this dashboard?"
message={dashboardHelpMessage(data, selectedDashboard)}
onHoverText="Click here to get help for this dashboard"
/>
<ButtonContainer>
<Tooltip title="Open in Full Screen">
<Button
className="periscope-btn"
loading={updateDashboardMutation.isLoading}
onClick={handle.enter}
icon={<FullscreenIcon size={16} />}
disabled={updateDashboardMutation.isLoading}
/>
</Tooltip>
{!isDashboardLocked && addPanelPermission && (
<Button
className="periscope-btn"
onClick={onAddPanelHandler}
icon={<PlusOutlined />}
data-testid="add-panel"
>
{t('dashboard:add_panel')}
</Button>
)}
</ButtonContainer>
{!isDashboardLocked && addPanelPermission && (
<Button
className="periscope-btn"
onClick={onAddPanelHandler}
icon={<PlusOutlined />}
data-testid="add-panel"
>
{t('dashboard:add_panel')}
</Button>
)}
{!isDashboardLocked && addPanelPermission && (
<Button
className="periscope-btn"
onClick={(): void => handleAddRow()}
icon={<PlusOutlined />}
data-testid="add-row"
>
{t('dashboard:add_row')}
</Button>
)}
</ButtonContainer>
</Flex>
<FullScreen handle={handle} className="fullscreen-grid-container">
<ReactGridLayout
cols={12}
rowHeight={100}
rowHeight={45}
autoSize
width={100}
useCSSTransforms
@@ -179,6 +543,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
isResizable={!isDashboardLocked && addPanelPermission}
allowOverlap={false}
onLayoutChange={handleLayoutChange}
onDragStop={handleDragStop}
draggableHandle=".drag-handle"
layout={dashboardLayout}
style={{ backgroundColor: isDarkMode ? '' : themeColors.snowWhite }}
@@ -187,6 +552,58 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
const { i: id } = layout;
const currentWidget = (widgets || [])?.find((e) => e.id === id);
if (currentWidget?.panelTypes === PANEL_GROUP_TYPES.ROW) {
const rowWidgetProperties = currentPanelMap[id] || {};
return (
<CardContainer
className="row-card"
isDarkMode={isDarkMode}
key={id}
data-grid={JSON.stringify(currentWidget)}
>
<div className={cx('row-panel')}>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<Button
disabled={updateDashboardMutation.isLoading}
icon={
rowWidgetProperties.collapsed ? (
<MoveDown size={14} />
) : (
<MoveUp size={14} />
)
}
type="text"
onClick={(): void => handleRowCollapse(id)}
/>
<Typography.Text>{currentWidget.title}</Typography.Text>
<Button
icon={<Settings size={14} />}
type="text"
onClick={(): void => handleRowSettingsClick(id)}
/>
</div>
{rowWidgetProperties.collapsed && (
<Button
type="text"
icon={<GripVertical size={14} />}
className="drag-handle"
/>
)}
{!rowWidgetProperties.collapsed && (
<Button
type="text"
icon={<Trash2 size={14} />}
onClick={(): void => {
setIsDeleteModalOpen(true);
setCurrentSelectRowId(id);
}}
/>
)}
</div>
</CardContainer>
);
}
return (
<CardContainer
className={isDashboardLocked ? '' : 'enable-resize'}
@@ -199,18 +616,57 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
$panelType={currentWidget?.panelTypes || PANEL_TYPES.TIME_SERIES}
>
<GridCard
widget={currentWidget || ({ id, query: {} } as Widgets)}
name={currentWidget?.id || ''}
widget={(currentWidget as Widgets) || ({ id, query: {} } as Widgets)}
headerMenuList={widgetActions}
variables={variables}
fillSpans={currentWidget?.fillSpans}
version={selectedDashboard?.data?.version}
onDragSelect={onDragSelect}
/>
</Card>
</CardContainer>
);
})}
</ReactGridLayout>
<Modal
open={isSettingsModalOpen}
title="Row Options"
destroyOnClose
footer={null}
onCancel={(): void => {
setIsSettingsModalOpen(false);
setCurrentSelectRowId(null);
}}
>
<Form form={form} onFinish={onSettingsModalSubmit} requiredMark>
<Form.Item required name={['title']}>
<Input
placeholder="Enter row name here..."
defaultValue={defaultTo(
widgets?.find((widget) => widget.id === currentSelectRowId)
?.title as string,
'Sample Title',
)}
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Apply Changes
</Button>
</Form.Item>
</Form>
</Modal>
<Modal
open={isDeleteModalOpen}
title="Delete Row"
destroyOnClose
onCancel={(): void => {
setIsDeleteModalOpen(false);
setCurrentSelectRowId(null);
}}
onOk={(): void => handleRowDelete()}
>
<Typography.Text>Are you sure you want to delete this row</Typography.Text>
</Modal>
</FullScreen>
</>
);

View File

@@ -16,6 +16,6 @@ export const EMPTY_WIDGET_LAYOUT = {
i: PANEL_TYPES.EMPTY_WIDGET,
w: 6,
x: 0,
h: 3,
h: 6,
y: 0,
};

View File

@@ -29,6 +29,17 @@ interface Props {
export const CardContainer = styled.div<Props>`
overflow: auto;
&.row-card {
.row-panel {
height: 100%;
display: flex;
justify-content: space-between;
background: var(--bg-ink-400);
align-items: center;
overflow: hidden;
}
}
&.enable-resize {
:hover {
.react-resizable-handle {

View File

@@ -1,10 +1,8 @@
import { ToggleGraphProps } from 'components/Graph/types';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { getComponentForPanelType } from 'constants/panelTypes';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config';
import { FC, forwardRef, memo, useMemo } from 'react';
import { DataSource } from 'types/common/queryBuilder';
import { GridPanelSwitchProps, PropsTypePropsMap } from './types';
@@ -21,10 +19,7 @@ const GridPanelSwitch = forwardRef<
query,
options,
thresholds,
selectedLogFields,
selectedTracesFields,
dataSource,
selectedTime,
},
ref,
): JSX.Element | null => {
@@ -46,20 +41,8 @@ const GridPanelSwitch = forwardRef<
query,
thresholds,
},
[PANEL_TYPES.LIST]:
dataSource === DataSource.LOGS
? {
selectedLogsFields: selectedLogFields || [],
query,
version: DEFAULT_ENTITY_VERSION, // As we don't support for Metrics, defaulting to v3
selectedTime,
}
: {
selectedTracesFields: selectedTracesFields || [],
query,
version: DEFAULT_ENTITY_VERSION, // As we don't support for Metrics, defaulting to v3
selectedTime,
},
[PANEL_TYPES.LIST]: null,
[PANEL_TYPES.PIE]: null,
[PANEL_TYPES.TRACE]: null,
[PANEL_TYPES.BAR]: {
data,
@@ -70,19 +53,7 @@ const GridPanelSwitch = forwardRef<
};
return result;
}, [
data,
options,
ref,
yAxisUnit,
thresholds,
panelData,
query,
dataSource,
selectedLogFields,
selectedTime,
selectedTracesFields,
]);
}, [data, options, ref, yAxisUnit, thresholds, panelData, query]);
const Component = getComponentForPanelType(panelType, dataSource) as FC<
PropsTypePropsMap[typeof panelType]

View File

@@ -2,9 +2,7 @@ import { StaticLineProps, ToggleGraphProps } from 'components/Graph/types';
import { UplotProps } from 'components/Uplot/Uplot';
import { GridTableComponentProps } from 'container/GridTableComponent/types';
import { GridValueComponentProps } from 'container/GridValueComponent/types';
import { LogsPanelComponentProps } from 'container/LogsPanelTable/LogsPanelComponent';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
import { TracesTableComponentProps } from 'container/TracesTableComponent/TracesTableComponent';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { ForwardedRef } from 'react';
import { Widgets } from 'types/api/dashboard/getAll';
@@ -40,7 +38,8 @@ export type PropsTypePropsMap = {
[PANEL_TYPES.VALUE]: GridValueComponentProps;
[PANEL_TYPES.TABLE]: GridTableComponentProps;
[PANEL_TYPES.TRACE]: null;
[PANEL_TYPES.LIST]: LogsPanelComponentProps | TracesTableComponentProps;
[PANEL_TYPES.PIE]: null;
[PANEL_TYPES.LIST]: null;
[PANEL_TYPES.BAR]: UplotProps & {
ref: ForwardedRef<ToggleGraphProps | undefined>;
};

View File

@@ -4,6 +4,7 @@ import { Input, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table/interface';
import saveAlertApi from 'api/alerts/save';
import DropDown from 'components/DropDown/DropDown';
import { listAlertMessage } from 'components/facingIssueBtn/util';
import {
DynamicColumnsKey,
TableDataSource,
@@ -358,6 +359,15 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
pagination={{
defaultCurrent: Number(paginationParam) || 1,
}}
facingIssueBtn={{
attributes: {
screen: 'Alert list page',
},
eventName: 'Alert: Facing Issues in alert',
buttonText: 'Facing issues with alerts?',
message: listAlertMessage,
onHoverText: 'Click here to get help with alerts',
}}
/>
</>
);

View File

@@ -3,6 +3,7 @@ import { Card, Col, Dropdown, Input, Row, TableColumnProps } from 'antd';
import { ItemType } from 'antd/es/menu/hooks/useItems';
import createDashboard from 'api/dashboard/create';
import { AxiosError } from 'axios';
import { dashboardListMessage } from 'components/facingIssueBtn/util';
import {
DynamicColumnsKey,
TableDataSource,
@@ -385,6 +386,15 @@ function DashboardsList(): JSX.Element {
dataSource={data}
onChange={handleChange}
showSorterTooltip
facingIssueBtn={{
attributes: {
screen: 'Dashboard list page',
},
eventName: 'Dashboard: Facing Issues in dashboard',
buttonText: 'Facing issues with dashboards?',
message: dashboardListMessage,
onHoverText: 'Click here to get help with dashboards',
}}
/>
</TableContainer>
</Card>

View File

@@ -85,8 +85,8 @@ function LogControls(): JSX.Element | null {
logs.map((log) => {
const timestamp =
typeof log.timestamp === 'string'
? dayjs(log.timestamp).format()
: dayjs(log.timestamp / 1e6).format();
? dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(log.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
return FlatLogData({
...log,

View File

@@ -531,8 +531,8 @@ function LogsExplorerViews({
logs.map((log) => {
const timestamp =
typeof log.timestamp === 'string'
? dayjs(log.timestamp).format()
: dayjs(log.timestamp / 1e6).format();
? dayjs(log.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(log.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
return FlatLogData({
timestamp,
@@ -608,6 +608,7 @@ function LogsExplorerViews({
className="periscope-btn"
onClick={handleToggleShowFormatOptions}
icon={<Sliders size={14} />}
data-testid="periscope-btn"
/>
{showFormatMenuItems && (

View File

@@ -0,0 +1,151 @@
import { render, RenderResult } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { VirtuosoMockContext } from 'react-virtuoso';
import i18n from 'ReactI18';
import store from 'store';
import LogsExplorerViews from '..';
import { logsQueryRangeSuccessNewFormatResponse } from './mock';
const logExplorerRoute = '/logs/logs-explorer';
const queryRangeURL = 'http://localhost/api/v3/query_range';
const lodsQueryServerRequest = (): void =>
server.use(
rest.post(queryRangeURL, (req, res, ctx) =>
res(ctx.status(200), ctx.json(logsQueryRangeSuccessResponse)),
),
);
// mocking the graph components in this test as this should be handled separately
jest.mock(
'container/TimeSeriesView/TimeSeriesView',
() =>
// eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name
function () {
return <div>Time Series Chart</div>;
},
);
jest.mock(
'container/LogsExplorerChart',
() =>
// eslint-disable-next-line func-names, @typescript-eslint/explicit-function-return-type, react/display-name
function () {
return <div>Histogram Chart</div>;
},
);
jest.mock('constants/panelTypes', () => ({
AVAILABLE_EXPORT_PANEL_TYPES: ['graph', 'table'],
}));
jest.mock('d3-interpolate', () => ({
interpolate: jest.fn(),
}));
jest.mock('hooks/queryBuilder/useGetExplorerQueryRange', () => ({
__esModule: true,
useGetExplorerQueryRange: jest.fn(),
}));
// Set up the specific behavior for useGetExplorerQueryRange in individual test cases
beforeEach(() => {
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
data: { payload: logsQueryRangeSuccessNewFormatResponse },
});
});
const renderer = (): RenderResult =>
render(
<MemoryRouter initialEntries={[logExplorerRoute]}>
<Provider store={store}>
<I18nextProvider i18n={i18n}>
<MockQueryClientProvider>
<QueryBuilderProvider>
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
<LogsExplorerViews selectedView={SELECTED_VIEWS.SEARCH} showHistogram />
</VirtuosoMockContext.Provider>
</QueryBuilderProvider>
</MockQueryClientProvider>
</I18nextProvider>
</Provider>
</MemoryRouter>,
);
describe('LogsExplorerViews -', () => {
it('render correctly with props - list and table', async () => {
lodsQueryServerRequest();
const { queryByText, queryByTestId } = renderer();
expect(queryByTestId('periscope-btn')).toBeInTheDocument();
await userEvent.click(queryByTestId('periscope-btn') as HTMLElement);
expect(document.querySelector('.menu-container')).toBeInTheDocument();
const menuItems = document.querySelectorAll('.menu-items .item');
expect(menuItems.length).toBe(3);
// switch to table view
// eslint-disable-next-line sonarjs/no-duplicate-string
await userEvent.click(queryByTestId('table-view') as HTMLElement);
expect(
queryByText(
'{"container_id":"container_id","container_name":"container_name","driver":"driver","eta":"2m0s","location":"frontend","log_level":"INFO","message":"Dispatch successful","service":"frontend","span_id":"span_id","trace_id":"span_id"}',
),
).toBeInTheDocument();
});
it('check isLoading state', async () => {
lodsQueryServerRequest();
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
data: { payload: logsQueryRangeSuccessNewFormatResponse },
isLoading: true,
isFetching: false,
});
const { queryByText, queryByTestId } = renderer();
// switch to table view
await userEvent.click(queryByTestId('table-view') as HTMLElement);
expect(
queryByText(
'Just a bit of patience, just a little bits enough ⎯ were getting your logs!',
),
).toBeInTheDocument();
});
it('check error state', async () => {
lodsQueryServerRequest();
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
data: { payload: logsQueryRangeSuccessNewFormatResponse },
isLoading: false,
isFetching: false,
isError: true,
});
const { queryByText, queryByTestId } = renderer();
expect(
queryByText('Something went wrong. Please try again or contact support.'),
).toBeInTheDocument();
// switch to table view
await userEvent.click(queryByTestId('table-view') as HTMLElement);
expect(
queryByText('Something went wrong. Please try again or contact support.'),
).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,51 @@
export const logsQueryRangeSuccessNewFormatResponse = {
data: {
result: [],
resultType: '',
newResult: {
status: 'success',
data: {
resultType: '',
result: [
{
queryName: 'A',
series: null,
list: [
{
timestamp: '2024-02-15T21:20:22Z',
data: {
attributes_bool: {},
attributes_float64: {},
attributes_int64: {},
attributes_string: {
container_id: 'container_id',
container_name: 'container_name',
driver: 'driver',
eta: '2m0s',
location: 'frontend',
log_level: 'INFO',
message: 'Dispatch successful',
service: 'frontend',
span_id: 'span_id',
trace_id: 'span_id',
},
body:
'2024-02-15T21:20:22.035Z\tINFO\tfrontend\tDispatch successful\t{"service": "frontend", "trace_id": "span_id", "span_id": "span_id", "driver": "driver", "eta": "2m0s"}',
id: 'id',
resources_string: {
'container.name': 'container_name',
},
severity_number: 0,
severity_text: '',
span_id: '',
trace_flags: 0,
trace_id: '',
},
},
],
},
],
},
},
},
};

View File

@@ -4,82 +4,53 @@ import { Table } from 'antd';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { OPERATORS, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { PANEL_TYPES } from 'constants/queryBuilder';
import Controls from 'container/Controls';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
import { tableStyles } from 'container/TracesExplorer/ListView/styles';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { Pagination } from 'hooks/queryPagination';
import { useLogsData } from 'hooks/useLogsData';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { FlatLogData } from 'lib/logs/flatLogData';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
Dispatch,
HTMLAttributes,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuid } from 'uuid';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { getLogPanelColumnsList } from './utils';
import { getLogPanelColumnsList, getNextOrPreviousItems } from './utils';
function LogsPanelComponent({
selectedLogsFields,
query,
selectedTime,
widget,
setRequestData,
queryResponse,
}: LogsPanelComponentProps): JSX.Element {
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const [pagination, setPagination] = useState<Pagination>({
offset: 0,
limit: query.builder.queryData[0].limit || 0,
});
const [requestData, setRequestData] = useState<GetQueryResultsProps>(() => {
const updatedQuery = { ...query };
updatedQuery.builder.queryData[0].pageSize = 10;
return {
query: updatedQuery,
graphType: PANEL_TYPES.LIST,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: globalSelectedTime,
tableParams: {
pagination,
},
};
limit: widget.query.builder.queryData[0].limit || 0,
});
useEffect(() => {
setRequestData({
...requestData,
globalSelectedInterval: globalSelectedTime,
setRequestData((prev) => ({
...prev,
tableParams: {
pagination,
},
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pagination]);
}));
}, [pagination, setRequestData]);
const [pageSize, setPageSize] = useState<number>(10);
const { selectedDashboard } = useDashboard();
const handleChangePageSize = (value: number): void => {
setPagination({
@@ -88,53 +59,35 @@ function LogsPanelComponent({
offset: value,
});
setPageSize(value);
const newQueryData = { ...requestData.query };
newQueryData.builder.queryData[0].pageSize = value;
const newRequestData = {
...requestData,
query: newQueryData,
tableParams: {
pagination,
},
};
setRequestData(newRequestData);
setRequestData((prev) => {
const newQueryData = { ...prev.query };
newQueryData.builder.queryData[0].pageSize = value;
return {
...prev,
query: newQueryData,
tableParams: {
pagination: {
limit: 0,
offset: value,
},
},
};
});
};
const { data, isFetching, isError } = useGetQueryRange(
{
...requestData,
globalSelectedInterval: globalSelectedTime,
selectedTime: selectedTime?.enum || 'GLOBAL_TIME',
variables: getDashboardVariables(selectedDashboard?.data.variables),
},
DEFAULT_ENTITY_VERSION,
{
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,
globalSelectedTime,
maxTime,
minTime,
requestData,
pagination,
selectedDashboard?.data.variables,
],
enabled: !!requestData.query && !!selectedLogsFields?.length,
},
);
const columns = getLogPanelColumnsList(selectedLogsFields);
const columns = getLogPanelColumnsList(widget.selectedLogFields);
const dataLength =
data?.payload?.data?.newResult?.data?.result[0]?.list?.length;
queryResponse.data?.payload?.data?.newResult?.data?.result[0]?.list?.length;
const totalCount = useMemo(() => dataLength || 0, [dataLength]);
const [firstLog, setFirstLog] = useState<ILog>();
const [lastLog, setLastLog] = useState<ILog>();
const { logs } = useLogsData({
result: data?.payload.data.newResult.data.result,
result: queryResponse.data?.payload.data.newResult.data.result,
panelType: PANEL_TYPES.LIST,
stagedQuery: query,
stagedQuery: widget.query,
});
useEffect(() => {
@@ -167,92 +120,86 @@ function LogsPanelComponent({
);
const isOrderByTimeStamp =
query.builder.queryData[0].orderBy.length > 0 &&
query.builder.queryData[0].orderBy[0].columnName === 'timestamp';
widget.query.builder.queryData[0].orderBy.length > 0 &&
widget.query.builder.queryData[0].orderBy[0].columnName === 'timestamp';
const handlePreviousPagination = (): void => {
if (isOrderByTimeStamp) {
setRequestData({
...requestData,
setRequestData((prev) => ({
...prev,
query: {
...requestData.query,
...prev.query,
builder: {
...requestData.query.builder,
...prev.query.builder,
queryData: [
{
...requestData.query.builder.queryData[0],
...prev.query.builder.queryData[0],
filters: {
...requestData.query.builder.queryData[0].filters,
...prev.query.builder.queryData[0].filters,
items: [
{
id: uuid(),
key: {
key: 'id',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: OPERATORS['>'],
value: firstLog?.id || '',
},
...getNextOrPreviousItems(
prev.query.builder.queryData[0].filters.items,
'PREV',
firstLog,
),
],
},
limit: 0,
offset: 0,
},
],
},
},
});
return;
}));
}
if (!isOrderByTimeStamp) {
setPagination({
...pagination,
limit: 0,
offset: pagination.offset - pageSize,
});
}
setPagination({
...pagination,
limit: 0,
offset: pagination.offset - pageSize,
});
};
const handleNextPagination = (): void => {
if (isOrderByTimeStamp) {
setRequestData({
...requestData,
setRequestData((prev) => ({
...prev,
query: {
...requestData.query,
...prev.query,
builder: {
...requestData.query.builder,
...prev.query.builder,
queryData: [
{
...requestData.query.builder.queryData[0],
...prev.query.builder.queryData[0],
filters: {
...requestData.query.builder.queryData[0].filters,
...prev.query.builder.queryData[0].filters,
items: [
{
id: uuid(),
key: {
key: 'id',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: OPERATORS['<'],
value: lastLog?.id || '',
},
...getNextOrPreviousItems(
prev.query.builder.queryData[0].filters.items,
'NEXT',
lastLog,
),
],
},
limit: 0,
offset: 0,
},
],
},
},
});
return;
}));
}
if (!isOrderByTimeStamp) {
setPagination({
...pagination,
limit: 0,
offset: pagination.offset + pageSize,
});
}
setPagination({
...pagination,
limit: 0,
offset: pagination.offset + pageSize,
});
};
if (isError) {
if (queryResponse.isError) {
return <div>{SOMETHING_WENT_WRONG}</div>;
}
@@ -265,19 +212,19 @@ function LogsPanelComponent({
tableLayout="fixed"
scroll={{ x: `calc(50vw - 10px)` }}
sticky
loading={isFetching}
loading={queryResponse.isFetching}
style={tableStyles}
dataSource={flattenLogData}
columns={columns}
onRow={handleRow}
/>
</div>
{!query.builder.queryData[0].limit && (
{!widget.query.builder.queryData[0].limit && (
<div className="controller">
<Controls
totalCount={totalCount}
perPageOptions={PER_PAGE_OPTIONS}
isLoading={isFetching}
isLoading={queryResponse.isFetching}
offset={pagination.offset}
countPerPage={pageSize}
handleNavigatePrevious={handlePreviousPagination}
@@ -301,13 +248,12 @@ function LogsPanelComponent({
}
export type LogsPanelComponentProps = {
selectedLogsFields: Widgets['selectedLogFields'];
query: Query;
selectedTime?: timePreferance;
};
LogsPanelComponent.defaultProps = {
selectedTime: undefined,
setRequestData: Dispatch<SetStateAction<GetQueryResultsProps>>;
queryResponse: UseQueryResult<
SuccessResponse<MetricRangePayloadProps, unknown>,
Error
>;
widget: Widgets;
};
export default LogsPanelComponent;

View File

@@ -1,10 +1,15 @@
import { ColumnsType } from 'antd/es/table';
import { Typography } from 'antd/lib';
import { OPERATORS } from 'constants/queryBuilder';
// import Typography from 'antd/es/typography/Typography';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { ReactNode } from 'react';
import { Widgets } from 'types/api/dashboard/getAll';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
export const getLogPanelColumnsList = (
selectedLogFields: Widgets['selectedLogFields'],
@@ -36,3 +41,49 @@ export const getLogPanelColumnsList = (
return [...initialColumns, ...columns];
};
export const getNextOrPreviousItems = (
items: TagFilterItem[],
direction: 'NEXT' | 'PREV',
log?: ILog,
): TagFilterItem[] => {
const nextItem = {
id: uuid(),
key: {
key: 'id',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: OPERATORS['<'],
value: log?.id || '',
};
const prevItem = {
id: uuid(),
key: {
key: 'id',
type: '',
dataType: DataTypes.String,
isColumn: true,
},
op: OPERATORS['>'],
value: log?.id || '',
};
let index = items.findIndex((item) => item.op === OPERATORS['<']);
if (index === -1) {
index = items.findIndex((item) => item.op === OPERATORS['>']);
}
if (index === -1) {
if (direction === 'NEXT') {
return [...items, nextItem];
}
return [...items, prevItem];
}
const newItems = [...items];
if (direction === 'NEXT') {
newItems[index] = nextItem;
} else {
newItems[index] = prevItem;
}
return newItems;
};

View File

@@ -8,6 +8,7 @@ export const getWidgetQueryBuilder = ({
title = '',
panelTypes,
yAxisUnit = '',
fillSpans = false,
id,
}: GetWidgetQueryBuilderProps): Widgets => ({
description: '',
@@ -24,4 +25,5 @@ export const getWidgetQueryBuilder = ({
softMin: null,
selectedLogFields: [],
selectedTracesFields: [],
fillSpans,
});

View File

@@ -70,6 +70,7 @@ function DBCall(): JSX.Element {
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'reqps',
id: SERVICE_CHART_ID.dbCallsRPS,
fillSpans: false,
}),
[servicename, tagFilterItems],
);
@@ -89,7 +90,8 @@ function DBCall(): JSX.Element {
title: GraphTitle.DATABASE_CALLS_AVG_DURATION,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ms',
id: SERVICE_CHART_ID.dbCallsAvgDuration,
id: GraphTitle.DATABASE_CALLS_AVG_DURATION,
fillSpans: true,
}),
[servicename, tagFilterItems],
);
@@ -112,8 +114,6 @@ function DBCall(): JSX.Element {
<Card data-testid="database_call_rps">
<GraphContainer>
<Graph
fillSpans={false}
name="database_call_rps"
widget={databaseCallsRPSWidget}
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
onGraphClickHandler(setSelectedTimeStamp)(
@@ -147,8 +147,6 @@ function DBCall(): JSX.Element {
<Card data-testid="database_call_avg_duration">
<GraphContainer>
<Graph
fillSpans
name="database_call_avg_duration"
widget={databaseCallsAverageDurationWidget}
headerMenuList={MENU_ITEMS}
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {

View File

@@ -18,7 +18,7 @@ import { useParams } from 'react-router-dom';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { GraphTitle, legend, MENU_ITEMS, SERVICE_CHART_ID } from '../constant';
import { GraphTitle, legend, MENU_ITEMS } from '../constant';
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
import { Card, GraphContainer, Row } from '../styles';
import { Button } from './styles';
@@ -60,7 +60,7 @@ function External(): JSX.Element {
title: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: '%',
id: SERVICE_CHART_ID.externalCallErrorPercentage,
id: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE,
}),
[servicename, tagFilterItems],
);
@@ -86,7 +86,8 @@ function External(): JSX.Element {
title: GraphTitle.EXTERNAL_CALL_DURATION,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ms',
id: SERVICE_CHART_ID.externalCallDuration,
id: GraphTitle.EXTERNAL_CALL_DURATION,
fillSpans: true,
}),
[servicename, tagFilterItems],
);
@@ -108,7 +109,8 @@ function External(): JSX.Element {
title: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'reqps',
id: SERVICE_CHART_ID.externalCallRPSByAddress,
id: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS,
fillSpans: true,
}),
[servicename, tagFilterItems],
);
@@ -130,7 +132,8 @@ function External(): JSX.Element {
title: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ms',
id: SERVICE_CHART_ID.externalCallDurationByAddress,
id: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS,
fillSpans: true,
}),
[servicename, tagFilterItems],
);
@@ -155,9 +158,7 @@ function External(): JSX.Element {
<Card data-testid="external_call_error_percentage">
<GraphContainer>
<Graph
fillSpans={false}
headerMenuList={MENU_ITEMS}
name="external_call_error_percentage"
widget={externalCallErrorWidget}
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
onGraphClickHandler(setSelectedTimeStamp)(
@@ -192,8 +193,6 @@ function External(): JSX.Element {
<Card data-testid="external_call_duration">
<GraphContainer>
<Graph
fillSpans
name="external_call_duration"
headerMenuList={MENU_ITEMS}
widget={externalCallDurationWidget}
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
@@ -230,8 +229,6 @@ function External(): JSX.Element {
<Card data-testid="external_call_rps_by_address">
<GraphContainer>
<Graph
fillSpans
name="external_call_rps_by_address"
widget={externalCallRPSWidget}
headerMenuList={MENU_ITEMS}
onClickHandler={(xValue, yValue, mouseX, mouseY): Promise<void> =>
@@ -267,10 +264,8 @@ function External(): JSX.Element {
<Card data-testid="external_call_duration_by_address">
<GraphContainer>
<Graph
name="external_call_duration_by_address"
widget={externalCallDurationAddressWidget}
headerMenuList={MENU_ITEMS}
fillSpans
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
onGraphClickHandler(setSelectedTimeStamp)(
xValue,

View File

@@ -19,13 +19,10 @@ 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';
import { useDispatch } from 'react-redux';
import { useLocation, useParams } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { v4 as uuid } from 'uuid';
import { GraphTitle, SERVICE_CHART_ID } from '../constant';
@@ -49,9 +46,6 @@ import {
} from './util';
function Application(): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { servicename: encodedServiceName } = useParams<IServiceName>();
const servicename = decodeURIComponent(encodedServiceName);
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
@@ -59,10 +53,6 @@ function Application(): JSX.Element {
const { queries } = useResourceAttribute();
const urlQuery = useUrlQuery();
const selectedTags = useMemo(
() => (convertRawQueriesToTraceSelectedTags(queries) as Tags[]) || [],
[queries],
);
const isSpanMetricEnabled = useFeatureFlag(FeatureKeys.USE_SPAN_METRICS)
?.active;
@@ -94,7 +84,7 @@ function Application(): JSX.Element {
isLoading: topLevelOperationsIsLoading,
isError: topLevelOperationsIsError,
} = useQuery<ServiceDataProps>({
queryKey: [servicename, minTime, maxTime, selectedTags],
queryKey: [servicename],
queryFn: getTopLevelOperations,
});
@@ -116,49 +106,41 @@ function Application(): JSX.Element {
[servicename, topLevelOperations],
);
const operationPerSecWidget = useMemo(
() =>
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: operationPerSec({
servicename,
tagFilterItems,
topLevelOperations: topLevelOperationsRoute,
}),
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.RATE_PER_OPS,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ops',
id: SERVICE_CHART_ID.rps,
const operationPerSecWidget = getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: operationPerSec({
servicename,
tagFilterItems,
topLevelOperations: topLevelOperationsRoute,
}),
[servicename, tagFilterItems, topLevelOperationsRoute],
);
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.RATE_PER_OPS,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ops',
id: SERVICE_CHART_ID.rps,
});
const errorPercentageWidget = useMemo(
() =>
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: errorPercentage({
servicename,
tagFilterItems,
topLevelOperations: topLevelOperationsRoute,
}),
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.ERROR_PERCENTAGE,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: '%',
id: SERVICE_CHART_ID.errorPercentage,
const errorPercentageWidget = getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: errorPercentage({
servicename,
tagFilterItems,
topLevelOperations: topLevelOperationsRoute,
}),
[servicename, tagFilterItems, topLevelOperationsRoute],
);
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.ERROR_PERCENTAGE,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: '%',
id: SERVICE_CHART_ID.errorPercentage,
});
const onDragSelect = useCallback(
(start: number, end: number) => {

View File

@@ -89,8 +89,6 @@ function ApDexMetrics({
return (
<Graph
name="apdex"
fillSpans={false}
widget={apDexMetricsWidget}
onDragSelect={onDragSelect}
onClickHandler={handleGraphClick('ApDex')}

View File

@@ -50,7 +50,6 @@ function ApDexTraces({
return (
<Graph
name="apdex"
widget={apDexTracesWidget}
onDragSelect={onDragSelect}
onClickHandler={handleGraphClick('ApDex')}

View File

@@ -1,3 +1,4 @@
import { Skeleton } from 'antd';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { FeatureKeys } from 'constants/features';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -46,28 +47,24 @@ function ServiceOverview({
[isSpanMetricEnable, queries],
);
const latencyWidget = useMemo(
() =>
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: latency({
servicename,
tagFilterItems,
isSpanMetricEnable,
topLevelOperationsRoute,
}),
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.LATENCY,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ns',
id: SERVICE_CHART_ID.latency,
const latencyWidget = getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: latency({
servicename,
tagFilterItems,
isSpanMetricEnable,
topLevelOperationsRoute,
}),
[servicename, isSpanMetricEnable, topLevelOperationsRoute, tagFilterItems],
);
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.LATENCY,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ns',
id: SERVICE_CHART_ID.latency,
});
const isQueryEnabled =
!topLevelOperationsIsLoading && topLevelOperationsRoute.length > 0;
@@ -88,15 +85,23 @@ function ServiceOverview({
</Button>
<Card data-testid="service_latency">
<GraphContainer>
<Graph
name="service_latency"
onDragSelect={onDragSelect}
widget={latencyWidget}
onClickHandler={handleGraphClick('Service')}
isQueryEnabled={isQueryEnabled}
fillSpans={false}
version={ENTITY_VERSION_V4}
/>
{topLevelOperationsIsLoading && (
<Skeleton
style={{
height: '100%',
padding: '16px',
}}
/>
)}
{!topLevelOperationsIsLoading && (
<Graph
onDragSelect={onDragSelect}
widget={latencyWidget}
onClickHandler={handleGraphClick('Service')}
isQueryEnabled={isQueryEnabled}
version={ENTITY_VERSION_V4}
/>
)}
</GraphContainer>
</Card>
</>

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