Compare commits

...

128 Commits

Author SHA1 Message Date
Prashant Shahi
0ead935886 Merge branch 'develop' into release/v0.36.2 2023-12-29 22:28:38 +05:45
Raj Kamal Singh
fd9a502012 fix: opamp server: do not panic if config generation fails (#4307) 2023-12-29 21:55:38 +05:30
Prashant Shahi
6530873994 Merge branch 'main' into release/v0.36.2 2023-12-29 19:12:41 +05:30
Prashant Shahi
c9c0bd38be chore(signoz): 📌 pin versions: SigNoz 0.36.2
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-12-29 19:24:12 +05:45
Prashant Shahi
9ac22fcb10 Merge branch 'develop' into release/v0.36.1 2023-12-29 19:06:53 +05:45
Raj Kamal Singh
86ff865842 Fix: logs pipelines: ignore coalesce op when generating nil check for add value expressions (#4305)
* chore: panic if agent config recommendation can't be generated

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

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

* refactor: updated the css

* refactor: removed overflow hidden
2023-12-29 15:52:35 +05:30
Prashant Shahi
79c05d8fa8 Merge pull request #4304 from SigNoz/release/v0.36.1
Release/v0.36.1
2023-12-29 15:39:38 +05:30
Prashant Shahi
019bc8c1df chore(signoz): 📌 pin versions: SigNoz 0.36.1, SigNoz OtelCollector 0.88.6
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-12-29 15:44:27 +05:45
Yunus M
d688399b91 fix: overflow issue in service page graphs (#4300) 2023-12-29 13:11:23 +05:30
Rajat Dabade
cfc239e3c9 refactor: added 3 days global timestamp (#4290)
* refactor: added 3 days global timestamp

* refactor: updated 3 days data in right container

* refactor: common function for calculate start and end time
2023-12-29 12:54:02 +05:30
Srikanth Chekuri
3572baa5eb fix: adjust the start and end more accurately (#4263)
* fix: adjust the start and end more accurately
Part of https://github.com/SigNoz/signoz/issues/1327

* chore: cache friendly timestamps
2023-12-29 12:35:22 +05:30
Rajat Dabade
ff26c5f69c fix: make fill gap persistent (#4302) 2023-12-29 12:23:27 +05:30
Srikanth Chekuri
9230f2442f fix: normalize label name to follow prometheus spec (#4264) 2023-12-28 20:22:42 +05:30
Raj Kamal Singh
7fed80b145 Fix: log pipelines contains and ncontains filters should be case insensitive (#4299)
* chore: add test validating contains and ncontains in pipeline filter are case insensitive

* chore: qb2expr: translate contains and ncontains to case insensitive comparison

* chore: minor cleanup
2023-12-28 19:44:17 +05:30
Yunus M
a268bb910c fix: update logic to handle step paths in Kubernetes APM flow (#4297)
* fix: update logic to handle step paths in kubernetes APM flow

* fix: don't reset service name on data source component mount
2023-12-28 18:30:41 +05:30
Rajat Dabade
fbbe0bef86 [Fix]: live view details modal disappear bug (#4249) 2023-12-28 16:02:55 +05:30
Raj Kamal Singh
bcd6ac47f7 Fix: Logs: Pipelines: add nil check for grok parser parseFrom field in generated collector config (#4286)
* chore: add test validating grok parser doesn't spam logs if parse from is missing

* chore: add nil check for grok parser parseFrom
2023-12-28 11:03:31 +05:30
Raj Kamal Singh
ec27916fa5 Fix: QS: Log Pipelines: generate correct nil checks for operators referencing fields like attributes["http.status.code"] (#4284)
* chore: add test validating that using paths like attributes["http.method"] works

* chore: refactor nil checks on processor fields generated for pipelines

* chore: get nil checks working on paths like attributes["http.method"]

* chore: use parsed AST for generating nil checks for add operator value expressions

* chore: some cleanup

* chore: some more cleanup

* chore: some more cleanup

* chore: some more cleanup

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2023-12-28 10:31:36 +05:30
Srikanth Chekuri
263ac9fa5a fix: ignore timestamp from the subquery result for value panel (#4293) 2023-12-27 19:25:24 +05:30
Rajat Dabade
e3b2882811 [Fix]: scroll bar issue in table panel (#4043) 2023-12-27 10:56:36 +05:30
Rajat Dabade
63efb2b25a [Fix]: threshold table issue (#4247)
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2023-12-27 10:38:22 +05:30
Srikanth Chekuri
788a38d39c chore: update CODEOWNERS (#4285) 2023-12-26 18:54:49 +05:30
Sanjib Kumar Sah
bff39daef0 fix: word overflow for large number in flamegraph (#4122)
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2023-12-26 15:41:01 +05:30
CheetoDa
f2521b4c49 feat: onboarding flow updates and new flow addition (#4222)
* feat: onboarding flow updates and new flow addition

* feat: added reactjs files

* feat: added content for reactjs and others

* chore: implemented feedback

---------

Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
Co-authored-by: Yunus M <myounis.ar@live.com>
2023-12-26 10:43:55 +05:30
Ankit Nayan
16b846006a Merge pull request #4278 from SigNoz/release/v0.36.0
Release/v0.36.0
2023-12-22 15:18:38 +05:30
Prashant Shahi
0ea8e8e6b8 Merge branch 'develop' into release/v0.36.0 2023-12-22 15:12:32 +05:45
Prashant Shahi
da0ea7eb49 fix(dockerfile): copy invitation email templates for EE build (#4274)
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-12-22 14:55:30 +05:30
Prashant Shahi
ca41d7011e chore(signoz): 📌 pin versions: SigNoz 0.36.0, SigNoz OtelCol 0.88.4
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-12-22 14:37:29 +05:45
Vishal Sharma
f15e8f2fed Update new maintainers (#4277) 2023-12-22 12:46:21 +05:30
Yunus M
2e0fdbb498 feat: show rate limit warning in services page when total RPS > 100 (#4266)
* feat: show rate limit warning in services page when total rps > 100

* feat: update message

* feat: rate limit message should be shown only to cloud users on trail

---------

Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2023-12-22 00:51:37 +05:30
Vishal Sharma
585d6e2a21 chore: send event only if there are dashboards or alerts (#4253) 2023-12-21 19:06:13 +05:30
Vishal Sharma
d93cc767a6 chore: update active user event (#4271)
Merged on request by Visha
2023-12-21 19:05:21 +05:30
Prashant Shahi
a363b98657 feat(query-service): support for SMTP service (#4258)
* feat(query-service): support for SMTP service

Signed-off-by: Prashant Shahi <prashant@signoz.io>

* feat(query-service): smtp minor fixes

Signed-off-by: Prashant Shahi <prashant@signoz.io>

* chore: fix smtp and add email template

* chore: update template

* chore(smpt-service): configurable invite email template path

Signed-off-by: Prashant Shahi <prashant@signoz.io>

---------

Signed-off-by: Prashant Shahi <prashant@signoz.io>
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2023-12-21 18:27:30 +05:30
Vishal Sharma
2031a014a7 Revert "Fix: lettercase bug in query-builder (#4120)" (#4255)
This reverts commit a6b6abf1a7.

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2023-12-21 13:36:56 +05:30
Raj Kamal Singh
43d5ee9651 FE: Log Detail View: Support for dots in log attributes (#4252)
* chore: add support for showing attrib names containing dots in log details

* chore: add support for field names with dot in filter actions

* chore: minor cleanup

* chore: add support for filtering by clicking fields with dots in log details
2023-12-21 13:21:20 +05:30
Nityananda Gohain
f8bb42a13c fix: dot support in attribute name (#4121)
* fix: dot support

* fix: column name updated for materialized columns

* fix: tests updated

* fix: comments updated

* fix: enrichment test updated

* fix: dont return underscore fields in the fields API

* fix: update fields function changed to support default instead of materialized

* fix: updated how formulas are built and test added

* fix: don't create index for bool attributes

* fix: support for limit queries updated
2023-12-21 12:11:35 +05:30
CheetoDa
1be4731710 feat: added frontend for newly addded flows (#4151)
* feat: added frontend for newly addded flows

* chore: added content for flows

* chore: updated content for dotnet docs

* chore: updated go instrumentation and http logs content

* fix: removed console log and return true

* fix: quickstart by default and cloudwatch logs

* fix: removed console log
2023-12-20 20:16:21 +05:30
Yunus M
90b8959045 feat: gh-4211: show copy and share invite link message next to pending invites (#4260)
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2023-12-20 18:18:27 +05:30
Rajat Dabade
f487c1956b [Fix]: range issue in uplot charts (#4262)
* fix: range issue in uplot charts

* refactor: updated logic to strickly check is parsed value is number

* refactor: safe check for number value
2023-12-20 17:36:13 +05:30
Rajat Dabade
6b2f03d43f [Refactor]: Added tag and datatype in options for query builder (#3555)
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
Co-authored-by: Palash Gupta <palashgdev@gmail.com>
2023-12-20 15:31:37 +05:30
Raj Kamal Singh
581bd07b35 Fix: query service: escape dollars in collector config generated for pipelines. (#4242)
* chore: add test validating pipeline config containing dollar works

* chore: collector simulator: use fileprovider with expandconverter like actual collector

* chore: get tests passing

* chore: take a configGenerator func in simulation and unify config generation logic

* fix: escape $ in config generated for log pipelines
2023-12-19 21:24:53 +05:30
Vikrant Gupta
c5cba68b53 fix: suggestions not populating without space (#4257) 2023-12-19 21:08:55 +05:30
Sanjib Kumar Sah
a6b6abf1a7 Fix: lettercase bug in query-builder (#4120)
* fix: lettercase bug in query-builder

* fix: suggesting similar value when entering diffrnt case value

---------

Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2023-12-19 16:03:30 +05:30
Yunus M
7526888886 feat: show trace details on hover (#4241)
* feat: show trace details on hover

* feat: handle sider anchor styles in dark and light mode
2023-12-18 17:03:38 +05:30
Vikrant Gupta
ce8fdd509b fix: [GH-3585]: operator suggestion should work on the substring typed in by the user (#4246)
* fix: [GH-3585]: operator suggestion logic

* fix: [GH-3585]: address review comments

* fix: [GH-3585]: address review comments
2023-12-18 13:51:39 +05:30
Nityananda Gohain
2baa6028b5 fix: enrichment for json in alerts and disable timestamp adjustment (#4240)
* fix: enrichment for json in alerts and disable timestamp adjustment

* fix: tests updated
2023-12-16 17:06:49 +05:30
Srikanth Chekuri
8e653f9500 fix: do not use grouping sets when there is no tags for group by (#4237) 2023-12-15 18:56:05 +05:30
Rajat Dabade
cb1a823f91 fix: the min data point as 0 in time series (#4203) 2023-12-15 17:44:13 +05:30
Yunus M
c0b0920901 Round off x axis max timestamp to T - 1 min and 0th second (#4238)
* feat: round of x axis max timestamp to t - 1 min and 0th second

* feat: set min width of time selection dropdown to 120px

* feat: update logic comment

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2023-12-15 17:29:54 +05:30
Rajat Dabade
77b4e71543 [refactor]: persistance of sorting and page in table (#4221) 2023-12-15 17:23:01 +05:30
Palash Gupta
9d44ce3ee2 feat: on create alerts query range format is called to fill the variables (#4204) 2023-12-15 17:08:35 +05:30
Yunus M
1d014ab4f7 Rearrange variables (#4187)
* feat: variable re-arrange

* feat: update variable update from dashboard description

* feat: update variable update from dashboard description

* feat: update custom variable dropdown values on change

* feat: handle dependent value updates to dashboard description

* feat: handle dependent 0th order variable update

* feat: update variable item test

* feat: transform variables data to support rearraging

* feat: update modal import

* feat: remove console logs

* feat: ts-ignore

* feat: show variable name in delete modal
2023-12-15 13:10:02 +05:30
Yunus M
418ab67d50 Uplot time range (#4144)
* feat: show range bound chart based on the selected time range

* feat: handle no data

* feat: show bigger point if only data point exists

* feat: show bigger point if only data point exists

* feat: widget ui fixes

* feat: no data - full view fix

* fix: show closed point on hover

* feat: handle widget time preference in case of dashboard, edit view, full view and chart preview
2023-12-14 22:56:25 +05:30
Vikrant Gupta
7efe907757 fix: [GH-3790]: timerange not working for different users (#4192) 2023-12-14 22:14:58 +05:30
Rajat Dabade
1d1154aa8c [Refactor]: added tooltip for graph manager (#4236) 2023-12-14 18:10:52 +05:30
Nityananda Gohain
a16fca6376 fix: remove timestamp roundup for logs list api call (#4229)
* fix: remove timestamp roundup for logs list api call

* fix: test updated
2023-12-14 16:52:02 +05:30
Rajat Dabade
9c1ea0cde9 refactor: pop for unsaved changes (#4188) 2023-12-14 11:43:02 +05:30
Nityananda Gohain
ec500831ef feat: upgrade clickhouse to 23.11.1 (#4225) 2023-12-14 11:22:20 +05:30
Prashant Shahi
fcbf82c2f3 Merge pull request #4232 from SigNoz/release/v0.35.1
Release/v0.35.1
2023-12-13 22:42:29 +05:30
Prashant Shahi
a805eb7533 chore(signoz): 📌 pin versions: SigNoz 0.35.1, SigNoz OtelCollector 0.88.3
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-12-13 21:13:09 +05:45
Vishal Sharma
a8edc4fd95 chore: better error handling in getAlertsInfo (#4230) 2023-12-13 19:12:35 +05:30
Vishal Sharma
c66c8c2823 chore: add new dashboard/alerts info events (#4214)
* chore: add new dashboard/alerts info events
2023-12-13 18:14:55 +05:30
Srikanth Chekuri
c7b59d4405 chore: update .github/CODEOWNERS (#3539)
* chore: update .github/CODEOWNERS

* chore: remove team
2023-12-13 17:53:18 +05:30
Vishal Sharma
f56b5cb971 fix: createPAT method to return id (#4078)
Update token expiry validations
2023-12-13 17:05:59 +05:30
Srikanth Chekuri
29b1344557 chore: add prepare query for cumulative/unspecified timeseries (#4166) 2023-12-13 16:40:17 +05:30
Rajat Dabade
55664872bd [Feat]: only clicked legend graph visible (#4226)
* refactor: only clicked legend graph visible

* refactor: fix graph manage toggle issue
2023-12-13 16:26:25 +05:30
Yunus M
221861230a feat: track channel click event in support page (#4217) 2023-12-13 01:18:19 +05:30
Yunus M
8b1a781f58 feat: pass abort signal to cancel api request on query-key change or … (#4193)
* feat: pass abort signal to cancel api request on query-key change or dashboard unmount

* fix: transformIgnorePatterns axios

* fix: remove axios types

* feat: handle error type from dashboardAPI response

* feat: remove console.log
2023-12-12 17:18:57 +05:30
Yunus M
b557ca5519 fix: use updated query value on test query, restrict direct commit to… (#4210)
* fix: use updated query value on test query, restrict direct commit to develop,main

* fix: reset error preview on success
2023-12-12 16:30:22 +05:30
Palash Gupta
e557ff273f test: metrics application test are added (#4137)
* test: metrics application test are added

* fix: getTopOperationList is moved under __mocks__
2023-12-12 14:16:06 +05:30
Yunus M
3c284fc9ee Revert "fix: variable edit flow - use updated query value on test query" (#4207)
This reverts commit bcebe050b1.
2023-12-12 11:38:06 +05:30
Yunus M
bcebe050b1 fix: variable edit flow - use updated query value on test query 2023-12-12 11:19:06 +05:30
Srikanth Chekuri
9360c61dca chore: update BuilderQuery struct and add PrepareTimeseriesFilterQuery (#4165) 2023-12-12 07:24:33 +05:30
guangwu
fb1dbdc05e chore: use bytes.Equal instead (#4201) 2023-12-11 18:45:47 +05:30
Rajat Dabade
6170b2c5dc [Refactor]: added percent 0 - 100 in yaxis for alerts (#4173) 2023-12-11 18:34:24 +05:30
Srikanth Chekuri
9826ab04b3 chore: add new endpoint for variable replacement (#4191)
Co-authored-by: Palash Gupta <palashgdev@gmail.com>
2023-12-11 17:46:08 +05:30
Srikanth Chekuri
fd9566d471 fix: incorrect alert description and summary for prom rules (#4190) 2023-12-11 16:09:28 +05:30
Raj Kamal Singh
3a1e8d523a Fix: qs: allow saving pipelines without connected agents (#4189)
* chore: add test validating pipelines can be saved without connected agents

* chore: allow pipelines to be saved without connected agents
2023-12-09 10:17:06 +05:30
Gaurav Sharma
6dd34a7f29 Fix/2967 (#4071) 2023-12-08 12:37:19 +05:30
Avijeet Pandey
170e5e1686 fix(FE): Fixes the background color of the dashboards full screen view as per the mode selected i.e dark or light mode (#4175)
* fix: full screen bg color of graphs as per dark mode

* fix: colors from the constants
2023-12-08 11:53:56 +05:30
Prashant Shahi
16502feaad Merge pull request #4177 from SigNoz/release/v0.35.0
Release/v0.35.0
2023-12-06 22:15:19 +05:30
Prashant Shahi
09d579311e chore(signoz): 📌 pin versions: SigNoz 0.35.0
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-12-06 22:10:36 +05:45
dependabot[bot]
8072fede85 chore(deps): bump tj-actions/branch-names in /.github/workflows (#4164)
Bumps [tj-actions/branch-names](https://github.com/tj-actions/branch-names) from 5.1 to 7.0.7.
- [Release notes](https://github.com/tj-actions/branch-names/releases)
- [Changelog](https://github.com/tj-actions/branch-names/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/branch-names/compare/v5.1...v7.0.7)

---
updated-dependencies:
- dependency-name: tj-actions/branch-names
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-06 08:40:47 +05:30
Raj Kamal Singh
112783d618 Feat: fe: logs pipelines severity parsing processor (#4149) 2023-12-05 18:30:46 +05:30
Yunus M
4644b1c200 fix: custom variables options are not populated (#4154) 2023-12-05 16:09:50 +05:30
Yunus M
bb09c84679 fix: text formatting issues and upgrade button style updates (#4141) 2023-12-05 11:15:08 +05:30
Raj Kamal Singh
fc5f0fbf9e Feat: fe: logs pipelines timestamp parsing processor (#4106)
* chore: add processor config for time parsing processor

* chore: add select input and processor fields with enumerated options

* feat: set timestamp layout to default value when layout_type is changed

* chore: minor cleanup

* chore: some more cleanup

* chore: some more cleanup

* chore: get jest passing

* chore: normalize ts in pipelines previews input and output

* chore: some cleanup

* fix: set correct field id for timestamp format input
2023-12-04 15:57:14 +05:30
Ankit Nayan
d6f0559adc fix: ee/query-service/Dockerfile to reduce vulnerabilities (#4145)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE318-OPENSSL-6032386
- https://snyk.io/vuln/SNYK-ALPINE318-OPENSSL-6032386
- https://snyk.io/vuln/SNYK-ALPINE318-OPENSSL-6055795
- https://snyk.io/vuln/SNYK-ALPINE318-OPENSSL-6055795

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2023-12-03 08:00:33 +05:30
Yunus M
0d7f7df76c fix: pkg/query-service/Dockerfile to reduce vulnerabilities (#4146)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE318-OPENSSL-6032386
- https://snyk.io/vuln/SNYK-ALPINE318-OPENSSL-6032386
- https://snyk.io/vuln/SNYK-ALPINE318-OPENSSL-6055795
- https://snyk.io/vuln/SNYK-ALPINE318-OPENSSL-6055795

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-12-03 07:53:15 +05:30
Vikrant Gupta
7104d8e0f5 feat: [GH-4093]: move the name to the left and the actions to the right for widget header (#4130) 2023-12-02 14:47:08 +05:30
Yunus M
a20693fa9f fix: add onboarding complete event (#4140) 2023-12-01 21:55:21 +05:30
Rajat Dabade
0b991331d7 [Fix]: threshold in alerts (#4074) 2023-12-01 18:16:25 +05:30
Raj Kamal Singh
aad44a1037 Feat: QS: logs pipelines severity parsing processor (#4132)
* chore: update test helper for making logs

* chore: add happy case test for severity parser

* feat: get severity parsing processor working and add more happy path tests

* chore: add test for validating severity parser doesn't spam collector logs

* chore: add if condition to generated severity_parser operators

* chore: add postablePipeline validation for severity parser

* chore: minor cleanup in tests

* chore: allow trace and fatal in severity mappings
2023-12-01 17:22:22 +05:30
Yunus M
3e29161fea fix: update logic for handling data for uplot charts (#4131)
* fix: update logic for handling data for uplot charts

* fix: hide tooltip if no tooltip values present

* fix: remove console log
2023-12-01 17:08:24 +05:30
Rajat Dabade
b616dca52d fix: the full view in service layer (#4133)
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2023-12-01 16:42:19 +05:30
dependabot[bot]
be519666a3 chore(deps): bump @adobe/css-tools from 4.3.1 to 4.3.2 in /frontend (#4134)
Bumps [@adobe/css-tools](https://github.com/adobe/css-tools) from 4.3.1 to 4.3.2.
- [Changelog](https://github.com/adobe/css-tools/blob/main/History.md)
- [Commits](https://github.com/adobe/css-tools/commits)

---
updated-dependencies:
- dependency-name: "@adobe/css-tools"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 11:14:14 +05:30
Rajat Dabade
a48edac13b fix: the default query issue in log (#4108) 2023-11-30 18:56:09 +05:30
Palash Gupta
0a77c7ab85 fix: onRun Query offset is made zero (#4083) 2023-11-30 18:41:26 +05:30
Prashant Shahi
9fb32acf6d ci(staging-deployment): ✏️ fix command to pull latest schema migrator image (#4123)
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-11-30 18:02:18 +05:30
Yunus M
b2d6d75eef feat: dashboard perf improvements (#4010)
* feat: dashboard perf improvements

* feat: remove console logs

* fix: remove console.log

* fix: update tests

* fix: update tests

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2023-11-30 13:56:49 +05:30
Prashant Shahi
07d126c669 Merge pull request #4114 from SigNoz/release/v0.34.4
Release/v0.34.4
2023-11-29 22:55:53 +05:30
Prashant Shahi
50d584cc89 chore: 📌 pin versions: SigNoz 0.34.4
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2023-11-29 22:47:59 +05:45
Raj Kamal Singh
1b6b3c2fdf Feat: query service: logs pipelines timestamp parsing processor (#4105)
* chore: relocate tests for trace and grok parsing processor

* chore: add test for timestamp parsing processor

* feat: update PipelineOperator model for time parser fields and get tests passing

* chore: add test cases for validating time parser fails silently on mismatched logs

* chore: add helper for generating regex for strptime layouts

* feat: time_parser ignore logs whose parseFrom value doesn't match strptime layout

* chore: escape regex special chars if any in the layout string before preparing regex

* chore: add operator.If on time_parser when using layout type epoch

* chore: finish up with operator.If on time_parser for  layout type

* chore: postable pipeline validation for time parser

* chore: some cleanup

* chore: some more cleanup

* chore: add validation for strptime layouts in postable pipelines

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2023-11-29 18:55:01 +05:30
Palash Gupta
1f0fdfd403 feat: element is made into focus and scrolled into view after edit/save (#4046) 2023-11-29 18:21:26 +05:30
Raj Kamal Singh
ae3b604cdc Fix: some pipelines UI fixes (#4112)
* fix: log pipelines: change incorrect placeholder for  fields

* fix: incorrect timestamp display in logs preview output
2023-11-29 17:18:32 +05:30
Palash Gupta
381f497b95 fix: queryTable is updated with newData (#4084) 2023-11-29 12:08:20 +05:30
Vikrant Gupta
8045c4e5ae feat: add pr template (#4102)
* feat: address review comments

---------

Co-authored-by: Prashant Shahi <prashant@signoz.io>
2023-11-29 11:04:48 +05:30
Nityananda Gohain
7451e885c3 feat: custom timeout and contextTimeout flag in response (#4022) 2023-11-29 09:10:30 +05:30
Vikrant Gupta
01df53074c fix: [GH-4075]: block action on the view section if the dashboard is locked (#4089)
* fix: [GH-4075]: block action on the view section if the dashboard is locked
2023-11-29 00:02:51 +05:30
Srikanth Chekuri
b6a79ab22c fix: use window function lagInFrame for rate calculation (#4068) 2023-11-28 19:16:08 +05:30
Yunus M
dae817640b fix: [GH-4097]: Fix missing values in chart tooltip (#4098) 2023-11-28 17:18:48 +05:30
Palash Gupta
16839eb7d3 fix: updated the form value on mount (#4076)
* fix: updated the form value on mount

* fix: isLoading is replaced isFetching
2023-11-28 13:44:25 +05:30
Palash Gupta
780a863943 feat: added the share link for view widget mode (#4052) 2023-11-28 13:33:39 +05:30
Srikanth Chekuri
5e0b6366cc chore: update rule create response (#4090) 2023-11-28 10:44:11 +05:30
Vikrant Gupta
8eb2b9e3d0 fix: [GH-4081]: no whitespace should appear when we remove hidden from body styles (#4092) 2023-11-28 08:30:37 +05:30
Yunus M
97ed163002 fix: sort tooltip value based on value and highlight on hover (#4059)
* fix: sort tooltip value based on value and highlight on hover

* fix: tsc issues
2023-11-27 18:07:15 +05:30
Vikrant Gupta
e18bb7d5bc fix: [3958]: restrict dashboard api call on other pages (#4066) 2023-11-27 17:49:22 +05:30
Yunus M
1e4cf2513c fix: update logic for handling data for uplot charts (#4070)
* fix: update logic for handling data for uplot charts

* fix: handle NaN data
2023-11-27 16:57:41 +05:30
Raj Kamal Singh
988ede7776 Fix/pipelines temp work around for supporting dots in resource keys (#4064)
* chore: logs pipelines: add test for validating workaround for working with dots in keys

* fix: temp workaround for supporting pipeline filters using names with dots converted to underscore
2023-11-26 12:57:23 +05:30
Rajat Dabade
d1acad8ee4 fix: the undefined threshold format issue (#4058) 2023-11-24 18:17:58 +05:30
Raj Kamal Singh
f5b1d4146f Fix: pipelines: string ops on missing attribs in pipeline filters should not spam warnings (#4049)
* chore: add test validating collector doesn't spam logs for string ops on missing attributes

* fix: pipelines filter: check if attrib is not nil before running string operator

* chore: do a nil check for all but == and != ops
2023-11-24 17:00:39 +05:30
Alex Bowers
feaac39e2a Dashboard full screen should be allowed regardless of whether dashboard is locked (#4055) 2023-11-24 14:50:45 +05:30
Palash Gupta
fc4cdea539 fix: dashboard delete is fixed and toast message is added (#4050)
* fix: dashboard delete is fixed and toast message is added

* fix: dashboard delete is fixed and toast message is added

* chore: message is updated
2023-11-24 11:51:26 +05:30
Vikrant Gupta
399d49b3c0 feat: added auth as pre-requisite for the other tests (#4031)
* feat: added auth as pre-requisite for the other tests

* feat: added navigation checks

* feat: added navigation checks
2023-11-24 00:40:15 +05:30
Yunus M
ec8a74d385 fix: new dashboard menu items are flickering (#4039) 2023-11-24 00:03:47 +05:30
Prashant Shahi
7c87310fa6 Merge pull request #4045 from SigNoz/release/v0.34.3
Release/v0.34.3
2023-11-23 22:08:49 +05:30
392 changed files with 13508 additions and 2620 deletions

5
.github/CODEOWNERS vendored
View File

@@ -1,15 +1,10 @@
# CODEOWNERS info: https://help.github.com/en/articles/about-code-owners
# Owners are automatically requested for review for PRs that changes code
# that they own.
* @ankitnayan
/frontend/ @palashgdev @YounixM
/frontend/src/container/MetricsApplication @srikanthccv
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv
/deploy/ @prashant-shahi
/sample-apps/ @prashant-shahi
**/query-service/ @srikanthccv
Makefile @srikanthccv
go.* @srikanthccv
.git* @srikanthccv
.github @prashant-shahi

17
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,17 @@
### Summary
<!-- ✍️ A clear and concise description...-->
#### Related Issues / PR's
<!-- ✍️ Add the issues being resolved here and related PR's where applicable -->
#### Screenshots
NA
<!-- ✍️ Add screenshots of before and after changes where applicable-->
#### Affected Areas and Manually Tested Areas
<!-- ✍️ Add details of blast radius and dev testing areas where applicable-->

View File

@@ -34,7 +34,7 @@ jobs:
id: short-sha
- name: Get branch name
id: branch-name
uses: tj-actions/branch-names@v5.1
uses: tj-actions/branch-names@v7.0.7
- name: Set docker tag environment
run: |
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then
@@ -78,7 +78,7 @@ jobs:
id: short-sha
- name: Get branch name
id: branch-name
uses: tj-actions/branch-names@v5.1
uses: tj-actions/branch-names@v7.0.7
- name: Set docker tag environment
run: |
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then
@@ -127,7 +127,7 @@ jobs:
id: short-sha
- name: Get branch name
id: branch-name
uses: tj-actions/branch-names@v5.1
uses: tj-actions/branch-names@v7.0.7
- name: Set docker tag environment
run: |
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then
@@ -176,7 +176,7 @@ jobs:
id: short-sha
- name: Get branch name
id: branch-name
uses: tj-actions/branch-names@v5.1
uses: tj-actions/branch-names@v7.0.7
- name: Set docker tag environment
run: |
if [ '${{ steps.branch-name.outputs.is_tag }}' == 'true' ]; then

View File

@@ -29,7 +29,7 @@ jobs:
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
docker system prune --force
docker pull signoz/signoz-otel-collector:main
docker pull signoz/signoz/signoz-schema-migrator:main
docker pull signoz/signoz-schema-migrator:main
cd ~/signoz
git status
git add .

3
.gitignore vendored
View File

@@ -60,4 +60,5 @@ e2e/node_modules/
e2e/test-results/
e2e/playwright-report/
e2e/blob-report/
e2e/playwright/.cache/
e2e/playwright/.cache/
e2e/.auth

View File

@@ -199,10 +199,13 @@ Not sure how to get started? Just ping us on `#contributing` in our [slack commu
#### Frontend
- [Palash Gupta](https://github.com/palashgdev)
- [Yunus M](https://github.com/YounixM)
- [Rajat Dabade](https://github.com/Rajat-Dabade)
#### DevOps
- [Prashant Shahi](https://github.com/prashant-shahi)
- [Dhawal Sanghvi](https://github.com/dhawal1248)
<br /><br />

View File

@@ -1,7 +1,7 @@
version: "3.9"
x-clickhouse-defaults: &clickhouse-defaults
image: clickhouse/clickhouse-server:23.7.3-alpine
image: clickhouse/clickhouse-server:23.11.1-alpine
tty: true
deploy:
restart_policy:
@@ -146,7 +146,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.34.3
image: signoz/query-service:0.36.2
command:
[
"-config=/root/config/prometheus.yml",
@@ -186,7 +186,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:0.34.3
image: signoz/frontend:0.36.2
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.1
image: signoz/signoz-otel-collector:0.88.6
command:
[
"--config=/etc/otel-collector-config.yaml",
@@ -237,7 +237,7 @@ services:
- query-service
otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.88.1
image: signoz/signoz-schema-migrator:0.88.6
deploy:
restart_policy:
condition: on-failure
@@ -250,7 +250,7 @@ services:
# - clickhouse-3
otel-collector-metrics:
image: signoz/signoz-otel-collector:0.88.1
image: signoz/signoz-otel-collector:0.88.6
command:
[
"--config=/etc/otel-collector-metrics-config.yaml",

View File

@@ -66,7 +66,7 @@ services:
- --storage.path=/data
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.1}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.6}
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.1
image: signoz/signoz-otel-collector:0.88.6
command:
[
"--config=/etc/otel-collector-config.yaml",
@@ -118,7 +118,7 @@ services:
otel-collector-metrics:
container_name: signoz-otel-collector-metrics
image: signoz/signoz-otel-collector:0.88.1
image: signoz/signoz-otel-collector:0.88.6
command:
[
"--config=/etc/otel-collector-metrics-config.yaml",

View File

@@ -3,7 +3,7 @@ version: "2.4"
x-clickhouse-defaults: &clickhouse-defaults
restart: on-failure
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
image: clickhouse/clickhouse-server:23.7.3-alpine
image: clickhouse/clickhouse-server:23.11.1-alpine
tty: true
depends_on:
- zookeeper-1
@@ -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.34.3}
image: signoz/query-service:${DOCKER_TAG:-0.36.2}
container_name: signoz-query-service
command:
[
@@ -203,7 +203,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.34.3}
image: signoz/frontend:${DOCKER_TAG:-0.36.2}
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.1}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.6}
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.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.6}
container_name: signoz-otel-collector
command:
[
@@ -269,7 +269,7 @@ services:
condition: service_healthy
otel-collector-metrics:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.6}
container_name: signoz-otel-collector-metrics
command:
[

View File

@@ -30,4 +30,17 @@ export default defineConfig({
baseURL:
process.env.PLAYWRIGHT_TEST_BASE_URL || "https://stagingapp.signoz.io/",
},
projects: [
{ name: "setup", testMatch: /.*\.setup\.ts/ },
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
// Use prepared auth state.
storageState: ".auth/user.json",
},
dependencies: ["setup"],
},
],
});

View File

@@ -4,6 +4,8 @@ import dotenv from "dotenv";
dotenv.config();
const authFile = ".auth/user.json";
test("E2E Login Test", async ({ page }) => {
await Promise.all([page.goto("/"), page.waitForRequest("**/version")]);
@@ -30,4 +32,6 @@ test("E2E Login Test", async ({ page }) => {
await page.locator('button[data-attr="signup"]').click();
await expect(page).toHaveURL(ROUTES.APPLICATION);
await page.context().storageState({ path: authFile });
});

10
e2e/tests/contants.ts Normal file
View File

@@ -0,0 +1,10 @@
export const SERVICE_TABLE_HEADERS = {
APPLICATION: "Applicaton",
P99LATENCY: "P99 latency (in ms)",
ERROR_RATE: "Error Rate (% of total)",
OPS_PER_SECOND: "Operations Per Second",
};
export const DATA_TEST_IDS = {
NEW_DASHBOARD_BTN: "create-new-dashboard",
};

View File

@@ -0,0 +1,40 @@
import { test, expect } from "@playwright/test";
import ROUTES from "../../frontend/src/constants/routes";
import { DATA_TEST_IDS, SERVICE_TABLE_HEADERS } from "./contants";
test("Basic Navigation Check across different resources", async ({ page }) => {
// route to services page and check if the page renders fine with BE contract
await Promise.all([
page.goto(ROUTES.APPLICATION),
page.waitForRequest("**/v1/services"),
]);
const p99Latency = page.locator(
`th:has-text("${SERVICE_TABLE_HEADERS.P99LATENCY}")`
);
await expect(p99Latency).toBeVisible();
// route to the new trace explorer page and check if the page renders fine
await page.goto(ROUTES.TRACES_EXPLORER);
await page.waitForLoadState("networkidle");
const listViewTable = await page
.locator('div[role="presentation"]')
.isVisible();
expect(listViewTable).toBeTruthy();
// route to the dashboards page and check if the page renders fine
await Promise.all([
page.goto(ROUTES.ALL_DASHBOARD),
page.waitForRequest("**/v1/dashboards"),
]);
const newDashboardBtn = await page
.locator(`data-testid=${DATA_TEST_IDS.NEW_DASHBOARD_BTN}`)
.isVisible();
expect(newDashboardBtn).toBeTruthy();
});

View File

@@ -1,5 +1,5 @@
# use a minimal alpine image
FROM alpine:3.18.3
FROM alpine:3.18.5
# Add Maintainer Info
LABEL maintainer="signoz"
@@ -18,6 +18,7 @@ COPY ee/query-service/bin/query-service-${TARGETOS}-${TARGETARCH} /root/query-se
# copy prometheus YAML config
COPY pkg/query-service/config/prometheus.yml /root/config/prometheus.yml
COPY pkg/query-service/templates /root/templates
# Make query-service executable for non-root users
RUN chmod 755 /root /root/query-service

View File

@@ -12,6 +12,7 @@ import (
"github.com/gorilla/mux"
"go.signoz.io/signoz/ee/query-service/model"
"go.signoz.io/signoz/pkg/query-service/auth"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.uber.org/zap"
)
@@ -47,8 +48,18 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
req.CreatedAt = time.Now().Unix()
req.Token = generatePATToken()
// default expiry is 30 days
if req.ExpiresAt == 0 {
req.ExpiresAt = time.Now().AddDate(0, 0, 30).Unix()
}
// max expiry is 1 year
if req.ExpiresAt > time.Now().AddDate(1, 0, 0).Unix() {
req.ExpiresAt = time.Now().AddDate(1, 0, 0).Unix()
}
zap.S().Debugf("Got PAT request: %+v", req)
if apierr := ah.AppDao().CreatePAT(ctx, &req); apierr != nil {
var apierr basemodel.BaseApiError
if req, apierr = ah.AppDao().CreatePAT(ctx, req); apierr != nil {
RespondError(w, apierr, nil)
return
}

View File

@@ -480,7 +480,7 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
}
}
if _, ok := telemetry.IgnoredPaths()[path]; !ok {
if _, ok := telemetry.EnabledPaths()[path]; ok {
userEmail, err := auth.GetEmailFromJwt(r.Context())
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_PATH, data, userEmail)

View File

@@ -33,7 +33,7 @@ type ModelDao interface {
DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.BaseApiError
GetDomainByEmail(ctx context.Context, email string) (*model.OrgDomain, basemodel.BaseApiError)
CreatePAT(ctx context.Context, p *model.PAT) basemodel.BaseApiError
CreatePAT(ctx context.Context, p model.PAT) (model.PAT, basemodel.BaseApiError)
GetPAT(ctx context.Context, pat string) (*model.PAT, basemodel.BaseApiError)
GetPATByID(ctx context.Context, id string) (*model.PAT, basemodel.BaseApiError)
GetUserByPAT(ctx context.Context, token string) (*basemodel.UserPayload, basemodel.BaseApiError)

View File

@@ -3,14 +3,15 @@ package sqlite
import (
"context"
"fmt"
"strconv"
"go.signoz.io/signoz/ee/query-service/model"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.uber.org/zap"
)
func (m *modelDao) CreatePAT(ctx context.Context, p *model.PAT) basemodel.BaseApiError {
_, err := m.DB().ExecContext(ctx,
func (m *modelDao) CreatePAT(ctx context.Context, p model.PAT) (model.PAT, basemodel.BaseApiError) {
result, err := m.DB().ExecContext(ctx,
"INSERT INTO personal_access_tokens (user_id, token, name, created_at, expires_at) VALUES ($1, $2, $3, $4, $5)",
p.UserID,
p.Token,
@@ -19,9 +20,15 @@ func (m *modelDao) CreatePAT(ctx context.Context, p *model.PAT) basemodel.BaseAp
p.ExpiresAt)
if err != nil {
zap.S().Errorf("Failed to insert PAT in db, err: %v", zap.Error(err))
return model.InternalError(fmt.Errorf("PAT insertion failed"))
return model.PAT{}, model.InternalError(fmt.Errorf("PAT insertion failed"))
}
return nil
id, err := result.LastInsertId()
if err != nil {
zap.S().Errorf("Failed to get last inserted id, err: %v", zap.Error(err))
return model.PAT{}, model.InternalError(fmt.Errorf("PAT insertion failed"))
}
p.Id = strconv.Itoa(int(id))
return p, nil
}
func (m *modelDao) ListPATs(ctx context.Context, userID string) ([]model.PAT, basemodel.BaseApiError) {
@@ -90,7 +97,7 @@ func (m *modelDao) GetUserByPAT(ctx context.Context, token string) (*basemodel.U
u.org_id,
u.group_id
FROM users u, personal_access_tokens p
WHERE u.id = p.user_id and p.token=?;`
WHERE u.id = p.user_id and p.token=? and p.expires_at >= strftime('%s', 'now');`
if err := m.DB().Select(&users, query, token); err != nil {
return nil, model.InternalError(fmt.Errorf("failed to fetch user from PAT, err: %v", err))

View File

@@ -6,5 +6,5 @@ type PAT struct {
Token string `json:"token" db:"token"`
Name string `json:"name" db:"name"`
CreatedAt int64 `json:"createdAt" db:"created_at"`
ExpiresAt int64 `json:"expiresAt" db:"expires_at"` // unused as of now
ExpiresAt int64 `json:"expiresAt" db:"expires_at"`
}

View File

@@ -52,14 +52,14 @@ var BasicPlan = basemodel.FeatureSet{
Name: basemodel.QueryBuilderPanels,
Active: true,
Usage: 0,
UsageLimit: 5,
UsageLimit: 20,
Route: "",
},
basemodel.Feature{
Name: basemodel.QueryBuilderAlerts,
Active: true,
Usage: 0,
UsageLimit: 5,
UsageLimit: 10,
Route: "",
},
basemodel.Feature{

View File

@@ -86,6 +86,7 @@ module.exports = {
},
],
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
'no-plusplus': 'off',
'jsx-a11y/label-has-associated-control': [
'error',
{
@@ -109,7 +110,6 @@ module.exports = {
// eslint rules need to remove
'@typescript-eslint/no-shadow': 'off',
'import/no-cycle': 'off',
'prettier/prettier': [
'error',
{},

View File

@@ -2,3 +2,19 @@
. "$(dirname "$0")/_/husky.sh"
cd frontend && yarn run commitlint --edit $1
branch="$(git rev-parse --abbrev-ref HEAD)"
color_red="$(tput setaf 1)"
bold="$(tput bold)"
reset="$(tput sgr0)"
if [ "$branch" = "main" ]; then
echo "${color_red}${bold}You can't commit directly to the main branch${reset}"
exit 1
fi
if [ "$branch" = "develop" ]; then
echo "${color_red}${bold}You can't commit directly to the develop branch${reset}"
exit 1
fi

View File

@@ -22,7 +22,7 @@ const config: Config.InitialOptions = {
'^.+\\.(js|jsx)$': 'babel-jest',
},
transformIgnorePatterns: [
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend)/)',
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios)/)',
],
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],

View File

@@ -29,6 +29,9 @@
"dependencies": {
"@ant-design/colors": "6.0.0",
"@ant-design/icons": "4.8.0",
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
"@grafana/data": "^9.5.2",
"@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0",
@@ -38,7 +41,7 @@
"ansi-to-html": "0.7.2",
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
"axios": "^0.21.0",
"axios": "1.6.2",
"babel-eslint": "^10.1.0",
"babel-jest": "^29.6.4",
"babel-loader": "9.1.3",
@@ -87,7 +90,7 @@
"react-helmet-async": "1.3.0",
"react-i18next": "^11.16.1",
"react-markdown": "8.0.7",
"react-query": "^3.34.19",
"react-query": "3.39.3",
"react-redux": "^7.2.2",
"react-router-dom": "^5.2.0",
"react-syntax-highlighter": "15.5.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -20,5 +20,10 @@
"variable_updated_successfully": "Variable updated successfully",
"error_while_updating_variable": "Error while updating variable",
"dashboard_has_been_updated": "Dashboard has been updated",
"do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?"
"do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?",
"delete_dashboard_success": "{{name}} dashboard deleted successfully",
"dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.",
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
"your_graph_build_with": "Your graph built with",
"dashboar_ok_confirm": "query will be saved. Press OK to confirm."
}

View File

@@ -14,5 +14,6 @@
"delete_domain_message": "Are you sure you want to delete this domain?",
"delete_domain": "Delete Domain",
"add_domain": "Add Domains",
"saml_settings":"Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly"
"saml_settings": "Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly",
"invite_link_share_manually": "After inviting members, please copy the invite link and send them the link manually"
}

View File

@@ -0,0 +1,3 @@
{
"rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support."
}

View File

@@ -22,6 +22,11 @@
"error_while_updating_variable": "Error while updating variable",
"dashboard_has_been_updated": "Dashboard has been updated",
"do_you_want_to_refresh_the_dashboard": "Do you want to refresh the dashboard?",
"locked_dashboard_delete_tooltip_admin_author": "Dashboard is locked. Please unlock the dashboard to enable delete.",
"locked_dashboard_delete_tooltip_editor": "Dashboard is locked. Please contact admin to delete the dashboard."
"locked_dashboard_delete_tooltip_admin_author": "Dashboard is locked. Please unlock the dashboard to enable delete.",
"locked_dashboard_delete_tooltip_editor": "Dashboard is locked. Please contact admin to delete the dashboard.",
"delete_dashboard_success": "{{name}} dashboard deleted successfully",
"dashboard_unsave_changes": "There are unsaved changes in the Query builder, please stage and run the query or the changes will be lost. Press OK to discard.",
"dashboard_save_changes": "Your graph built with {{queryTag}} query will be saved. Press OK to confirm.",
"your_graph_build_with": "Your graph built with",
"dashboar_ok_confirm": "query will be saved. Press OK to confirm."
}

View File

@@ -14,5 +14,6 @@
"delete_domain_message": "Are you sure you want to delete this domain?",
"delete_domain": "Delete Domain",
"add_domain": "Add Domains",
"saml_settings":"Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly"
"saml_settings": "Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly",
"invite_link_share_manually": "After inviting members, please copy the invite link and send them the link manually"
}

View File

@@ -32,7 +32,7 @@
"processor_name_placeholder": "Name",
"processor_regex_placeholder": "Regex",
"processor_parsefrom_placeholder": "Parse From",
"processor_parseto_placeholder": "Parse From",
"processor_parseto_placeholder": "Parse To",
"processor_onerror_placeholder": "on Error",
"processor_pattern_placeholder": "Pattern",
"processor_field_placeholder": "Field",

View File

@@ -0,0 +1,3 @@
{
"rps_over_100": "You are sending data at more than 100 RPS, your ingestion may be rate limited. Please reach out to us via Intercom support."
}

View File

@@ -49,7 +49,8 @@ export const Onboarding = Loadable(
);
export const DashboardPage = Loadable(
() => import(/* webpackChunkName: "DashboardPage" */ 'pages/Dashboard'),
() =>
import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardsListPage'),
);
export const NewDashboardPage = Loadable(

View File

@@ -1,4 +1,4 @@
import { AxiosError } from 'axios';
import { AxiosError, AxiosResponse } from 'axios';
import { ErrorResponse } from 'types/api';
import { ErrorStatusCode } from 'types/common';
@@ -10,7 +10,7 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
const statusCode = response.status as ErrorStatusCode;
if (statusCode >= 400 && statusCode < 500) {
const { data } = response;
const { data } = response as AxiosResponse;
if (statusCode === 404) {
return {

View File

@@ -3,9 +3,9 @@ import { ApiResponse } from 'types/api';
import { Props } from 'types/api/dashboard/get';
import { Dashboard } from 'types/api/dashboard/getAll';
const get = (props: Props): Promise<Dashboard> =>
const getDashboard = (props: Props): Promise<Dashboard> =>
axios
.get<ApiResponse<Dashboard>>(`/dashboards/${props.uuid}`)
.then((res) => res.data.data);
export default get;
export default getDashboard;

View File

@@ -0,0 +1,15 @@
import { ApiV3Instance as axios } from 'api';
import { ApiResponse } from 'types/api';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { QueryRangePayload } from 'types/api/metrics/getQueryRange';
interface IQueryRangeFormat {
compositeQuery: ICompositeMetricQuery;
}
export const getQueryRangeFormat = (
props?: Partial<QueryRangePayload>,
): Promise<IQueryRangeFormat> =>
axios
.post<ApiResponse<IQueryRangeFormat>>('/query_range/format', props)
.then((res) => res.data.data);

View File

@@ -4,7 +4,7 @@ import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/update';
const update = async (
const updateDashboard = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
@@ -23,4 +23,4 @@ const update = async (
}
};
export default update;
export default updateDashboard;

View File

@@ -0,0 +1,30 @@
import { ApiV2Instance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
Props,
VariableResponseProps,
} from 'types/api/dashboard/variables/query';
const dashboardVariablesQuery = async (
props: Props,
): Promise<SuccessResponse<VariableResponseProps> | ErrorResponse> => {
try {
const response = await axios.post(`/variables/query`, props);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
const formattedError = ErrorResponseHandler(error as AxiosError);
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw { message: 'Error fetching data', details: formattedError };
}
};
export default dashboardVariablesQuery;

View File

@@ -1,24 +0,0 @@
import { ApiV2Instance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/variables/query';
const query = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post(`/variables/query`, props);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default query;

View File

@@ -4,7 +4,7 @@
import getLocalStorageApi from 'api/browser/localstorage/get';
import loginApi from 'api/user/login';
import afterLogin from 'AppRoutes/utils';
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { ENVIRONMENT } from 'constants/env';
import { LOCALSTORAGE } from 'constants/localStorage';
import store from 'store';
@@ -17,14 +17,16 @@ const interceptorsResponse = (
): Promise<AxiosResponse<any>> => Promise.resolve(value);
const interceptorsRequestResponse = (
value: AxiosRequestConfig,
): AxiosRequestConfig => {
value: InternalAxiosRequestConfig,
): InternalAxiosRequestConfig => {
const token =
store.getState().app.user?.accessJwt ||
getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) ||
'';
value.headers.Authorization = token ? `Bearer ${token}` : '';
if (value && value.headers) {
value.headers.Authorization = token ? `Bearer ${token}` : '';
}
return value;
};
@@ -92,8 +94,8 @@ const instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
});
instance.interceptors.response.use(interceptorsResponse, interceptorRejected);
instance.interceptors.request.use(interceptorsRequestResponse);
instance.interceptors.response.use(interceptorsResponse, interceptorRejected);
export const AxiosAlertManagerInstance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiAlertManager}`,

View File

@@ -9,9 +9,10 @@ import {
export const getMetricsQueryRange = async (
props: QueryRangePayload,
signal: AbortSignal,
): Promise<SuccessResponse<MetricRangePayloadV3> | ErrorResponse> => {
try {
const response = await axios.post('/query_range', props);
const response = await axios.post('/query_range', props, { signal });
return {
statusCode: 200,

View File

@@ -5,6 +5,7 @@ import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import isEqual from 'lodash-es/isEqual';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import {
DeleteViewHandlerProps,
@@ -35,6 +36,45 @@ export const getViewDetailsUsingViewKey: GetViewDetailsUsingViewKey = (
return undefined;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const omitIdFromQuery = (query: Query | null): any => ({
...query,
builder: {
...query?.builder,
queryData: query?.builder.queryData.map((queryData) => {
const { id, ...rest } = queryData.aggregateAttribute;
const newAggregateAttribute = rest;
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
const { id, ...rest } = groupByAttribute;
return rest;
});
const newItems = queryData.filters.items.map((item) => {
const { id, ...newItem } = item;
if (item.key) {
const { id, ...rest } = item.key;
return {
...newItem,
key: rest,
};
}
return newItem;
});
return {
...queryData,
aggregateAttribute: newAggregateAttribute,
groupBy: newGroupByAttributes,
filters: {
...queryData.filters,
items: newItems,
},
limit: queryData.limit ? queryData.limit : 0,
offset: queryData.offset ? queryData.offset : 0,
pageSize: queryData.pageSize ? queryData.pageSize : 0,
};
}),
},
});
export const isQueryUpdatedInView = ({
viewKey,
data,
@@ -48,43 +88,7 @@ export const isQueryUpdatedInView = ({
const { query, panelType } = currentViewDetails;
// Omitting id from aggregateAttribute and groupBy
const updatedCurrentQuery = {
...stagedQuery,
builder: {
...stagedQuery?.builder,
queryData: stagedQuery?.builder.queryData.map((queryData) => {
const { id, ...rest } = queryData.aggregateAttribute;
const newAggregateAttribute = rest;
const newGroupByAttributes = queryData.groupBy.map((groupByAttribute) => {
const { id, ...rest } = groupByAttribute;
return rest;
});
const newItems = queryData.filters.items.map((item) => {
const { id, ...newItem } = item;
if (item.key) {
const { id, ...rest } = item.key;
return {
...newItem,
key: rest,
};
}
return newItem;
});
return {
...queryData,
aggregateAttribute: newAggregateAttribute,
groupBy: newGroupByAttributes,
filters: {
...queryData.filters,
items: newItems,
},
limit: queryData.limit ? queryData.limit : 0,
offset: queryData.offset ? queryData.offset : 0,
pageSize: queryData.pageSize ? queryData.pageSize : 0,
};
}),
},
};
const updatedCurrentQuery = omitIdFromQuery(stagedQuery);
return (
panelType !== currentPanelType ||
@@ -153,7 +157,7 @@ export const deleteViewHandler = ({
if (viewId === viewKey) {
redirectWithQueryBuilderData(
updateAllQueriesOperators(
initialQueriesMap.traces,
initialQueriesMap[sourcePage],
panelType || PANEL_TYPES.LIST,
sourcePage,
),

View File

@@ -7,7 +7,6 @@ import {
} from '@ant-design/icons';
import Convert from 'ansi-to-html';
import { Button, Divider, Row, Typography } from 'antd';
import LogDetail from 'components/LogDetail';
import LogsExplorerContext from 'container/LogsExplorerContext';
import dayjs from 'dayjs';
import dompurify from 'dompurify';
@@ -95,11 +94,15 @@ function LogSelectedField({
type ListLogViewProps = {
logData: ILog;
selectedFields: IField[];
onSetActiveLog: (log: ILog) => void;
onAddToQuery: AddToQueryHOCProps['onAddToQuery'];
};
function ListLogView({
logData,
selectedFields,
onSetActiveLog,
onAddToQuery,
}: ListLogViewProps): JSX.Element {
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
@@ -113,12 +116,6 @@ function ListLogView({
onSetActiveLog: handleSetActiveContextLog,
onClearActiveLog: handleClearActiveContextLog,
} = useActiveLog();
const {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery,
} = useActiveLog();
const handleDetailedView = useCallback(() => {
onSetActiveLog(logData);
@@ -223,12 +220,6 @@ function ListLogView({
onClose={handleClearActiveContextLog}
/>
)}
<LogDetail
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
/>
</Row>
</Container>
);

View File

@@ -27,6 +27,7 @@ function DynamicColumnTable({
);
useEffect(() => {
setColumnsData(columns);
const visibleColumns = getVisibleColumns({
tablesource,
columnsData: columns,
@@ -42,7 +43,7 @@ function DynamicColumnTable({
: undefined,
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [columns]);
const onToggleHandler = (index: number) => (
checked: boolean,

View File

@@ -13,3 +13,11 @@
height: 100%;
width: 100%;
}
.uplot-no-data {
position: relative;
display: flex;
width: 100%;
flex-direction: column;
gap: 8px;
}

View File

@@ -1,8 +1,9 @@
/* eslint-disable sonarjs/cognitive-complexity */
import './uplot.scss';
import './Uplot.styles.scss';
import { Typography } from 'antd';
import { ToggleGraphProps } from 'components/Graph/types';
import { LineChart } from 'lucide-react';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import {
forwardRef,
@@ -127,6 +128,16 @@ const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
}
}, [data, resetScales, create]);
if (data && data[0] && data[0]?.length === 0) {
return (
<div className="uplot-no-data not-found">
<LineChart size={48} strokeWidth={0.5} />
<Typography>No Data</Typography>
</div>
);
}
return (
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
<div className="uplot-graph-container" ref={targetRef}>

View File

@@ -14,7 +14,7 @@ export const optionsUpdateState = (
if (lhsHeight !== rhsHeight || lhsWidth !== rhsWidth) {
state = 'update';
}
if (Object.keys(lhs).length !== Object.keys(rhs).length) {
if (Object.keys(lhs)?.length !== Object.keys(rhs)?.length) {
return 'create';
}
// eslint-disable-next-line no-restricted-syntax
@@ -31,12 +31,12 @@ export const dataMatch = (
lhs: uPlot.AlignedData,
rhs: uPlot.AlignedData,
): boolean => {
if (lhs.length !== rhs.length) {
if (lhs?.length !== rhs?.length) {
return false;
}
return lhs.every((lhsOneSeries, seriesIdx) => {
const rhsOneSeries = rhs[seriesIdx];
if (lhsOneSeries.length !== rhsOneSeries.length) {
if (lhsOneSeries?.length !== rhsOneSeries?.length) {
return false;
}

View File

@@ -0,0 +1,2 @@
const MAX_RPS_LIMIT = 100;
export { MAX_RPS_LIMIT };

View File

@@ -26,4 +26,6 @@ export enum QueryParams {
linesPerRow = 'linesPerRow',
viewName = 'viewName',
viewKey = 'viewKey',
expandedWidgetId = 'expandedWidgetId',
pagination = 'pagination',
}

View File

@@ -30,6 +30,52 @@ const themeColors = {
hemlock: '#66664D',
vidaLoca: '#4D8000',
rust: '#B33300',
red: '#FF0000', // Adding more colors, we need to get better colors from design team
blue: '#0000FF',
green: '#00FF00',
yellow: '#FFFF00',
purple: '#800080',
cyan: '#00FFFF',
magenta: '#FF00FF',
orange: '#FFA500',
pink: '#FFC0CB',
brown: '#A52A2A',
teal: '#008080',
lime: '#00FF00',
maroon: '#800000',
navy: '#000080',
aquamarine: '#7FFFD4',
gold: '#FFD700',
gray: '#808080',
skyBlue: '#87CEEB',
indigo: '#4B0082',
slateGray: '#708090',
chocolate: '#D2691E',
tomato: '#FF6347',
steelBlue: '#4682B4',
peru: '#CD853F',
darkOliveGreen: '#556B2F',
indianRed: '#CD5C5C',
mediumSlateBlue: '#7B68EE',
rosyBrown: '#BC8F8F',
darkSlateGray: '#2F4F4F',
mediumAquamarine: '#66CDAA',
lavender: '#E6E6FA',
thistle: '#D8BFD8',
salmon: '#FA8072',
darkSalmon: '#E9967A',
paleVioletRed: '#DB7093',
mediumPurple: '#9370DB',
darkOrchid: '#9932CC',
lawnGreen: '#7CFC00',
mediumSeaGreen: '#3CB371',
lightCoral: '#F08080',
darkSeaGreen: '#8FBC8F',
sandyBrown: '#F4A460',
darkKhaki: '#BDB76B',
cornflowerBlue: '#6495ED',
mediumVioletRed: '#C71585',
paleGreen: '#98FB98',
},
errorColor: '#d32f2f',
royalGrey: '#888888',

View File

@@ -22,7 +22,7 @@ import {
import FormAlertChannels from 'container/FormAlertChannels';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useCallback, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
@@ -57,6 +57,12 @@ function EditAlertChannels({
setType(value as ChannelType);
}, []);
useEffect(() => {
formInstance.setFieldsValue({
...initialValue,
});
}, [formInstance, initialValue]);
const prepareSlackRequest = useCallback(
() => ({
api_url: selectedConfig?.api_url || '',

View File

@@ -2,6 +2,7 @@ import { InfoCircleOutlined } from '@ant-design/icons';
import Spinner from 'components/Spinner';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import GridPanelSwitch from 'container/GridPanelSwitch';
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
@@ -9,7 +10,7 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useMemo, useRef } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -17,9 +18,10 @@ import { AlertDef } from 'types/api/alerts/def';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getTimeRange } from 'utils/getTimeRange';
import { ChartContainer, FailedMessageContainer } from './styles';
import { covertIntoDataFormats } from './utils';
import { getThresholdLabel } from './utils';
export interface ChartPreviewProps {
name: string;
@@ -31,6 +33,7 @@ export interface ChartPreviewProps {
alertDef?: AlertDef;
userQueryKey?: string;
allowSelectedIntervalForStepGen?: boolean;
yAxisUnit: string;
}
function ChartPreview({
@@ -43,18 +46,17 @@ function ChartPreview({
userQueryKey,
allowSelectedIntervalForStepGen = false,
alertDef,
yAxisUnit,
}: ChartPreviewProps): JSX.Element | null {
const { t } = useTranslation('alerts');
const threshold = alertDef?.condition.target || 0;
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const thresholdValue = covertIntoDataFormats({
value: threshold,
sourceUnit: alertDef?.condition.targetUnit,
targetUnit: query?.unit,
});
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const canQuery = useMemo((): boolean => {
if (!query || query == null) {
@@ -104,19 +106,31 @@ function ChartPreview({
const graphRef = useRef<HTMLDivElement>(null);
useEffect((): void => {
const { startTime, endTime } = getTimeRange(queryResponse);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [maxTime, minTime, globalSelectedInterval, queryResponse]);
const chartData = getUPlotChartData(queryResponse?.data?.payload);
const containerDimensions = useResizeObserver(graphRef);
const isDarkMode = useIsDarkMode();
const optionName =
getFormatNameByOptionId(alertDef?.condition.targetUnit || '') || '';
const options = useMemo(
() =>
getUPlotChartOptions({
id: 'alert_legend_widget',
yAxisUnit: query?.unit,
yAxisUnit,
apiResponse: queryResponse?.data?.payload,
dimensions: containerDimensions,
minTimeScale,
maxTimeScale,
isDarkMode,
thresholds: [
{
@@ -124,20 +138,30 @@ function ChartPreview({
keyIndex: 0,
moveThreshold: (): void => {},
selectedGraph: PANEL_TYPES.TIME_SERIES, // no impact
thresholdValue,
thresholdValue: threshold,
thresholdLabel: `${t(
'preview_chart_threshold_label',
)} (y=${thresholdValue} ${query?.unit || ''})`,
)} (y=${getThresholdLabel(
optionName,
threshold,
alertDef?.condition.targetUnit,
yAxisUnit,
)})`,
thresholdUnit: alertDef?.condition.targetUnit,
},
],
}),
[
query?.unit,
yAxisUnit,
queryResponse?.data?.payload,
containerDimensions,
minTimeScale,
maxTimeScale,
isDarkMode,
threshold,
t,
thresholdValue,
optionName,
alertDef?.condition.targetUnit,
],
);
@@ -162,7 +186,7 @@ function ChartPreview({
name={name || 'Chart Preview'}
panelData={queryResponse.data?.payload.data.newResult.data.result || []}
query={query || initialQueriesMap.metrics}
yAxisUnit={query?.unit}
yAxisUnit={yAxisUnit}
/>
</div>
)}

View File

@@ -51,6 +51,33 @@ export function covertIntoDataFormats({
return Number.isNaN(result) ? 0 : result;
}
export const getThresholdLabel = (
optionName: string,
value: number,
unit?: string,
yAxisUnit?: string,
): string => {
if (
unit === MiscellaneousFormats.PercentUnit ||
yAxisUnit === MiscellaneousFormats.PercentUnit
) {
if (unit === MiscellaneousFormats.Percent) {
return `${value}%`;
}
return `${value * 100}%`;
}
if (
unit === MiscellaneousFormats.Percent ||
yAxisUnit === MiscellaneousFormats.Percent
) {
if (unit === MiscellaneousFormats.PercentUnit) {
return `${value * 100}%`;
}
return `${value}%`;
}
return `${value} ${optionName}`;
};
interface IUnit {
value: number;
sourceUnit?: string;

View File

@@ -82,6 +82,7 @@ function FormAlertRules({
// alertDef holds the form values to be posted
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
const [yAxisUnit, setYAxisUnit] = useState<string>(currentQuery.unit || '');
// initQuery contains initial query when component was mounted
const initQuery = useMemo(() => initialValue.condition.compositeQuery, [
@@ -400,6 +401,7 @@ function FormAlertRules({
query={stagedQuery}
selectedInterval={globalSelectedInterval}
alertDef={alertDef}
yAxisUnit={yAxisUnit || ''}
/>
);
@@ -415,6 +417,7 @@ function FormAlertRules({
query={stagedQuery}
alertDef={alertDef}
selectedInterval={globalSelectedInterval}
yAxisUnit={yAxisUnit || ''}
/>
);
@@ -427,7 +430,8 @@ function FormAlertRules({
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
alertType !== AlertTypes.METRICS_BASED_ALERT;
const onUnitChangeHandler = (): void => {
const onUnitChangeHandler = (value: string): void => {
setYAxisUnit(value);
// reset target unit
setAlertDef((def) => ({
...def,
@@ -457,7 +461,10 @@ function FormAlertRules({
renderPromAndChQueryChartPreview()}
<StepContainer>
<BuilderUnitsFilter onChange={onUnitChangeHandler} />
<BuilderUnitsFilter
onChange={onUnitChangeHandler}
yAxisUnit={yAxisUnit}
/>
</StepContainer>
<QuerySection

View File

@@ -0,0 +1,16 @@
.span-container {
.spanDetails {
position: absolute;
height: 50px;
padding: 8px;
min-width: 150px;
background: lightcyan;
color: black;
bottom: 24px;
left: 0;
display: flex;
justify-content: center;
align-items: center;
}
}

View File

@@ -0,0 +1,96 @@
import '../GantChart.styles.scss';
import { Popover, Typography } from 'antd';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useEffect } from 'react';
import { toFixed } from 'utils/toFixed';
import { SpanBorder, SpanLine, SpanText, SpanWrapper } from './styles';
interface SpanLengthProps {
globalStart: number;
startTime: number;
name: string;
width: string;
leftOffset: string;
bgColor: string;
inMsCount: number;
}
function Span(props: SpanLengthProps): JSX.Element {
const {
width,
leftOffset,
bgColor,
inMsCount,
startTime,
name,
globalStart,
} = props;
const isDarkMode = useIsDarkMode();
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
useEffect(() => {
document.documentElement.scrollTop = document.documentElement.clientHeight;
document.documentElement.scrollLeft = document.documentElement.clientWidth;
}, []);
const getContent = (): JSX.Element => {
const timeStamp = dayjs(startTime).format('h:mm:ss:SSS A');
const startTimeInMs = startTime - globalStart;
return (
<div>
<Typography.Text style={{ marginBottom: '8px' }}>
{' '}
Duration : {inMsCount}
</Typography.Text>
<br />
<Typography.Text style={{ marginBottom: '8px' }}>
Start Time: {startTimeInMs}ms [{timeStamp}]{' '}
</Typography.Text>
</div>
);
};
return (
<SpanWrapper className="span-container">
<SpanLine
className="spanLine"
isDarkMode={isDarkMode}
bgColor={bgColor}
leftOffset={leftOffset}
width={width}
/>
<div>
<Popover
style={{
left: `${leftOffset}%`,
}}
title={name}
content={getContent()}
trigger="hover"
placement="left"
autoAdjustOverflow
>
<SpanBorder
className="spanTrack"
isDarkMode={isDarkMode}
bgColor={bgColor}
leftOffset={leftOffset}
width={width}
/>
</Popover>
</div>
<SpanText isDarkMode={isDarkMode} leftOffset={leftOffset}>{`${toFixed(
time,
2,
)} ${timeUnitName}`}</SpanText>
</SpanWrapper>
);
}
export default Span;

View File

@@ -1,40 +0,0 @@
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { toFixed } from 'utils/toFixed';
import { SpanBorder, SpanLine, SpanText, SpanWrapper } from './styles';
interface SpanLengthProps {
width: string;
leftOffset: string;
bgColor: string;
inMsCount: number;
}
function SpanLength(props: SpanLengthProps): JSX.Element {
const { width, leftOffset, bgColor, inMsCount } = props;
const isDarkMode = useIsDarkMode();
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
return (
<SpanWrapper>
<SpanLine
isDarkMode={isDarkMode}
bgColor={bgColor}
leftOffset={leftOffset}
width={width}
/>
<SpanBorder
isDarkMode={isDarkMode}
bgColor={bgColor}
leftOffset={leftOffset}
width={width}
/>
<SpanText isDarkMode={isDarkMode} leftOffset={leftOffset}>{`${toFixed(
time,
2,
)} ${timeUnitName}`}</SpanText>
</SpanWrapper>
);
}
export default SpanLength;

View File

@@ -16,7 +16,7 @@ import {
import { ITraceTree } from 'types/api/trace/getTraceItem';
import { ITraceMetaData } from '..';
import SpanLength from '../SpanLength';
import Span from '../Span';
import SpanName from '../SpanName';
import { getMetaDataFromSpanTree, getTopLeftFromBody } from '../utils';
import {
@@ -158,7 +158,7 @@ function Trace(props: TraceProps): JSX.Element {
isDarkMode={isDarkMode}
onClick={onClickTreeExpansion}
>
<Typography>{totalSpans}</Typography>
<Typography style={{ wordBreak: 'normal' }}>{totalSpans}</Typography>
<CaretContainer>{icon}</CaretContainer>
</CardComponent>
)}
@@ -169,7 +169,10 @@ function Trace(props: TraceProps): JSX.Element {
</StyledRow>
</StyledCol>
<Col flex="1">
<SpanLength
<Span
globalStart={globalStart}
startTime={startTime}
name={name}
leftOffset={nodeLeftOffset.toString()}
width={width.toString()}
bgColor={serviceColour}

View File

@@ -41,7 +41,11 @@ function GanttChart(props: GanttChartProps): JSX.Element {
onClick={handleCollapse}
title={isExpandAll ? 'Collapse All' : 'Expand All'}
>
{isExpandAll ? <MinusSquareOutlined /> : <PlusSquareOutlined />}
{isExpandAll ? (
<MinusSquareOutlined style={{ fontSize: '16px', color: '#08c' }} />
) : (
<PlusSquareOutlined style={{ fontSize: '16px', color: '#08c' }} />
)}
</CollapseButton>
<CardWrapper>
<Trace

View File

@@ -11,6 +11,7 @@ export const Wrapper = styled.ul`
border-left: 1px solid #434343;
padding-left: 1rem;
width: 100%;
margin: 0px;
}
ul li {
@@ -44,6 +45,4 @@ export const CardContainer = styled.li`
export const CollapseButton = styled.div`
position: absolute;
top: 0;
left: 0;
font-size: 1.2rem;
`;

View File

@@ -4,6 +4,7 @@ import { Button, Input } from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import { ResizeTable } from 'components/ResizeTable';
import { useNotifications } from 'hooks/useNotifications';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useCallback, useState } from 'react';
import { getGraphManagerTableColumns } from './TableRender/GraphManagerColumns';
@@ -29,6 +30,7 @@ function GraphManager({
);
const { notifications } = useNotifications();
const { isDashboardLocked } = useDashboard();
const checkBoxOnChangeHandler = useCallback(
(e: CheckboxChangeEvent, index: number): void => {
@@ -66,6 +68,7 @@ function GraphManager({
graphVisibilityState: graphsVisibilityStates,
labelClickedHandler,
yAxisUnit,
isGraphDisabled: isDashboardLocked,
});
const filterHandler = useCallback(

View File

@@ -9,6 +9,7 @@ function CustomCheckBox({
index,
graphVisibilityState = [],
checkBoxOnChangeHandler,
disabled = false,
}: CheckBoxProps): JSX.Element {
const onChangeHandler = (e: CheckboxChangeEvent): void => {
checkBoxOnChangeHandler(e, index);
@@ -28,7 +29,11 @@ function CustomCheckBox({
},
}}
>
<Checkbox onChange={onChangeHandler} checked={isChecked} />
<Checkbox
onChange={onChangeHandler}
checked={isChecked}
disabled={disabled}
/>
</ConfigProvider>
);
}

View File

@@ -5,12 +5,14 @@ import Label from './Label';
export const getLabel = (
labelClickedHandler: (labelIndex: number) => void,
disabled?: boolean,
): ColumnType<DataSetProps> => ({
render: (label: string, record): JSX.Element => (
<Label
label={label}
labelIndex={record.index}
labelClickedHandler={labelClickedHandler}
disabled={disabled}
/>
),
});

View File

@@ -13,6 +13,7 @@ export const getGraphManagerTableColumns = ({
graphVisibilityState,
labelClickedHandler,
yAxisUnit,
isGraphDisabled,
}: GetGraphManagerTableColumnsProps): ColumnType<DataSetProps>[] => [
{
title: '',
@@ -25,6 +26,7 @@ export const getGraphManagerTableColumns = ({
index={record.index}
checkBoxOnChangeHandler={checkBoxOnChangeHandler}
graphVisibilityState={graphVisibilityState}
disabled={isGraphDisabled}
/>
),
},
@@ -33,7 +35,7 @@ export const getGraphManagerTableColumns = ({
width: 300,
dataIndex: ColumnsKeyAndDataIndex.Label,
key: ColumnsKeyAndDataIndex.Label,
...getLabel(labelClickedHandler),
...getLabel(labelClickedHandler, isGraphDisabled),
},
{
title: getGraphManagerTableHeaderTitle(
@@ -79,4 +81,5 @@ interface GetGraphManagerTableColumnsProps {
labelClickedHandler: (labelIndex: number) => void;
graphVisibilityState: boolean[];
yAxisUnit?: string;
isGraphDisabled?: boolean;
}

View File

@@ -1,3 +1,4 @@
import { Tooltip } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { LabelContainer } from '../styles';
@@ -8,6 +9,7 @@ function Label({
labelClickedHandler,
labelIndex,
label,
disabled = false,
}: LabelProps): JSX.Element {
const isDarkMode = useIsDarkMode();
@@ -19,9 +21,12 @@ function Label({
<LabelContainer
isDarkMode={isDarkMode}
type="button"
disabled={disabled}
onClick={onClickHandler}
>
{getAbbreviatedLabel(label)}
<Tooltip title={label} placement="topLeft">
{getAbbreviatedLabel(label)}
</Tooltip>
</LabelContainer>
);
}

View File

@@ -18,6 +18,10 @@
border-radius: 3px;
}
.disabled {
height: calc(100% - 65px);
}
.graph-manager-container {
height: calc(40% - 40px);

View File

@@ -23,6 +23,7 @@ import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
import GraphManager from './GraphManager';
@@ -52,7 +53,7 @@ function FullView({
const [chartOptions, setChartOptions] = useState<uPlot.Options>();
const { selectedDashboard } = useDashboard();
const { selectedDashboard, isDashboardLocked } = useDashboard();
const getSelectedTime = useCallback(
() =>
@@ -92,6 +93,21 @@ function FullView({
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
@@ -114,6 +130,8 @@ function FullView({
graphsVisibilityStates,
setGraphsVisibilityStates,
thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
});
setChartOptions(newChartOptions);
@@ -155,7 +173,12 @@ function FullView({
)}
</div>
<div className="graph-container" ref={fullViewRef}>
<div
className={
isDashboardLocked ? 'graph-container disabled' : 'graph-container'
}
ref={fullViewRef}
>
{chartOptions && (
<GraphContainer
style={{ height: '90%' }}
@@ -178,7 +201,7 @@ function FullView({
)}
</div>
{canModifyChart && chartOptions && (
{canModifyChart && chartOptions && !isDashboardLocked && (
<GraphManager
data={chartData}
name={name}

View File

@@ -31,9 +31,12 @@ export const GraphContainer = styled.div<GraphContainerProps>`
isGraphLegendToggleAvailable ? '50%' : '100%'};
`;
export const LabelContainer = styled.button<{ isDarkMode?: boolean }>`
export const LabelContainer = styled.button<{
isDarkMode?: boolean;
disabled?: boolean;
}>`
max-width: 18.75rem;
cursor: pointer;
cursor: ${(props): string => (props.disabled ? 'no-drop' : 'pointer')};
border: none;
background-color: transparent;
color: ${(props): string =>

View File

@@ -42,6 +42,7 @@ export interface LabelProps {
labelClickedHandler: (labelIndex: number) => void;
labelIndex: number;
label: string;
disabled?: boolean;
}
export interface FullViewProps {
@@ -74,6 +75,7 @@ export interface CheckBoxProps {
index: number;
graphVisibilityState: boolean[];
checkBoxOnChangeHandler: (e: CheckboxChangeEvent, index: number) => void;
disabled?: boolean;
}
export interface SaveLegendEntriesToLocalStoreProps {

View File

@@ -25,19 +25,26 @@ export const getDefaultTableDataSet = (
data: uPlot.AlignedData,
): ExtendedChartDataset[] =>
options.series.map(
(item: uPlot.Series, index: number): ExtendedChartDataset => ({
...item,
index,
show: true,
sum: convertToTwoDecimalsOrZero(
(data[index] as number[]).reduce((a, b) => a + b, 0),
),
avg: convertToTwoDecimalsOrZero(
(data[index] as number[]).reduce((a, b) => a + b, 0) / data[index].length,
),
max: convertToTwoDecimalsOrZero(Math.max(...(data[index] as number[]))),
min: convertToTwoDecimalsOrZero(Math.min(...(data[index] as number[]))),
}),
(item: uPlot.Series, index: number): ExtendedChartDataset => {
let arr: number[];
if (data[index]) {
arr = data[index] as number[];
} else {
arr = [];
}
return {
...item,
index,
show: true,
sum: convertToTwoDecimalsOrZero(arr.reduce((a, b) => a + b, 0) || 0),
avg: convertToTwoDecimalsOrZero(
(arr.reduce((a, b) => a + b, 0) || 0) / (arr.length || 1),
),
max: convertToTwoDecimalsOrZero(Math.max(...arr)),
min: convertToTwoDecimalsOrZero(Math.min(...arr)),
};
},
);
export const getAbbreviatedLabel = (label: string): string => {

View File

@@ -1,9 +1,14 @@
import '../GridCardLayout.styles.scss';
import { Skeleton, Typography } from 'antd';
import cx from 'classnames';
import { ToggleGraphProps } from 'components/Graph/types';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import GridPanelSwitch from 'container/GridPanelSwitch';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { useDashboard } from 'providers/Dashboard/Dashboard';
@@ -43,10 +48,13 @@ function WidgetGraphComponent({
onDragSelect,
}: WidgetGraphComponentProps): JSX.Element {
const [deleteModal, setDeleteModal] = useState(false);
const [modal, setModal] = useState<boolean>(false);
const [hovered, setHovered] = useState(false);
const { notifications } = useNotifications();
const { pathname } = useLocation();
const { pathname, search } = useLocation();
const params = useUrlQuery();
const isFullViewOpen = params.get(QueryParams.expandedWidgetId) === widget.id;
const lineChartRef = useRef<ToggleGraphProps>();
const graphRef = useRef<HTMLDivElement>(null);
@@ -175,7 +183,24 @@ function WidgetGraphComponent({
};
const handleOnView = (): void => {
onToggleModal(setModal);
const queryParams = {
[QueryParams.expandedWidgetId]: widget.id,
};
const updatedSearch = createQueryParams(queryParams);
const existingSearch = new URLSearchParams(search);
const isExpandedWidgetIdPresent = existingSearch.has(
QueryParams.expandedWidgetId,
);
if (isExpandedWidgetIdPresent) {
existingSearch.delete(QueryParams.expandedWidgetId);
}
const separator = existingSearch.toString() ? '&' : '';
const newSearch = `${existingSearch}${separator}${updatedSearch}`;
history.push({
pathname,
search: newSearch,
});
};
const handleOnDelete = (): void => {
@@ -187,7 +212,13 @@ function WidgetGraphComponent({
};
const onToggleModelHandler = (): void => {
onToggleModal(setModal);
const existingSearchParams = new URLSearchParams(search);
existingSearchParams.delete(QueryParams.expandedWidgetId);
const updatedQueryParams = Object.fromEntries(existingSearchParams.entries());
history.push({
pathname,
search: createQueryParams(updatedQueryParams),
});
};
if (queryResponse.isLoading || queryResponse.status === 'idle') {
@@ -236,7 +267,7 @@ function WidgetGraphComponent({
title={widget?.title || 'View'}
footer={[]}
centered
open={modal}
open={isFullViewOpen}
onCancel={onToggleModelHandler}
width="85%"
destroyOnClose
@@ -270,7 +301,10 @@ function WidgetGraphComponent({
</div>
{queryResponse.isLoading && <Skeleton />}
{queryResponse.isSuccess && (
<div style={{ height: '90%' }} ref={graphRef}>
<div
className={cx('widget-graph-container', widget.panelTypes)}
ref={graphRef}
>
<GridPanelSwitch
panelType={widget.panelTypes}
data={data}

View File

@@ -9,11 +9,13 @@ 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 { memo, useCallback, useMemo, useRef, useState } from 'react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getTimeRange } from 'utils/getTimeRange';
import EmptyWidget from '../EmptyWidget';
import { MenuItemKeys } from '../WidgetHeader/contants';
@@ -28,10 +30,13 @@ function GridCardGraph({
isQueryEnabled,
threshold,
variables,
filterNaN,
fillSpans = false,
}: 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 onDragSelect = useCallback(
(start: number, end: number): void => {
@@ -49,16 +54,27 @@ function GridCardGraph({
const isVisible = useIntersectionObserver(graphRef, undefined, true);
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
useEffect(() => {
if (toScrollWidgetId === widget.id) {
graphRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
graphRef.current?.focus();
setToScrollWidgetId('');
}
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
const updatedQuery = useStepInterval(widget?.query);
const isEmptyWidget =
widget?.id === PANEL_TYPES.EMPTY_WIDGET || isEmpty(widget);
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const queryResponse = useGetQueryRange(
{
selectedTime: widget?.timePreferance,
@@ -90,11 +106,14 @@ function GridCardGraph({
const containerDimensions = useResizeObserver(graphRef);
const chartData = getUPlotChartData(
queryResponse?.data?.payload,
undefined,
filterNaN,
);
useEffect((): void => {
const { startTime, endTime } = getTimeRange(queryResponse);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [maxTime, minTime, globalSelectedInterval, queryResponse]);
const chartData = getUPlotChartData(queryResponse?.data?.payload, fillSpans);
const isDarkMode = useIsDarkMode();
@@ -114,6 +133,8 @@ function GridCardGraph({
yAxisUnit: widget?.yAxisUnit,
onClickHandler,
thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
}),
[
widget?.id,
@@ -124,6 +145,8 @@ function GridCardGraph({
isDarkMode,
onDragSelect,
onClickHandler,
minTimeScale,
maxTimeScale,
],
);

View File

@@ -39,7 +39,7 @@ export interface GridCardGraphProps {
headerMenuList?: WidgetGraphComponentProps['headerMenuList'];
isQueryEnabled: boolean;
variables?: Dashboard['data']['variables'];
filterNaN?: boolean;
fillSpans?: boolean;
}
export interface GetGraphVisibilityStateOnLegendClickProps {

View File

@@ -5,3 +5,11 @@
border: none !important;
}
}
.widget-graph-container {
height: 100%;
&.graph {
height: calc(100% - 30px);
}
}

View File

@@ -3,6 +3,7 @@ import './GridCardLayout.styles.scss';
import { PlusOutlined, SaveFilled } from '@ant-design/icons';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { 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';
@@ -109,39 +110,37 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
return (
<>
{!isDashboardLocked && (
<ButtonContainer>
<ButtonContainer>
<Button
loading={updateDashboardMutation.isLoading}
onClick={handle.enter}
icon={<FullscreenIcon size={16} />}
disabled={updateDashboardMutation.isLoading}
>
{t('dashboard:full_view')}
</Button>
{!isDashboardLocked && saveLayoutPermission && (
<Button
loading={updateDashboardMutation.isLoading}
onClick={handle.enter}
icon={<FullscreenIcon size={16} />}
onClick={onSaveHandler}
icon={<SaveFilled />}
disabled={updateDashboardMutation.isLoading}
>
{t('dashboard:full_view')}
{t('dashboard:save_layout')}
</Button>
)}
{saveLayoutPermission && (
<Button
loading={updateDashboardMutation.isLoading}
onClick={onSaveHandler}
icon={<SaveFilled />}
disabled={updateDashboardMutation.isLoading}
>
{t('dashboard:save_layout')}
</Button>
)}
{addPanelPermission && (
<Button
onClick={onAddPanelHandler}
icon={<PlusOutlined />}
data-testid="add-panel"
>
{t('dashboard:add_panel')}
</Button>
)}
</ButtonContainer>
)}
{!isDashboardLocked && addPanelPermission && (
<Button
onClick={onAddPanelHandler}
icon={<PlusOutlined />}
data-testid="add-panel"
>
{t('dashboard:add_panel')}
</Button>
)}
</ButtonContainer>
<FullScreen handle={handle} className="fullscreen-grid-container">
<ReactGridLayout
@@ -157,6 +156,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
onLayoutChange={setLayouts}
draggableHandle=".drag-handle"
layout={layouts}
style={{ backgroundColor: isDarkMode ? '' : themeColors.snowWhite }}
>
{layouts.map((layout) => {
const { i: id } = layout;
@@ -167,7 +167,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
className={isDashboardLocked ? '' : 'enable-resize'}
isDarkMode={isDarkMode}
key={id}
data-grid={layout}
data-grid={JSON.stringify(currentWidget)}
>
<Card
className="grid-item"

View File

@@ -0,0 +1,37 @@
.widget-header-container {
display: flex;
justify-content: space-between;
align-items: center;
height: 40px;
width: 100%;
padding: 0.5rem;
box-sizing: border-box;
font-size: 14px;
font-weight: 600;
}
.widget-header-title {
max-width: 80%;
}
.widget-header-actions {
display: flex;
align-items: center;
}
.widget-header-more-options {
visibility: hidden;
border: none;
box-shadow: none;
cursor: pointer;
font: 14px;
font-weight: 600;
padding: 8px;
}
.widget-header-hover {
visibility: visible;
}
.widget-api-actions {
padding-right: 0.25rem;
}

View File

@@ -1,21 +1,23 @@
import './WidgetHeader.styles.scss';
import {
AlertOutlined,
CopyOutlined,
DeleteOutlined,
DownOutlined,
EditFilled,
ExclamationCircleOutlined,
FullscreenOutlined,
MoreOutlined,
WarningOutlined,
} from '@ant-design/icons';
import { Dropdown, MenuProps, Tooltip, Typography } from 'antd';
import Spinner from 'components/Spinner';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history';
import { ReactNode, useCallback, useMemo, useState } from 'react';
import { ReactNode, useCallback, useMemo } from 'react';
import { UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -23,23 +25,9 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import AppReducer from 'types/reducer/app';
import { popupContainer } from 'utils/selectPopupContainer';
import {
errorTooltipPosition,
overlayStyles,
spinnerStyles,
tooltipStyles,
WARNING_MESSAGE,
} from './config';
import { errorTooltipPosition, WARNING_MESSAGE } from './config';
import { MENUITEM_KEYS_VS_LABELS, MenuItemKeys } from './contants';
import {
ArrowContainer,
HeaderContainer,
HeaderContentContainer,
ThesholdContainer,
WidgetHeaderContainer,
} from './styles';
import { MenuItem } from './types';
import { generateMenuList, isTWidgetOptions } from './utils';
@@ -72,9 +60,6 @@ function WidgetHeader({
headerMenuList,
isWarning,
}: IWidgetHeaderProps): JSX.Element | null {
const [localHover, setLocalHover] = useState(false);
const [isOpen, setIsOpen] = useState<boolean>(false);
const onEditHandler = useCallback((): void => {
const widgetId = widget.id;
history.push(
@@ -86,13 +71,7 @@ function WidgetHeader({
);
}, [widget.id, widget.panelTypes, widget.query]);
const onCreateAlertsHandler = useCallback(() => {
history.push(
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
JSON.stringify(widget.query),
)}`,
);
}, [widget]);
const onCreateAlertsHandler = useCreateAlerts(widget);
const keyMethodMapping = useMemo(
() => ({
@@ -112,7 +91,6 @@ function WidgetHeader({
if (functionToCall) {
functionToCall();
setIsOpen(false);
}
}
},
@@ -169,10 +147,6 @@ function WidgetHeader({
const updatedMenuList = useMemo(() => generateMenuList(actions), [actions]);
const onClickHandler = (): void => {
setIsOpen(!isOpen);
};
const menu = useMemo(
() => ({
items: updatedMenuList,
@@ -186,49 +160,47 @@ function WidgetHeader({
}
return (
<WidgetHeaderContainer>
<Dropdown
getPopupContainer={popupContainer}
destroyPopupOnHide
open={isOpen}
onOpenChange={setIsOpen}
menu={menu}
trigger={['click']}
overlayStyle={overlayStyles}
<div className="widget-header-container">
<Typography.Text
ellipsis
data-testid={title}
className="widget-header-title"
>
<HeaderContainer
onMouseOver={(): void => setLocalHover(true)}
onMouseOut={(): void => setLocalHover(false)}
hover={localHover}
onClick={onClickHandler}
>
<HeaderContentContainer>
<Typography.Text style={{ maxWidth: '80%' }} ellipsis data-testid={title}>
{title}
</Typography.Text>
<ArrowContainer hover={parentHover}>
<DownOutlined />
</ArrowContainer>
</HeaderContentContainer>
</HeaderContainer>
</Dropdown>
{title}
</Typography.Text>
<div className="widget-header-actions">
<div className="widget-api-actions">{threshold}</div>
{queryResponse.isFetching && !queryResponse.isError && (
<Spinner style={{ paddingRight: '0.25rem' }} />
)}
{queryResponse.isError && (
<Tooltip
title={errorMessage}
placement={errorTooltipPosition}
className="widget-api-actions"
>
<ExclamationCircleOutlined />
</Tooltip>
)}
<ThesholdContainer>{threshold}</ThesholdContainer>
{queryResponse.isFetching && !queryResponse.isError && (
<Spinner height="5vh" style={spinnerStyles} />
)}
{queryResponse.isError && (
<Tooltip title={errorMessage} placement={errorTooltipPosition}>
<ExclamationCircleOutlined style={tooltipStyles} />
</Tooltip>
)}
{isWarning && (
<Tooltip title={WARNING_MESSAGE} placement={errorTooltipPosition}>
<WarningOutlined style={tooltipStyles} />
</Tooltip>
)}
</WidgetHeaderContainer>
{isWarning && (
<Tooltip
title={WARNING_MESSAGE}
placement={errorTooltipPosition}
className="widget-api-actions"
>
<WarningOutlined />
</Tooltip>
)}
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
<MoreOutlined
className={`widget-header-more-options ${
parentHover ? 'widget-header-hover' : ''
}`}
/>
</Dropdown>
</div>
</div>
);
}

View File

@@ -41,8 +41,6 @@ export const WidgetHeaderContainer = styled.div`
export const ArrowContainer = styled.span<{ hover: boolean }>`
visibility: ${({ hover }): string => (hover ? 'visible' : 'hidden')};
position: absolute;
right: -1rem;
`;
export const Typography = styled(TypographyComponent)`

View File

@@ -2,7 +2,7 @@ import { Button as ButtonComponent, Card as CardComponent, Space } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { StyledCSS } from 'container/GantChart/Trace/styles';
import RGL, { WidthProvider } from 'react-grid-layout';
import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
import styled, { css } from 'styled-components';
const ReactGridLayoutComponent = WidthProvider(RGL);
@@ -17,14 +17,8 @@ export const Card = styled(CardComponent)<CardProps>`
}
.ant-card-body {
height: 90%;
height: calc(100% - 40px);
padding: 0;
${({ $panelType }): FlattenSimpleInterpolation =>
$panelType === PANEL_TYPES.TABLE
? css`
padding-top: 1.8rem;
`
: css``}
}
`;

View File

@@ -41,7 +41,7 @@ function GridTableComponent({
const idx = thresholds.findIndex(
(t) => t.thresholdTableOptions === e.title,
);
if (idx !== -1) {
if (threshold && idx !== -1) {
return (
<div
style={

View File

@@ -1,7 +1,7 @@
import styled from 'styled-components';
export const WrapperStyled = styled.div`
height: 100%;
height: 95%;
overflow: hidden;
& .ant-table-wrapper {
@@ -19,5 +19,13 @@ export const WrapperStyled = styled.div`
& .ant-table {
flex: 1;
overflow: auto;
> .ant-table-container {
> .ant-table-content {
> table {
min-width: 99% !important;
}
}
}
}
`;

View File

@@ -8,5 +8,18 @@
.upgrade-link {
padding: 0px;
padding-right: 4px;
display: inline !important;
color: white;
text-decoration: underline;
text-decoration-color: white;
text-decoration-thickness: 2px;
text-underline-offset: 2px;
&:hover {
color: white;
text-decoration: underline;
text-decoration-color: white;
text-decoration-thickness: 2px;
text-underline-offset: 2px;
}
}

View File

@@ -1,3 +1,6 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/anchor-is-valid */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './Header.styles.scss';
import {
@@ -135,16 +138,17 @@ function HeaderContainer(): JSX.Element {
<>
{showTrialExpiryBanner && (
<div className="trial-expiry-banner">
You are in free trial period. Your free trial will end on
You are in free trial period. Your free trial will end on{' '}
<span>
{getFormattedDate(licenseData?.payload?.trialEnd || Date.now())}.
</span>
{role === 'ADMIN' ? (
<span>
Please
<Button className="upgrade-link" type="link" onClick={handleUpgrade}>
{' '}
Please{' '}
<a className="upgrade-link" onClick={handleUpgrade}>
upgrade
</Button>
</a>
to continue using SigNoz features.
</span>
) : (

View File

@@ -1,7 +1,7 @@
/* eslint-disable react/display-name */
import { PlusOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
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 {
@@ -14,9 +14,12 @@ import LabelColumn from 'components/TableRenderer/LabelColumn';
import TextToolTip from 'components/TextToolTip';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import useSortableTable from 'hooks/ResizeTable/useSortableTable';
import useComponentPermission from 'hooks/useComponentPermission';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import useInterval from 'hooks/useInterval';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { useCallback, useState } from 'react';
@@ -29,12 +32,19 @@ import { GettableAlert } from 'types/api/alerts/get';
import AppReducer from 'types/reducer/app';
import DeleteAlert from './DeleteAlert';
import { Button, ButtonContainer, ColumnButton } from './styles';
import {
Button,
ButtonContainer,
ColumnButton,
SearchContainer,
} from './styles';
import Status from './TableComponents/Status';
import ToggleAlertState from './ToggleAlertState';
import { filterAlerts } from './utils';
const { Search } = Input;
function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const [data, setData] = useState<GettableAlert[]>(allAlertRules || []);
const { t } = useTranslation('common');
const { role, featureResponse } = useSelector<AppState, AppReducer>(
(state) => state.app,
@@ -44,13 +54,39 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
role,
);
const params = useUrlQuery();
const orderColumnParam = params.get('columnKey');
const orderQueryParam = params.get('order');
const paginationParam = params.get('page');
const searchParams = params.get('search');
const [searchString, setSearchString] = useState<string>(searchParams || '');
const [data, setData] = useState<GettableAlert[]>(() => {
const value = searchString.toLowerCase();
const filteredData = filterAlerts(allAlertRules, value);
return filteredData || [];
});
// Type asuring
const sortingOrder: 'ascend' | 'descend' | null =
orderQueryParam === 'ascend' || orderQueryParam === 'descend'
? orderQueryParam
: null;
const { sortedInfo, handleChange } = useSortableTable<GettableAlert>(
sortingOrder,
orderColumnParam || '',
searchString,
);
const { notifications: notificationsApi } = useNotifications();
useInterval(() => {
(async (): Promise<void> => {
const { data: refetchData, status } = await refetch();
if (status === 'success') {
setData(refetchData?.payload || []);
const value = searchString.toLowerCase();
const filteredData = filterAlerts(refetchData.payload || [], value);
setData(filteredData || []);
}
if (status === 'error') {
notificationsApi.error({
@@ -128,6 +164,13 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
}
};
const handleSearch = useDebouncedFn((e: unknown) => {
const value = (e as React.BaseSyntheticEvent).target.value.toLowerCase();
setSearchString(value);
const filteredData = filterAlerts(allAlertRules, value);
setData(filteredData);
});
const dynamicColumns: ColumnsType<GettableAlert> = [
{
title: 'Created At',
@@ -142,6 +185,10 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
return prev - next;
},
render: DateComponent,
sortOrder:
sortedInfo.columnKey === DynamicColumnsKey.CreatedAt
? sortedInfo.order
: null,
},
{
title: 'Created By',
@@ -163,6 +210,10 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
return prev - next;
},
render: DateComponent,
sortOrder:
sortedInfo.columnKey === DynamicColumnsKey.UpdatedAt
? sortedInfo.order
: null,
},
{
title: 'Updated By',
@@ -183,6 +234,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
(b.state ? b.state.charCodeAt(0) : 1000) -
(a.state ? a.state.charCodeAt(0) : 1000),
render: (value): JSX.Element => <Status status={value} />,
sortOrder: sortedInfo.columnKey === 'state' ? sortedInfo.order : null,
},
{
title: 'Alert Name',
@@ -198,6 +250,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
render: (value, record): JSX.Element => (
<Typography.Link onClick={onEditHandler(record)}>{value}</Typography.Link>
),
sortOrder: sortedInfo.columnKey === 'name' ? sortedInfo.order : null,
},
{
title: 'Severity',
@@ -214,6 +267,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
return <Typography>{severityValue}</Typography>;
},
sortOrder: sortedInfo.columnKey === 'severity' ? sortedInfo.order : null,
},
{
title: 'Labels',
@@ -271,26 +325,37 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
return (
<>
<ButtonContainer>
<TextToolTip
{...{
text: `More details on how to create alerts`,
url: 'https://signoz.io/docs/userguide/alerts-management/',
}}
<SearchContainer>
<Search
placeholder="Search by Alert Name, Severity and Labels"
onChange={handleSearch}
defaultValue={searchString}
/>
<ButtonContainer>
<TextToolTip
{...{
text: `More details on how to create alerts`,
url: 'https://signoz.io/docs/userguide/alerts-management/',
}}
/>
{addNewAlert && (
<Button onClick={onClickNewAlertHandler} icon={<PlusOutlined />}>
New Alert
</Button>
)}
</ButtonContainer>
{addNewAlert && (
<Button onClick={onClickNewAlertHandler} icon={<PlusOutlined />}>
New Alert
</Button>
)}
</ButtonContainer>
</SearchContainer>
<DynamicColumnTable
tablesource={TableDataSource.Alert}
columns={columns}
rowKey="id"
dataSource={data}
dynamicColumns={dynamicColumns}
onChange={handleChange}
pagination={{
defaultCurrent: Number(paginationParam) || 1,
}}
/>
</>
);

View File

@@ -1,11 +1,17 @@
import { Button as ButtonComponent } from 'antd';
import styled from 'styled-components';
export const SearchContainer = styled.div`
&&& {
display: flex;
margin-bottom: 2rem;
align-items: center;
gap: 2rem;
}
`;
export const ButtonContainer = styled.div`
&&& {
display: flex;
justify-content: flex-end;
margin-bottom: 2rem;
align-items: center;
}
`;

View File

@@ -0,0 +1,25 @@
import { GettableAlert } from 'types/api/alerts/get';
export const filterAlerts = (
allAlertRules: GettableAlert[],
filter: string,
): GettableAlert[] => {
const value = filter.toLowerCase();
return allAlertRules.filter((alert) => {
const alertName = alert.alert.toLowerCase();
const severity = alert.labels?.severity.toLowerCase();
const labels = Object.keys(alert.labels || {})
.filter((e) => e !== 'severity')
.join(' ')
.toLowerCase();
const labelValue = Object.values(alert.labels || {});
return (
alertName.includes(value) ||
severity?.includes(value) ||
labels.includes(value) ||
labelValue.includes(value)
);
});
};

View File

@@ -0,0 +1,378 @@
import { PlusOutlined } from '@ant-design/icons';
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 {
DynamicColumnsKey,
TableDataSource,
} from 'components/ResizeTable/contants';
import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable';
import LabelColumn from 'components/TableRenderer/LabelColumn';
import TextToolTip from 'components/TextToolTip';
import ROUTES from 'constants/routes';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import history from 'lib/history';
import { Key, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { generatePath } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Dashboard } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import DateComponent from '../../components/ResizeTable/TableComponent/DateComponent';
import ImportJSON from './ImportJSON';
import { ButtonContainer, NewDashboardButton, TableContainer } from './styles';
import DeleteButton from './TableComponents/DeleteButton';
import Name from './TableComponents/Name';
const { Search } = Input;
function DashboardsList(): JSX.Element {
const {
data: dashboardListResponse = [],
isLoading: isDashboardListLoading,
refetch: refetchDashboardList,
} = useGetAllDashboard();
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [action, createNewDashboard] = useComponentPermission(
['action', 'create_new_dashboards'],
role,
);
const { t } = useTranslation('dashboard');
const [
isImportJSONModalVisible,
setIsImportJSONModalVisible,
] = useState<boolean>(false);
const [uploadedGrafana, setUploadedGrafana] = useState<boolean>(false);
const [isFilteringDashboards, setIsFilteringDashboards] = useState(false);
const [dashboards, setDashboards] = useState<Dashboard[]>();
const sortDashboardsByCreatedAt = (dashboards: Dashboard[]): void => {
const sortedDashboards = dashboards.sort(
(a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
);
setDashboards(sortedDashboards);
};
useEffect(() => {
sortDashboardsByCreatedAt(dashboardListResponse);
}, [dashboardListResponse]);
const [newDashboardState, setNewDashboardState] = useState({
loading: false,
error: false,
errorMessage: '',
});
const dynamicColumns: TableColumnProps<Data>[] = [
{
title: 'Created At',
dataIndex: 'createdAt',
width: 30,
key: DynamicColumnsKey.CreatedAt,
sorter: (a: Data, b: Data): number => {
console.log({ a });
const prev = new Date(a.createdAt).getTime();
const next = new Date(b.createdAt).getTime();
return prev - next;
},
render: DateComponent,
},
{
title: 'Created By',
dataIndex: 'createdBy',
width: 30,
key: DynamicColumnsKey.CreatedBy,
},
{
title: 'Last Updated Time',
width: 30,
dataIndex: 'lastUpdatedTime',
key: DynamicColumnsKey.UpdatedAt,
sorter: (a: Data, b: Data): number => {
const prev = new Date(a.lastUpdatedTime).getTime();
const next = new Date(b.lastUpdatedTime).getTime();
return prev - next;
},
render: DateComponent,
},
{
title: 'Last Updated By',
dataIndex: 'lastUpdatedBy',
width: 30,
key: DynamicColumnsKey.UpdatedBy,
},
];
const columns = useMemo(() => {
const tableColumns: TableColumnProps<Data>[] = [
{
title: 'Name',
dataIndex: 'name',
width: 40,
render: Name,
},
{
title: 'Description',
width: 50,
dataIndex: 'description',
},
{
title: 'Tags',
dataIndex: 'tags',
width: 50,
render: (value): JSX.Element => <LabelColumn labels={value} />,
},
];
if (action) {
tableColumns.push({
title: 'Action',
dataIndex: '',
width: 40,
render: DeleteButton,
});
}
return tableColumns;
}, [action]);
const data: Data[] =
dashboards?.map((e) => ({
createdAt: e.created_at,
description: e.data.description || '',
id: e.uuid,
lastUpdatedTime: e.updated_at,
name: e.data.title,
tags: e.data.tags || [],
key: e.uuid,
createdBy: e.created_by,
isLocked: !!e.isLocked || false,
lastUpdatedBy: e.updated_by,
refetchDashboardList,
})) || [];
const onNewDashboardHandler = useCallback(async () => {
try {
setNewDashboardState({
...newDashboardState,
loading: true,
});
const response = await createDashboard({
title: t('new_dashboard_title', {
ns: 'dashboard',
}),
uploadedGrafana: false,
});
if (response.statusCode === 200) {
history.push(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.payload.uuid,
}),
);
} else {
setNewDashboardState({
...newDashboardState,
loading: false,
error: true,
errorMessage: response.error || 'Something went wrong',
});
}
} catch (error) {
setNewDashboardState({
...newDashboardState,
error: true,
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
});
}
}, [newDashboardState, t]);
const getText = useCallback(() => {
if (!newDashboardState.error && !newDashboardState.loading) {
return 'New Dashboard';
}
if (newDashboardState.loading) {
return 'Loading';
}
return newDashboardState.errorMessage;
}, [
newDashboardState.error,
newDashboardState.errorMessage,
newDashboardState.loading,
]);
const onModalHandler = (uploadedGrafana: boolean): void => {
setIsImportJSONModalVisible((state) => !state);
setUploadedGrafana(uploadedGrafana);
};
const getMenuItems = useMemo(() => {
const menuItems: ItemType[] = [
{
key: t('import_json').toString(),
label: t('import_json'),
onClick: (): void => onModalHandler(false),
},
{
key: t('import_grafana_json').toString(),
label: t('import_grafana_json'),
onClick: (): void => onModalHandler(true),
disabled: true,
},
];
if (createNewDashboard) {
menuItems.unshift({
key: t('create_dashboard').toString(),
label: t('create_dashboard'),
disabled: isDashboardListLoading,
onClick: onNewDashboardHandler,
});
}
return menuItems;
}, [createNewDashboard, isDashboardListLoading, onNewDashboardHandler, t]);
const searchArrayOfObjects = (searchValue: string): any[] => {
// Convert the searchValue to lowercase for case-insensitive search
const searchValueLowerCase = searchValue.toLowerCase();
// Use the filter method to find matching objects
return dashboardListResponse.filter((item: any) => {
// Convert each property value to lowercase for case-insensitive search
const itemValues = Object.values(item?.data).map((value: any) =>
value.toString().toLowerCase(),
);
// Check if any property value contains the searchValue
return itemValues.some((value) => value.includes(searchValueLowerCase));
});
};
const handleSearch = useDebouncedFn((event: unknown): void => {
setIsFilteringDashboards(true);
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
const filteredDashboards = searchArrayOfObjects(searchText);
setDashboards(filteredDashboards);
setIsFilteringDashboards(false);
}, 500);
const GetHeader = useMemo(
() => (
<Row gutter={16} align="middle">
<Col span={18}>
<Search
disabled={isDashboardListLoading}
placeholder="Search by Name, Description, Tags"
onChange={handleSearch}
loading={isFilteringDashboards}
style={{ marginBottom: 16, marginTop: 16 }}
/>
</Col>
<Col
span={6}
style={{
display: 'flex',
justifyContent: 'flex-end',
}}
>
<ButtonContainer>
<TextToolTip
{...{
text: `More details on how to create dashboards`,
url: 'https://signoz.io/docs/userguide/dashboards',
}}
/>
</ButtonContainer>
<Dropdown
menu={{ items: getMenuItems }}
disabled={isDashboardListLoading}
placement="bottomRight"
>
<NewDashboardButton
icon={<PlusOutlined />}
type="primary"
data-testid="create-new-dashboard"
loading={newDashboardState.loading}
danger={newDashboardState.error}
>
{getText()}
</NewDashboardButton>
</Dropdown>
</Col>
</Row>
),
[
isDashboardListLoading,
handleSearch,
isFilteringDashboards,
getMenuItems,
newDashboardState.loading,
newDashboardState.error,
getText,
],
);
return (
<Card>
{GetHeader}
<TableContainer>
<ImportJSON
isImportJSONModalVisible={isImportJSONModalVisible}
uploadedGrafana={uploadedGrafana}
onModalHandler={(): void => onModalHandler(false)}
/>
<DynamicColumnTable
tablesource={TableDataSource.Dashboard}
dynamicColumns={dynamicColumns}
columns={columns}
pagination={{
pageSize: 10,
defaultPageSize: 10,
total: data?.length || 0,
}}
showHeader
bordered
sticky
loading={isDashboardListLoading}
dataSource={data}
showSorterTooltip
/>
</TableContainer>
</Card>
);
}
export interface Data {
key: Key;
name: string;
description: string;
tags: string[];
createdBy: string;
createdAt: string;
lastUpdatedTime: string;
lastUpdatedBy: string;
isLocked: boolean;
id: string;
}
export default DashboardsList;

View File

@@ -2,7 +2,7 @@ import { Typography } from 'antd';
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
import getFormattedDate from 'lib/getFormatedDate';
import { Data } from '..';
import { Data } from '../DashboardsList';
function Created(createdBy: Data['createdBy']): JSX.Element {
const time = new Date(createdBy);

View File

@@ -2,6 +2,7 @@ import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
import { Modal, Tooltip, Typography } from 'antd';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard';
import { useNotifications } from 'hooks/useNotifications';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
@@ -10,7 +11,7 @@ import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { Data } from '..';
import { Data } from '../DashboardsList';
import { TableLinkText } from './styles';
interface DeleteButtonProps {
@@ -32,6 +33,8 @@ function DeleteButton({
const queryClient = useQueryClient();
const { notifications } = useNotifications();
const { t } = useTranslation(['dashboard']);
const deleteDashboardMutation = useDeleteDashboard(id);
@@ -49,6 +52,11 @@ function DeleteButton({
onOk() {
deleteDashboardMutation.mutateAsync(undefined, {
onSuccess: () => {
notifications.success({
message: t('dashboard:delete_dashboard_success', {
name,
}),
});
queryClient.invalidateQueries([REACT_QUERY_KEY.GET_ALL_DASHBOARDS]);
},
});
@@ -57,7 +65,7 @@ function DeleteButton({
okButtonProps: { danger: true },
centered: true,
});
}, [modal, name, deleteDashboardMutation, queryClient]);
}, [modal, name, deleteDashboardMutation, notifications, t, queryClient]);
const getDeleteTooltipContent = (): string => {
if (isLocked) {

View File

@@ -2,7 +2,7 @@ import { LockFilled } from '@ant-design/icons';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { Data } from '..';
import { Data } from '../DashboardsList';
import { TableLinkText } from './styles';
function Name(name: Data['name'], data: Data): JSX.Element {

View File

@@ -1,7 +1,7 @@
/* eslint-disable react/destructuring-assignment */
import { Tag } from 'antd';
import { Data } from '../index';
import { Data } from '../DashboardsList';
function Tags(data: Data['tags']): JSX.Element {
return (

View File

@@ -1,400 +1,3 @@
import { PlusOutlined } from '@ant-design/icons';
import {
Card,
Col,
Dropdown,
Input,
MenuProps,
Row,
TableColumnProps,
} from 'antd';
import { ItemType } from 'antd/es/menu/hooks/useItems';
import createDashboard from 'api/dashboard/create';
import { AxiosError } from 'axios';
import {
DynamicColumnsKey,
TableDataSource,
} from 'components/ResizeTable/contants';
import DynamicColumnTable from 'components/ResizeTable/DynamicColumnTable';
import LabelColumn from 'components/TableRenderer/LabelColumn';
import TextToolTip from 'components/TextToolTip';
import ROUTES from 'constants/routes';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import history from 'lib/history';
import { Key, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { generatePath } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Dashboard } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { popupContainer } from 'utils/selectPopupContainer';
import DashboardsList from './DashboardsList';
import DateComponent from '../../components/ResizeTable/TableComponent/DateComponent';
import ImportJSON from './ImportJSON';
import { ButtonContainer, NewDashboardButton, TableContainer } from './styles';
import DeleteButton from './TableComponents/DeleteButton';
import Name from './TableComponents/Name';
function ListOfAllDashboard(): JSX.Element {
const { Search } = Input;
const {
data: dashboardListResponse = [],
isLoading: isDashboardListLoading,
refetch: refetchDashboardList,
} = useGetAllDashboard();
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [action, createNewDashboard, newDashboard] = useComponentPermission(
['action', 'create_new_dashboards', 'new_dashboard'],
role,
);
const { t } = useTranslation('dashboard');
const [
isImportJSONModalVisible,
setIsImportJSONModalVisible,
] = useState<boolean>(false);
const [uploadedGrafana, setUploadedGrafana] = useState<boolean>(false);
const [isFilteringDashboards, setIsFilteringDashboards] = useState(false);
const [dashboards, setDashboards] = useState<Dashboard[]>();
const sortDashboardsByCreatedAt = (dashboards: Dashboard[]): void => {
const sortedDashboards = dashboards.sort(
(a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
);
setDashboards(sortedDashboards);
};
useEffect(() => {
if (dashboardListResponse.length) {
sortDashboardsByCreatedAt(dashboardListResponse);
}
}, [dashboardListResponse]);
const [newDashboardState, setNewDashboardState] = useState({
loading: false,
error: false,
errorMessage: '',
});
const dynamicColumns: TableColumnProps<Data>[] = [
{
title: 'Created At',
dataIndex: 'createdAt',
width: 30,
key: DynamicColumnsKey.CreatedAt,
sorter: (a: Data, b: Data): number => {
console.log({ a });
const prev = new Date(a.createdAt).getTime();
const next = new Date(b.createdAt).getTime();
return prev - next;
},
render: DateComponent,
},
{
title: 'Created By',
dataIndex: 'createdBy',
width: 30,
key: DynamicColumnsKey.CreatedBy,
},
{
title: 'Last Updated Time',
width: 30,
dataIndex: 'lastUpdatedTime',
key: DynamicColumnsKey.UpdatedAt,
sorter: (a: Data, b: Data): number => {
const prev = new Date(a.lastUpdatedTime).getTime();
const next = new Date(b.lastUpdatedTime).getTime();
return prev - next;
},
render: DateComponent,
},
{
title: 'Last Updated By',
dataIndex: 'lastUpdatedBy',
width: 30,
key: DynamicColumnsKey.UpdatedBy,
},
];
const columns = useMemo(() => {
const tableColumns: TableColumnProps<Data>[] = [
{
title: 'Name',
dataIndex: 'name',
width: 40,
render: Name,
},
{
title: 'Description',
width: 50,
dataIndex: 'description',
},
{
title: 'Tags',
dataIndex: 'tags',
width: 50,
render: (value): JSX.Element => <LabelColumn labels={value} />,
},
];
if (action) {
tableColumns.push({
title: 'Action',
dataIndex: '',
width: 40,
render: DeleteButton,
});
}
return tableColumns;
}, [action]);
const data: Data[] =
dashboards?.map((e) => ({
createdAt: e.created_at,
description: e.data.description || '',
id: e.uuid,
lastUpdatedTime: e.updated_at,
name: e.data.title,
tags: e.data.tags || [],
key: e.uuid,
createdBy: e.created_by,
isLocked: !!e.isLocked || false,
lastUpdatedBy: e.updated_by,
refetchDashboardList,
})) || [];
const onNewDashboardHandler = useCallback(async () => {
try {
setNewDashboardState({
...newDashboardState,
loading: true,
});
const response = await createDashboard({
title: t('new_dashboard_title', {
ns: 'dashboard',
}),
uploadedGrafana: false,
});
if (response.statusCode === 200) {
history.push(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.payload.uuid,
}),
);
} else {
setNewDashboardState({
...newDashboardState,
loading: false,
error: true,
errorMessage: response.error || 'Something went wrong',
});
}
} catch (error) {
setNewDashboardState({
...newDashboardState,
error: true,
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
});
}
}, [newDashboardState, t]);
const getText = useCallback(() => {
if (!newDashboardState.error && !newDashboardState.loading) {
return 'New Dashboard';
}
if (newDashboardState.loading) {
return 'Loading';
}
return newDashboardState.errorMessage;
}, [
newDashboardState.error,
newDashboardState.errorMessage,
newDashboardState.loading,
]);
const onModalHandler = (uploadedGrafana: boolean): void => {
setIsImportJSONModalVisible((state) => !state);
setUploadedGrafana(uploadedGrafana);
};
const getMenuItems = useMemo(() => {
const menuItems: ItemType[] = [];
if (createNewDashboard) {
menuItems.push({
key: t('create_dashboard').toString(),
label: t('create_dashboard'),
disabled: isDashboardListLoading,
onClick: onNewDashboardHandler,
});
}
menuItems.push({
key: t('import_json').toString(),
label: t('import_json'),
onClick: (): void => onModalHandler(false),
});
menuItems.push({
key: t('import_grafana_json').toString(),
label: t('import_grafana_json'),
onClick: (): void => onModalHandler(true),
disabled: true,
});
return menuItems;
}, [createNewDashboard, isDashboardListLoading, onNewDashboardHandler, t]);
const menu: MenuProps = useMemo(
() => ({
items: getMenuItems,
}),
[getMenuItems],
);
const searchArrayOfObjects = (searchValue: string): any[] => {
// Convert the searchValue to lowercase for case-insensitive search
const searchValueLowerCase = searchValue.toLowerCase();
// Use the filter method to find matching objects
return dashboardListResponse.filter((item: any) => {
// Convert each property value to lowercase for case-insensitive search
const itemValues = Object.values(item?.data).map((value: any) =>
value.toString().toLowerCase(),
);
// Check if any property value contains the searchValue
return itemValues.some((value) => value.includes(searchValueLowerCase));
});
};
const handleSearch = useDebouncedFn((event: unknown): void => {
setIsFilteringDashboards(true);
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
const filteredDashboards = searchArrayOfObjects(searchText);
setDashboards(filteredDashboards);
setIsFilteringDashboards(false);
}, 500);
const GetHeader = useMemo(
() => (
<Row gutter={16} align="middle">
<Col span={18}>
<Search
disabled={isDashboardListLoading}
placeholder="Search by Name, Description, Tags"
onChange={handleSearch}
loading={isFilteringDashboards}
style={{ marginBottom: 16, marginTop: 16 }}
/>
</Col>
<Col
span={6}
style={{
display: 'flex',
justifyContent: 'flex-end',
}}
>
<ButtonContainer>
<TextToolTip
{...{
text: `More details on how to create dashboards`,
url: 'https://signoz.io/docs/userguide/dashboards',
}}
/>
{newDashboard && (
<Dropdown
getPopupContainer={popupContainer}
disabled={isDashboardListLoading}
trigger={['click']}
menu={menu}
>
<NewDashboardButton
icon={<PlusOutlined />}
type="primary"
data-testid="create-new-dashboard"
loading={newDashboardState.loading}
danger={newDashboardState.error}
>
{getText()}
</NewDashboardButton>
</Dropdown>
)}
</ButtonContainer>
</Col>
</Row>
),
[
Search,
isDashboardListLoading,
handleSearch,
isFilteringDashboards,
newDashboard,
menu,
newDashboardState.loading,
newDashboardState.error,
getText,
],
);
return (
<Card>
{GetHeader}
<TableContainer>
<ImportJSON
isImportJSONModalVisible={isImportJSONModalVisible}
uploadedGrafana={uploadedGrafana}
onModalHandler={(): void => onModalHandler(false)}
/>
<DynamicColumnTable
tablesource={TableDataSource.Dashboard}
dynamicColumns={dynamicColumns}
columns={columns}
pagination={{
pageSize: 10,
defaultPageSize: 10,
total: data?.length || 0,
}}
showHeader
bordered
sticky
loading={isDashboardListLoading}
dataSource={data}
showSorterTooltip
/>
</TableContainer>
</Card>
);
}
export interface Data {
key: Key;
name: string;
description: string;
tags: string[];
createdBy: string;
createdAt: string;
lastUpdatedTime: string;
lastUpdatedBy: string;
isLocked: boolean;
id: string;
}
export default ListOfAllDashboard;
export default DashboardsList;

View File

@@ -1,4 +1,5 @@
import { Card, Typography } from 'antd';
import LogDetail from 'components/LogDetail';
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
import Spinner from 'components/Spinner';
@@ -10,6 +11,7 @@ import { InfinityWrapperStyled } from 'container/LogsExplorerList/styles';
import { convertKeysToColumnFields } from 'container/LogsExplorerList/utils';
import { Heading } from 'container/LogsTable/styles';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import useFontFaceObserver from 'hooks/useFontObserver';
import { useEventSource } from 'providers/EventSource';
@@ -31,6 +33,13 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
const { activeLogId } = useCopyLogLink();
const {
activeLog,
onClearActiveLog,
onAddToQuery,
onSetActiveLog,
} = useActiveLog();
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: DataSource.LOGS,
@@ -66,10 +75,22 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
}
return (
<ListLogView key={log.id} logData={log} selectedFields={selectedFields} />
<ListLogView
key={log.id}
logData={log}
selectedFields={selectedFields}
onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog}
/>
);
},
[options.format, options.maxLines, selectedFields],
[
onAddToQuery,
onSetActiveLog,
options.format,
options.maxLines,
selectedFields,
],
);
useEffect(() => {
@@ -123,6 +144,12 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
)}
</InfinityWrapperStyled>
)}
<LogDetail
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
/>
</>
);
}

View File

@@ -22,6 +22,7 @@ import { ILog } from 'types/api/logs/log';
import ActionItem, { ActionItemProps } from './ActionItem';
import FieldRenderer from './FieldRenderer';
import {
filterKeyForField,
flattenObject,
jsonToDataNodes,
recursiveParseJSON,
@@ -98,11 +99,12 @@ function TableView({
title: 'Action',
width: 11,
render: (fieldData: Record<string, string>): JSX.Element | null => {
const fieldKey = fieldData.field.split('.').slice(-1);
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
const fieldFilterKey = filterKeyForField(fieldData.field);
if (!RESTRICTED_FIELDS.includes(fieldFilterKey)) {
return (
<ActionItem
fieldKey={fieldKey[0]}
fieldKey={fieldFilterKey}
fieldValue={fieldData.value}
onClickActionItem={onClickActionItem}
/>
@@ -119,7 +121,6 @@ function TableView({
align: 'left',
ellipsis: true,
render: (field: string, record): JSX.Element => {
const fieldKey = field.split('.').slice(-1);
const renderedField = <FieldRenderer field={field} />;
if (record.field === 'trace_id') {
@@ -148,10 +149,11 @@ function TableView({
);
}
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
const fieldFilterKey = filterKeyForField(field);
if (!RESTRICTED_FIELDS.includes(fieldFilterKey)) {
return (
<AddToQueryHOC
fieldKey={fieldKey[0]}
fieldKey={fieldFilterKey}
fieldValue={flattenLogData[field]}
onAddToQuery={onAddToQuery}
>

View File

@@ -132,6 +132,16 @@ export const generateFieldKeyForArray = (
export const removeObjectFromString = (str: string): string =>
str.replace(/\[object Object\]./g, '');
// Split `str` on the first occurrence of `delimiter`
// For example, will return `['a', 'b.c']` when splitting `'a.b.c'` at dots
const splitOnce = (str: string, delimiter: string): string[] => {
const parts = str.split(delimiter);
if (parts.length < 2) {
return parts;
}
return [parts[0], parts.slice(1).join(delimiter)];
};
export const getFieldAttributes = (field: string): IFieldAttributes => {
let dataType;
let newField;
@@ -140,18 +150,30 @@ export const getFieldAttributes = (field: string): IFieldAttributes => {
if (field.startsWith('attributes_')) {
logType = MetricsType.Tag;
const stringWithoutPrefix = field.slice('attributes_'.length);
const parts = stringWithoutPrefix.split('.');
const parts = splitOnce(stringWithoutPrefix, '.');
[dataType, newField] = parts;
} else if (field.startsWith('resources_')) {
logType = MetricsType.Resource;
const stringWithoutPrefix = field.slice('resources_'.length);
const parts = stringWithoutPrefix.split('.');
const parts = splitOnce(stringWithoutPrefix, '.');
[dataType, newField] = parts;
}
return { dataType, newField, logType };
};
// Returns key to be used when filtering for `field` via
// the query builder. This is useful for powering filtering
// by field values from log details view.
export const filterKeyForField = (field: string): string => {
// Must work for all 3 of the following types of cases
// timestamp -> timestamp
// attributes_string.log.file -> log.file
// resources_string.k8s.pod.name -> k8s.pod.name
const fieldAttribs = getFieldAttributes(field);
return fieldAttribs?.newField || field;
};
export const aggregateAttributesResourcesToString = (logData: ILog): string => {
const outputJson: ILogAggregateAttributesResources = {
body: logData.body,

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