Compare commits

...

227 Commits

Author SHA1 Message Date
Ankit Nayan
bf2f3f8f5e Merge pull request #1402 from zriyansh/develop
update contributing.md file
2022-07-16 14:29:21 +05:30
Amol Umbark
d92aad38df fix: removed background color from sections in alert ui (#1413) 2022-07-16 12:08:17 +05:30
Pranay Prateek
78d13c94ae Merge branch 'develop' into develop 2022-07-15 23:43:04 +05:30
Ankit Nayan
73e699080d Merge pull request #1406 from SigNoz/release/v0.10.0
Release/v0.10.0
2022-07-15 22:19:13 +05:30
Prashant Shahi
b6aa378fae chore(release): 📌 pin versions: SigNoz 0.10.0, OtelCollector 0.45.1-1.1
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2022-07-15 21:46:32 +05:30
Amol Umbark
b5fe3d7fa1 fix: changed translation file for rules (#1405) 2022-07-15 21:33:30 +05:30
Amol Umbark
7c14a75c68 feat: Right side panel in create/edit alert UI (#1404)
* feat: metrics builder

* feat: tag key selection

* feat: metrics builder

* poc version

* added more changes to query builder

* added types for composite queries

* (feat): added edit rules and create rules forms

* added label key value input item

* (chore): added hidden labels for labelinput

* (chore): resolved some merge conflicts from develop

* (chore): added translations

* (chore): removed some old files in metric builder

* (chore): restored some of the files from develop branch

* (chore): restored env.ts

* (fix): solved empty builder queries errors

* (fix): changed queryIndex and formulaIndex type to string|number from number

* (feat): added chart preview for alert metric ui

* (feat): added threshold in chart, translations in alert form and a few fixes

* (fix): restoring env.ts

* (fix): placed threshold on horizontal line

* fix: resolved review comments

* fix: resolved label remove issue

* fix: removed console log

* fix: resolved issue with edit rule - old state values shown after update of threshold

* fix: resolved issue with match condition dropdown in alert ui

* fix: increased size of timeframe drop down

* fix: fixed label key value field and chart auto update when eval window changes

* feat: added a link for alert name in list alerts page and source for each rule update

* fix: resolved review coments in querysection of alerts ui

* feat: adding panel user guide in alerting form

* feat: added user guide panel in the alert form

* feat: added more help icon in user guide and fixed the sizing issue

Co-authored-by: Pranshu Chittora <pranshu@signoz.io>
2022-07-15 17:48:16 +05:30
Prashant Shahi
10c6325e46 chore(clickhouse): 🔊 update logging level to info (#1401)
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2022-07-15 17:10:27 +05:30
Prashant Shahi
e4883495c3 fix(exceptions-page): 🚑 unix nanoseconds operations (#1403)
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2022-07-15 16:40:45 +05:30
Palash
b9d63d6b8f feat: text is now ellipsed (#1392)
* feat: text is now ellipsed
2022-07-15 14:17:29 +05:30
Priyansh Khodiyar
65804f245c Merge branch 'SigNoz:develop' into develop 2022-07-15 13:38:54 +05:30
Ankit Anand
964b819f20 Update CONTRIBUTING.md (#1) 2022-07-15 13:38:39 +05:30
Palash
e22be60a9e Create dependency-review.yml (#1360)
* Create dependency-review.yml
2022-07-15 13:01:29 +05:30
Palash
b6a6833a64 test: utils unit case is updated (#1396) 2022-07-15 12:46:57 +05:30
Vishal Sharma
c90e9ffa34 fix: remove requirement of exceptionType and serviceName from errorDetail page URL (#1400)
* fix: remove requirement of exceptionType and serviceName from errorDetail page URL

* chore: id is updated

* chore: commented code is removed

* chore: eslint error is fixed

Co-authored-by: Palash <palashgdev@gmail.com>
2022-07-15 12:35:15 +05:30
Srikanth Chekuri
c5c7fb238f fix: update the error rate percentage text and scale (#1399) 2022-07-15 09:55:43 +05:30
Priyansh Khodiyar
4ad79bee18 add images 2022-07-14 22:51:51 +05:30
Priyansh Khodiyar
bebfaa1c4c Update CONTRIBUTING.md 2022-07-14 22:41:11 +05:30
Prashant Shahi
6fb7e34dbc chore: 🔧 otel-collector config changes (#1388)
* chore: 🔧 otel-collector config changes

* chore: 🗑️  remove redundant users.xml

* chore: 🔧 otel-config changes

- seperate scraper job for otel-collector and otel-collector-metrcs internal metrics
- use resourcedetection only for hostmetrics
- add swarm service name and task name in resource attributes env

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2022-07-14 19:36:19 +05:30
Amol Umbark
a2e1c41343 fix: edit form shows incorrect eval window when 24hours is saved (#1393)
* fix: edit form shows incorrect eval window when 24hours is saved

* fix: edit form shows incorrect eval window when 24hours is saved

* feat: added 4 hour window to alert ui

Co-authored-by: Palash <palashgdev@gmail.com>
2022-07-14 18:23:02 +05:30
Palash
46f258747a Merge pull request #1395 from mindhash/amol/alert-0-10-0-fix3
feat: remove global time selection on alerts pages
2022-07-14 18:15:33 +05:30
Amol Umbark
11c352741d Merge branch 'develop' into amol/alert-0-10-0-fix3 2022-07-14 17:34:50 +05:30
Amol
a63267cf90 feat: remove global time selection on alerts pages 2022-07-14 17:28:30 +05:30
Palash
3200248e98 fix: error page is updated (#1394) 2022-07-14 17:14:13 +05:30
Amol Umbark
a8c7237bbb Alert UI with metrics builder (#1359)
* added more changes to query builder

* added types for composite queries

* (feat): added edit rules and create rules forms

* (feat): added chart preview for alert metric ui

* (feat): added threshold in chart, translations in alert form and a few fixes

* feat: added a link for alert name in list alerts page and source for each rule update

Co-authored-by: Pranshu Chittora <pranshu@signoz.io>
2022-07-14 13:23:15 +05:30
Amol Umbark
3a287b2b16 Alerts backend with metrics (#1346)
Alerts backend with metrics
2022-07-14 11:59:06 +05:30
Srikanth Chekuri
c3d665e119 feat: make SigNoz rpc aware (#1366)
* feat: make SigNoz rpc aware
* chore: update the code/method logic
2022-07-14 10:24:03 +05:30
Srikanth Chekuri
8d03569a0a fix: error rate as a percentage of total requests (#1391) 2022-07-14 10:00:24 +05:30
Srikanth Chekuri
0620cacb0b Revert "ci: add golangci to workflow (#1369)" (#1390)
This reverts commit 7aeaecaf1f.
2022-07-14 09:55:01 +05:30
Priyansh Khodiyar
7da69f6a75 Update CONTRIBUTING.md 2022-07-14 00:21:59 +05:30
Srikanth Chekuri
7aeaecaf1f ci: add golangci to workflow (#1369)
* style: reformat the code to follow go guidelines
* chore: add golangci lint
* chore: remove context check
* chore: go fmt
2022-07-13 23:44:42 +05:30
Priyansh Khodiyar
3ea36092f6 Update CONTRIBUTING.md 2022-07-13 23:44:25 +05:30
Priyansh Khodiyar
8db4793ad6 change main branch to develop branch for contribution 2022-07-13 23:33:41 +05:30
Priyansh Khodiyar
83f3180641 Update CONTRIBUTING.md 2022-07-13 23:28:13 +05:30
Palash
64e638fd58 test: signup page and login page test are updated (#1351)
* test: sign-up test are updated
* test: fail test of version api is added
* test: more test case over signup page is added
* test: coverage is added
* chore: auth json is updated
* test: auth token and refresh token test is updated
2022-07-13 20:43:36 +05:30
Palash
5554cce379 feat: exception page is updated (#1376)
* chore: all error utils is added

* chore: error page list is added with total page and other handlings

* test: unit test case for order is added
2022-07-13 19:49:27 +05:30
Priyansh Khodiyar
3e2a6df200 Update CONTRIBUTING.md 2022-07-13 18:08:02 +05:30
Priyansh Khodiyar
3dc1dc970f v6 2022-07-13 18:00:46 +05:30
Priyansh Khodiyar
0ceaa56679 v6 2022-07-13 17:46:48 +05:30
Priyansh Khodiyar
ab52538e91 v5 2022-07-13 17:43:23 +05:30
Priyansh Khodiyar
ef69505bf9 remove arm version of docker-compose file 2022-07-13 17:22:31 +05:30
Priyansh Khodiyar
61b79742dc v4 2022-07-13 17:17:37 +05:30
Palash
4d1516e3fc chore: removed stale make commands (#1340)
Co-authored-by: Prashant Shahi <prashant@signoz.io>
2022-07-13 16:08:46 +05:30
Pranshu Chittora
0b08c80038 chore: tests for span to trace tree with missing spans support (#1368)
* chore: tests for span to trace tree with missing spans support
2022-07-13 15:59:22 +05:30
Vishal Sharma
a84754e8a8 perf: exception page optimization (#1287)
* feat: update ListErrors API

* feat: update error detail APIs and add a new API for fetching next prev error IDs

* feat: update GetNextPrevErrorIDs API to handle an edge case

* perf: use timestamp for fetching individual column

* feat: add countErrors API
2022-07-13 15:55:43 +05:30
Pranshu Chittora
a09a4c264e feat: change interval of PromQL queries (#1385) 2022-07-13 15:44:28 +05:30
Priyansh Khodiyar
54e09e1292 v3 2022-07-13 13:48:18 +05:30
Priyansh Khodiyar
8477aebc8e V2 2022-07-13 12:27:28 +05:30
Priyansh Khodiyar
d7f7f20520 1st iteration 2022-07-13 02:23:06 +05:30
Pranshu Chittora
4ee92b7c55 fix: query builder update legend on empty values (#1367) 2022-07-11 19:03:16 +05:30
Pranshu Chittora
80c80b2180 feat: missing spans handling by returning a forest of trees (#1365)
* feat: spanToTree 2.0

* feat: spanToTree EPIFI data

* feat: missing spans multiple trees

* chore: migrated to popoverss

Co-authored-by: Palash <palashgdev@gmail.com>
2022-07-08 16:18:08 +05:30
Srikanth Chekuri
da368ab5e8 feat: add support for not regex (#1328)
* feat: add support for not regex

Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2022-07-06 15:49:27 +05:30
Ankit Anand
091d769ad8 Updated Typo (#1362) 2022-07-06 11:45:42 +05:30
Ankit Nayan
be814afeea Merge pull request #1357 from SigNoz/release/v0.9.2
Release/v0.9.2
2022-07-04 22:30:46 +05:30
Prashant Shahi
6697702c0f chore: 📌 pin signoz v0.9.2
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2022-07-04 21:27:11 +05:30
Ankit Nayan
5e93f266c1 Merge branch 'main' into develop 2022-07-04 20:45:49 +05:30
Palash
352c7ac581 Merge pull request #1322 from sf-pchakraborty/506-integrate-reactquery-devtools
chore: integrate reactquery devtools in dev env
2022-07-04 18:40:21 +05:30
Palash
db8f35cca2 Merge branch 'develop' into 506-integrate-reactquery-devtools 2022-07-04 18:28:09 +05:30
Palash
3af1d2b5bb Merge pull request #1335 from palashgdev/513-dashboard
fix: dashboard data is flushed from redux while creating the dashboard
2022-07-04 18:27:31 +05:30
Palash
58038c222f Merge branch 'develop' into 513-dashboard 2022-07-04 18:20:01 +05:30
Palash
b3b5459a08 Merge pull request #1341 from palashgdev/517-ts-commit-lint
chore: commitlint config is updated to ts
2022-07-04 18:19:48 +05:30
Palash
1bfc9877fc Merge branch 'develop' into 517-ts-commit-lint 2022-07-04 17:43:51 +05:30
Pranshu Chittora
8ce806169f docs: add list of project maintainers (#1354)
* docs: add list of project maintainers

Co-authored-by: Palash <palashgdev@gmail.com>
2022-07-04 17:18:23 +05:30
Srikanth Chekuri
3c7e0f66fa chore: bump SigNoz/prometheus to 1.9.73 (#1355) 2022-07-04 17:16:55 +05:30
Ankit Nayan
cbdeb5ad03 chore: added metrics for analytics (#1356) 2022-07-04 17:13:36 +05:30
Priyanka Chakraborty
5bbe1246cc Merge branch '506-integrate-reactquery-devtools' of https://github.com/sf-pchakraborty/signoz into 506-integrate-reactquery-devtools 2022-07-04 14:37:48 +05:30
Priyanka Chakraborty
789d65d7c4 chore: set initialIsOpen as true 2022-07-04 14:37:09 +05:30
Palash
5cbc8af4af Merge branch 'develop' into 506-integrate-reactquery-devtools 2022-07-04 14:31:22 +05:30
Palash
2cffe0c53e Merge branch 'develop' into 513-dashboard 2022-07-04 13:24:01 +05:30
Palash
cf0eb44143 Merge pull request #1339 from pranshuchittora/pranshuchittora/fix/custom-legend-for-empty-metrics
fix: legend for empty metrics names list
2022-07-04 13:23:26 +05:30
Palash
d7ce786f4b Merge branch 'develop' into 517-ts-commit-lint 2022-07-04 13:19:54 +05:30
Palash
ae5d4326a2 Merge branch 'develop' into 506-integrate-reactquery-devtools 2022-07-04 13:10:56 +05:30
Palash
6fa0209104 Merge branch 'develop' into pranshuchittora/fix/custom-legend-for-empty-metrics 2022-07-01 11:38:21 +05:30
Palash
50501ea80f Merge pull request #1348 from SigNoz/issue-1329
fix: add request/response interceptors for ApiV2Instance
2022-07-01 10:39:46 +05:30
Srikanth Chekuri
669dc05eec fix: add request/response interceptors for ApiV2Instance 2022-07-01 00:56:15 +05:30
Palash
e839920b3b Merge branch 'develop' into 517-ts-commit-lint 2022-06-30 21:35:18 +05:30
Ankit Nayan
32b4bbcaec Merge pull request #1347 from SigNoz/release/v0.9.1
Release/v0.9.1
2022-06-30 20:43:16 +05:30
Prashant Shahi
58b0c08d71 chore: 📌 pin SigNoz v0.9.1 and clickhouse in TTY mode
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2022-06-30 19:20:50 +05:30
Palash
dd9cbcee33 fix: decimal-precision is made undefined (#1344)
Co-authored-by: Prashant Shahi <prashant@signoz.io>
2022-06-30 19:02:31 +05:30
Palash
9c3b4508be Merge branch 'develop' into 517-ts-commit-lint 2022-06-30 02:55:33 +05:30
Palash
450407d0bf chore: commitlint config is updated to ts 2022-06-30 02:51:31 +05:30
Palash
276b26b170 Merge branch 'develop' into pranshuchittora/fix/custom-legend-for-empty-metrics 2022-06-30 00:38:08 +05:30
Palash
475723a03a Merge branch 'develop' into 513-dashboard 2022-06-29 22:52:57 +05:30
Palash
897728cc71 Merge pull request #1331 from palashgdev/sidebar-text
chore: css for light mode is updated in sidebar
2022-06-29 22:52:39 +05:30
Palash
bdf78cbf2c Merge branch 'develop' into sidebar-text 2022-06-29 22:44:25 +05:30
Pranshu Chittora
e88cfcd4da fix: legend for empty metrics names list 2022-06-29 16:24:49 +05:30
Ankit Nayan
90566360ae Merge pull request #1336 from SigNoz/release/v0.9.0
Release/v0.9.0
2022-06-29 15:26:36 +05:30
Prashant Shahi
0a6fa0ee85 chore(release): 📌 pin versions: SigNoz 0.9.0, OtelCollector 0.45.1-1.0
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2022-06-29 13:38:56 +05:30
Palash
ba35b3e442 fix: dashboard data is flushed from redux while creating the dashboard from import dashboard json 2022-06-29 12:35:45 +05:30
Palash
73c2137cd7 chore: css for light is updated 2022-06-29 09:14:27 +05:30
Prashant Shahi
dbe68c064c chore(install-script): 🗑️ remove arm YAML as already satisfied by single YAML (#1326)
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2022-06-28 17:33:50 +05:30
Pranshu Chittora
ba7427f280 feat: Metrics Builder Enhancements and Code Cleanup (#1325)
* feat: improved ts typings
* chore: remove uncommented code
2022-06-28 17:32:02 +05:30
Palash
6dbc11991b feat: service map color is updated according to the darkMode (#1324) 2022-06-28 16:20:18 +05:30
Pranshu Chittora
eeae71163c fix: dashboard save layout (#1320)
Co-authored-by: Palash <palashgdev@gmail.com>
2022-06-28 16:19:46 +05:30
Palash
a25e7a64ce fix: light mode issues are fixed (#1319)
Co-authored-by: Ankit Nayan <ankit@signoz.io>
2022-06-28 16:18:53 +05:30
Palash
d0e272b679 fix: text is updated in the light and dark mode (#1318) 2022-06-28 16:18:16 +05:30
Priyanka Chakraborty
16ff59b4de chore: integrate reactquery devtools in dev env 2022-06-27 16:54:58 +05:30
Priyanka Chakraborty
f2074a9d0e chore: integrate reactquery devtools in dev env 2022-06-27 16:33:50 +05:30
Palash
47e6e00a64 test: playwright github action is updated (#1286)
* test: playwright github action is updated

Co-authored-by: Pranshu Chittora <pranshu@signoz.io>
2022-06-24 19:21:23 +05:30
Srikanth Chekuri
282c47def8 chore(deps): bump github.com/SigNoz/prometheus from v1.9.71 to v1.9.72 (#1317) 2022-06-24 15:19:20 +05:30
Pranshu Chittora
9d3fc493a3 feat: Metrics Query Builder (#1166)
* feat: metrics builder metrics name suggestion UX changes
* feat: metrics builder metric name and single value selection
* feat: code cleanup
* feat: improved ts typings

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2022-06-24 15:00:21 +05:30
Ankit Nayan
b2afb9aabc chore: changed scrape interval to 60s and batch size to 10000 (#1316)
* chore: changed scrape interval to 60s and batch size to 10000
* chore: added send_batch_max_size to the batch processor
2022-06-24 14:55:02 +05:30
Srikanth Chekuri
a733adad2c Add v2 query range metrics API (#1020)
* Queryrange params tests

* review suggestions, quantile, simple metric filter and some refactoring

* Add value type support

* Add supprot for re2 regex, refactor, update tests and other changes

* chore: update govaluate dep to signoz/govaluate

* chore: add name to grouping

* chore: add support for NOOP

* fix: make result format compatible with prom HTTP API

* chore: update clickhouse server and update query builder to use new schema

* chore: use metric_name in auto suggest APIs

* chore: add reduce operator and new aggregate functions

* chore: add support for not like op

* chore: fix the dip at the end for incomplete time range

* chore: rounddown the end to exclude the incomplete collection
2022-06-24 14:52:11 +05:30
Palash
cc18cc9087 Merge pull request #1312 from palashgdev/trace-filter-toolip
feat: light mode tooltip is updated
2022-06-24 11:54:55 +05:30
Palash
ecb2ed8ac8 Merge branch 'develop' into trace-filter-toolip 2022-06-24 11:47:12 +05:30
Palash
31931e5a6c Merge pull request #1311 from palashgdev/490-dashboard
feat: dashboard breadcrumb is added
2022-06-24 11:46:58 +05:30
Palash
31848c488d Merge branch 'develop' into trace-filter-toolip 2022-06-24 11:33:05 +05:30
Palash
bdcc997672 Merge branch 'develop' into 490-dashboard 2022-06-24 11:13:10 +05:30
Palash
d68334b2ca Merge pull request #1235 from palashgdev/store-fix-1
fix: using legacy_createStore instead of createStore as it seem it is depecreated
2022-06-24 00:34:54 +05:30
Palash
3ebded66ea Merge branch 'develop' into 490-dashboard 2022-06-24 00:28:55 +05:30
Palash
2ed24df250 Merge branch 'develop' into store-fix-1 2022-06-24 00:27:38 +05:30
Palash
5a34ce2221 Merge pull request #1304 from SigNoz/417-search-filter
feat: search filter is added in the trace filter
2022-06-24 00:23:22 +05:30
Palash
aae6a1adf1 Merge branch 'develop' into 417-search-filter 2022-06-24 00:11:37 +05:30
Palash
bef83d30cc feat: duration filter is updated (#1272)
* feat: duration filter is updated

* feat: search filter is added in the trace filter

Co-authored-by: Palash gupta <palash@signoz.io>
2022-06-23 19:12:43 +05:30
Palash
1ebf3dbf65 feat: select tags key and value are updated to autocomplete filtering (#1267)
* feat: select tags key and value are updated to autocomplete filtering
Co-authored-by: Palash gupta <palash@signoz.io>
2022-06-23 19:11:19 +05:30
Palash
f57808bdb4 Merge branch 'develop' into 417-search-filter 2022-06-23 18:43:21 +05:30
Palash
6bdcd4f5bb Merge branch 'develop' into 490-dashboard 2022-06-23 18:35:10 +05:30
Palash
d726ad9ca6 Merge branch 'develop' into trace-filter-toolip 2022-06-23 18:35:02 +05:30
Palash
4ed3295b80 Merge pull request #1239 from palashgdev/412-trace-detail
FE: Trace Detail text is now ellipsed
2022-06-23 18:34:42 +05:30
Palash
72dc4d62ce Merge branch 'develop' into 412-trace-detail 2022-06-23 18:27:55 +05:30
Palash
186f4dca71 feat: light mode tooltip is updated 2022-06-23 18:07:03 +05:30
Palash
e4f2219f8c feat: dashboard breadcrumb is added 2022-06-23 16:29:38 +05:30
Palash
fe9a6c2448 Merge pull request #1250 from palashgdev/1249-service-tab
feat: metrics is renamed to services in sidebar
2022-06-23 16:08:19 +05:30
Palash
5c2a875211 Merge branch 'develop' into 1249-service-tab 2022-06-23 16:01:25 +05:30
Palash
6dab77409d Merge pull request #1251 from palashgdev/1244-edit-alert
feat: rule id is passed as params
2022-06-23 16:00:37 +05:30
Palash
0f811af34e Merge branch 'develop' into 1244-edit-alert 2022-06-23 15:51:42 +05:30
Palash
bdbcbb5f6c Merge pull request #1264 from zedongh/develop
fix: GetMinMax with 'GLOBAL_TIME' and 'custom' need pass globalTime
2022-06-23 15:44:36 +05:30
Palash
ae91d7e8a9 Merge branch 'develop' into develop 2022-06-23 15:37:37 +05:30
Ankit Nayan
64927acd97 updated codeowners for query-service 2022-06-23 15:33:31 +05:30
Palash
9dae957c8f Merge branch 'develop' into develop 2022-06-23 15:30:07 +05:30
Ankit Nayan
afbcde5edc fix: added 404 for error in getRule api (#1309)
* fix: added multiple error checks in getRule api
2022-06-23 15:29:15 +05:30
Palash
b8c3fd1cbf test: test pipeline for unit test is configured (#1277)
* test: test pipeline is configured
Co-authored-by: Palash gupta <palash@signoz.io>
2022-06-23 15:26:44 +05:30
Palash
93cf5dfa46 Merge branch 'develop' into 1244-edit-alert 2022-06-23 15:06:47 +05:30
Palash
d2c28a47c2 Merge pull request #1268 from palashgdev/450-alerts
feat: alerts breadcrumb is added
2022-06-23 15:06:29 +05:30
Palash
9c68c6af93 Merge branch 'develop' into 412-trace-detail 2022-06-23 13:00:25 +05:30
Palash
3771f85c7d Merge branch 'develop' into 450-alerts 2022-06-23 12:41:40 +05:30
Palash
b39e0465b0 Merge branch 'develop' into 1249-service-tab 2022-06-23 12:38:48 +05:30
Palash
bc97ea8fc0 Merge pull request #1308 from palashgdev/417-search-filter
feat: condition is updated
2022-06-23 12:38:28 +05:30
Palash
1e980c3886 feat: condition is updated 2022-06-23 12:37:42 +05:30
Palash
5ec52f03ad Merge branch 'develop' into 417-search-filter 2022-06-23 12:35:39 +05:30
Palash
4aab923e40 Merge branch 'develop' into develop 2022-06-23 12:29:54 +05:30
Palash
17b0ee5434 Merge pull request #1306 from palashgdev/489-layout
feat: removed auto save layout from dashboard
2022-06-23 12:29:12 +05:30
Palash
08c3c4c51c Merge branch 'develop' into 1249-service-tab 2022-06-23 12:13:16 +05:30
Palash
5f802e0e20 Merge branch 'develop' into 489-layout 2022-06-23 10:55:20 +05:30
Palash
63e663a92d feat: removed auto save layout from dashboard 2022-06-23 10:54:15 +05:30
rw4nn
d21ab7b82d fix(FE): escape regular expression to filter dashboards with special characters (#1279)
* fix(FE): escape reg exp to filter dashboards
* test(FE): add type and use uuid v4
2022-06-23 10:25:55 +05:30
Prashant Shahi
84b876170d chore: clickhouse version bump (#1280)
* chore: clickhouse version bump
2022-06-23 10:15:21 +05:30
Palash
88d8dba90e Merge branch '417-search-filter' of https://github.com/SigNoz/signoz into 417-search-filter 2022-06-23 01:12:21 +05:30
Palash
d7d0d70aa5 chore: search filter is made conditional as filters need to be present 2022-06-23 01:12:12 +05:30
Palash
671b441ec9 Merge branch 'develop' into 417-search-filter 2022-06-23 01:09:59 +05:30
Palash
729c7fce7b chore: initial value is made 8 2022-06-23 01:08:51 +05:30
Palash
224ec8d0d9 feat: search filter is added in the trace filter 2022-06-23 01:07:02 +05:30
Palash
7eed865660 Merge branch 'develop' into develop 2022-06-22 23:47:16 +05:30
Palash
241121ebec chore: serivce name now ellipsed 2022-06-22 23:46:30 +05:30
Palash
15af158a9c Merge branch 'develop' into 412-trace-detail 2022-06-22 22:58:56 +05:30
Palash
2f02aeb031 Merge branch 'develop' into 1249-service-tab 2022-06-22 22:52:17 +05:30
Palash
3603e497a6 chore: error state is updated 2022-06-22 22:49:01 +05:30
Palash
070d32a0ef Merge branch 'develop' into 1244-edit-alert 2022-06-22 22:45:54 +05:30
Palash
0b36da714f Merge pull request #1276 from palashgdev/414-uri
feat: nginx uri issue is handled by increasing buffers
2022-06-21 17:36:41 +05:30
Palash
ce0ac1e3af Merge branch 'develop' into 414-uri 2022-06-21 17:29:55 +05:30
Palash
bcb5256de0 Update CODEOWNERS (#1265) 2022-06-21 17:25:59 +05:30
Palash
fdca72b9b2 chore: nginx config is updated 2022-06-21 16:09:17 +05:30
Palash
7f64dfd023 chore: nginx config is updated 2022-06-21 16:05:18 +05:30
Palash
8871d53ae0 nginx config is updated 2022-06-21 15:12:03 +05:30
Palash
2313ec3f9a Merge branch 'develop' into 1249-service-tab 2022-06-21 14:22:53 +05:30
Palash
56208c9b06 Merge branch 'develop' into store-fix-1 2022-06-19 14:32:05 +05:30
Palash gupta
84e281271c Merge branch '414-uri' of https://github.com/palash-signoz/signoz into 414-uri 2022-06-17 12:31:59 +05:30
Palash gupta
43e4f637d1 fix: remove fastcgi 2022-06-17 12:31:48 +05:30
Palash
c156b9c403 Merge branch 'develop' into 414-uri 2022-06-17 08:08:11 +05:30
Palash gupta
9885572842 feat: nginx uri issue is handled by increasing buffers 2022-06-17 08:07:33 +05:30
Palash
4803fd9c8e Merge branch 'develop' into 450-alerts 2022-06-15 11:45:35 +05:30
Palash
c2fe35388e Merge branch 'develop' into develop 2022-06-15 10:39:39 +05:30
zedongh
ba5e3dcfd3 fix: getMinMax with 'GLOBAL_TIME' and 'custom' need pass globalTimefeat (#1269) 2022-06-15 11:13:46 +08:00
Ankit Nayan
9c8c31d912 Merge branch 'develop' of https://github.com/SigNoz/signoz into develop 2022-06-15 02:45:49 +05:30
Ankit Nayan
469254e9fc Merge pull request #1266 from SigNoz/release/v0.8.2
Release/v0.8.2
2022-06-15 02:45:01 +05:30
Prashant Shahi
1f2ec0d728 chore(release): 📌 pin versions: SigNoz 0.8.2, OtelCollector 0.45.1-0.3
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2022-06-15 01:47:04 +05:30
Palash
ff1fc83b66 Merge branch 'develop' into 450-alerts 2022-06-15 01:35:28 +05:30
Palash gupta
0a5eff2255 feat: alerts breadcrumb is added 2022-06-15 01:34:56 +05:30
Ankit Nayan
24e84bac2a Create codeball.yml 2022-06-14 20:50:13 +05:30
Palash
db00a78a4e Merge branch 'develop' into 1244-edit-alert 2022-06-14 12:24:48 +05:30
Palash
4d2e8b0ea5 Merge branch 'develop' into 412-trace-detail 2022-06-14 12:24:22 +05:30
Srikanth Chekuri
4f12f8c85c fix: incorrect 5xx rate calculation (#1229) 2022-06-14 01:09:44 +05:30
Palash
fabab345cb Merge branch 'develop' into 412-trace-detail 2022-06-13 22:58:02 +05:30
Palash
00355b3383 Merge branch 'develop' into 1244-edit-alert 2022-06-13 19:07:07 +05:30
Palash gupta
c16ae790d4 feat: rule id is passed as params 2022-06-13 19:05:17 +05:30
Palash
c6d57a7a53 Merge branch 'develop' into 1249-service-tab 2022-06-13 18:39:52 +05:30
Palash gupta
d8775c91d7 feat: metrics is renamed to services in sidebar 2022-06-13 18:38:06 +05:30
Ankit Nayan
7b315c6766 Merge pull request #1246 from SigNoz/release/v0.8.1
Release/v0.8.1
2022-06-09 21:17:59 +05:30
Prashant Shahi
676fe892a5 chore(release): 📌 pin versions: OtelCollectors 0.45.1-0.2 and config changes
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2022-06-09 20:58:10 +05:30
Prashant Shahi
15260e0e14 Merge branch 'main' into release/v0.8.1 2022-06-09 17:24:12 +05:30
Prashant Shahi
ce7be6e7cd chore(release): 📌 pin versions: SigNoz 0.8.1
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2022-06-09 17:20:11 +05:30
palash-signoz
99d38860cb Merge pull request #1243 from pranshuchittora/pranshuchittora/feat/dashboard-save-rbac
feat(FE): save dashboard with RBAC permissions
2022-06-09 12:23:18 +05:30
Pranshu Chittora
1f4f281965 feat(FE): save dashbaord with RBAC permissions 2022-06-09 12:04:47 +05:30
palash-signoz
4aa4bf9ea2 Merge pull request #1242 from pranshuchittora/pranshuchittora/feat/dashboard-edit-permission
feat(FE): dashboard edit permission based on RBAC
2022-06-08 23:25:21 +05:30
Pranshu Chittora
052eb25cff chore(FE): sidebar red dot styling 2022-06-08 23:15:48 +05:30
Pranshu Chittora
ce14638a63 feat(FE): dashboard edit permission based on RBAC 2022-06-08 22:57:34 +05:30
palash-signoz
b3dfd567e0 Merge branch 'develop' into 412-trace-detail 2022-06-08 16:25:31 +05:30
Prashant Shahi
fa142707dc chore(alertmanager): 🔧 use query-service internalport (#1241)
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2022-06-08 16:03:48 +05:30
Amol Umbark
5ae4e05c96 HTTP listener for internal services (#1238)
* feat: added private http server to handle internal service requests
* feat: added private port default to constants
2022-06-08 12:22:25 +05:30
palash-signoz
b7d52b8fba fix: dashboard is updated (#1240)
* fix: dashboard is updated

* fix: redux is made empty when creating dashbaord

Co-authored-by: Palash gupta <palash@signoz.io>
2022-06-08 11:50:41 +05:30
palash-signoz
660391c360 Merge branch 'develop' into store-fix-1 2022-06-07 16:47:18 +05:30
palash-signoz
1c90e62189 feat: dashboard layout is updated (#1221)
* feat: dashboard layout is updated

* feat: onClick is made fixed

* feat: layout is updated

* feat: layout is updated

* feat: layout is updated

* fix: memo is removed and grid layout component is refactored to use use query

* fix: saveDashboard is updated

* feat: layout is fixed

* fix: tsc error are fixed

* fix: delete widgets is updated

* fix: useMount once is added

* fix: useMount once is removed

* chore: removed the commented code

Co-authored-by: Ankit Nayan <ankit@signoz.io>
2022-06-07 16:14:49 +05:30
palash-signoz
cfeb631a6e Merge pull request #1217 from palash-signoz/1215-signup
fix: button is disable until condition is met
2022-06-07 16:02:40 +05:30
Palash gupta
8a0bcf6cd9 feat: operation name is now ellipsed 2022-06-07 15:56:16 +05:30
Palash gupta
0c06c5ee0e fix: trace detail is updated 2022-06-06 17:06:16 +05:30
palash-signoz
f3610ffe55 Merge branch 'develop' into store-fix-1 2022-06-06 14:07:27 +05:30
Palash gupta
d150cfa46c fix: using legacy_createStore instead of createStore as it seem it is depecreated 2022-06-06 10:52:09 +05:30
palash-signoz
4fc4ab0611 Merge branch 'develop' into 1215-signup 2022-06-01 18:16:08 +05:30
palash-signoz
b107902c31 Merge pull request #1220 from palash-signoz/1219-video-update
chore: video link is updated
2022-06-01 18:15:52 +05:30
palash-signoz
2d83afd0c4 Merge branch 'develop' into 1215-signup 2022-05-31 11:20:29 +05:30
palash-signoz
e641577e1c Merge branch 'develop' into 1219-video-update 2022-05-31 11:19:49 +05:30
Palash gupta
3e4b56e012 chore: video link is updated 2022-05-31 11:18:52 +05:30
palash-signoz
697fd1d1bf Merge pull request #1209 from palash-signoz/dashboard-layout-fix
fix: layout is updated
2022-05-30 22:31:13 +05:30
palash-signoz
21dbdb57da Merge branch 'develop' into dashboard-layout-fix 2022-05-30 22:21:17 +05:30
palash-signoz
3406bcaa5f Merge branch 'develop' into 1215-signup 2022-05-30 22:06:05 +05:30
Palash gupta
de0fd64a5e fix: button is disable until condition is met 2022-05-30 22:04:45 +05:30
Pranay Prateek
c27c026e25 Update SECURITY.md 2022-05-30 17:22:47 +05:30
Pranay Prateek
0a4bc7e181 Update SECURITY.md 2022-05-30 17:22:20 +05:30
Pranay Prateek
b6cfe9d08e Update SECURITY.md 2022-05-30 17:14:01 +05:30
Pranay Prateek
b5b9f20b1f Update SECURITY.md 2022-05-30 17:13:34 +05:30
Pranay Prateek
25c6106bd6 Create SECURITY.md 2022-05-30 17:04:02 +05:30
Palash gupta
d5877337ec fix: layout is updated 2022-05-27 07:04:22 +05:30
Palash gupta
51e0972219 fix: layout is updated 2022-05-27 07:03:12 +05:30
palash-signoz
38c0bcf4ea fix: trace table is fixed (#1208) 2022-05-26 16:51:18 +05:30
palash-signoz
d863c2781a feat: dashboard layout is updated from widgets (#1207) 2022-05-26 15:09:59 +05:30
Ankit Nayan
be01bc9b82 Revert "fix: frontend/package.json & frontend/yarn.lock to reduce vulnerabilities (#1147)" (#1182)
This reverts commit 5a2ad9492c.
2022-05-20 14:08:12 +02:00
palash-signoz
5a2ad9492c fix: frontend/package.json & frontend/yarn.lock to reduce vulnerabilities (#1147)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-ANSIREGEX-1583908

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2022-05-20 13:38:22 +02:00
302 changed files with 19483 additions and 4357 deletions

3
.github/CODEOWNERS vendored
View File

@@ -2,5 +2,6 @@
# Owners are automatically requested for review for PRs that changes code
# that they own.
* @ankitnayan
/frontend/ @palash-signoz @pranshuchittora
/frontend/ @palashgdev @pranshuchittora
/deploy/ @prashant-shahi
/pkg/query-service/ @srikanthccv @makeavish @nityanandagohain

View File

@@ -17,6 +17,8 @@ jobs:
run: cd frontend && yarn install
- name: Run ESLint
run: cd frontend && npm run lint
- name: Run Jest
run: cd frontend && npm run jest
- name: TSC
run: yarn tsc
working-directory: ./frontend

17
.github/workflows/codeball.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Codeball
on: [pull_request]
jobs:
codeball_job:
runs-on: ubuntu-latest
name: Codeball
steps:
# Run Codeball on all new Pull Requests 🚀
# For customizations and more documentation, see https://github.com/sturdy-dev/codeball-action
- name: Codeball
uses: sturdy-dev/codeball-action@v2
with:
approvePullRequests: "true"
labelPullRequestsWhenApproved: "true"
labelPullRequestsWhenReviewNeeded: "false"
failJobsWhenReviewNeeded: "false"

22
.github/workflows/dependency-review.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
# Dependency Review Action
#
# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
#
# Source repository: https://github.com/actions/dependency-review-action
# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
- name: 'Dependency Review'
with:
fail-on-severity: high
uses: actions/dependency-review-action@v2

View File

@@ -1,22 +1,24 @@
name: Playwright Tests
on:
deployment_status:
on: [pull_request]
jobs:
test:
playwright:
defaults:
run:
working-directory: frontend
timeout-minutes: 60
runs-on: ubuntu-latest
if: github.event.deployment_status.state == 'success'
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: "14.x"
node-version: "16.x"
- name: Install dependencies
run: npm ci
run: CI=1 yarn install
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npm run test:e2e
run: yarn playwright
env:
# This might depend on your test-runner/language binding
PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.target_url }}
PLAYWRIGHT_TEST_BASE_URL: ${{ secrets.PLAYWRIGHT_TEST_BASE_URL }}

View File

@@ -1,122 +1,331 @@
# How to Contribute
# Contributing Guidelines
There are primarily 2 areas in which you can contribute in SigNoz
## Welcome to SigNoz Contributing section 🎉
- Frontend ( written in Typescript, React)
- Backend - ( Query Service - written in Go)
Hi there! We're thrilled that you'd like to contribute to this project, thank you for your interest. Whether it's a bug report, new feature, correction, or additional documentation, we greatly value feedback and contributions from our community.
Depending upon your area of expertise & interest, you can chose one or more to contribute. Below are detailed instructions to contribute in each area
Please read through this document before submitting any issues or pull requests to ensure we have all the necessary information to effectively respond to your bug report or contribution.
> Please note: If you want to work on an issue, please ask the maintainers to assign the issue to you before starting work on it. This would help us understand who is working on an issue and prevent duplicate work. 🙏🏻
- We accept contributions made to the [SigNoz `develop` branch]()
- Find all SigNoz Docker Hub images here
- [signoz/frontend](https://hub.docker.com/r/signoz/frontend)
- [signoz/query-service](https://hub.docker.com/r/signoz/query-service)
- [signoz/otelcontribcol](https://hub.docker.com/r/signoz/otelcontribcol)
> If you just raise a PR, without the corresponding issue being assigned to you - it may not be accepted.
## Finding contributions to work on 💬
# Develop Frontend
Looking at the existing issues is a great way to find something to contribute on.
Also, have a look at these [good first issues label](https://github.com/SigNoz/signoz/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) to start with.
Need to update [https://github.com/SigNoz/signoz/tree/main/frontend](https://github.com/SigNoz/signoz/tree/main/frontend)
### Contribute to Frontend with Docker installation of SigNoz
## Sections:
- [General Instructions](#1-general-instructions-)
- [For Creating Issue(s)](#11-for-creating-issues)
- [For Pull Requests(s)](#12-for-pull-requests)
- [How to Contribute](#2-how-to-contribute-%EF%B8%8F)
- [Develop Frontend](#3-develop-frontend-)
- [Contribute to Frontend with Docker installation of SigNoz](#31-contribute-to-frontend-with-docker-installation-of-signoz)
- [Contribute to Frontend without installing SigNoz backend](#32-contribute-to-frontend-without-installing-signoz-backend)
- [Contribute to Backend (Query-Service)](#4-contribute-to-backend-query-service-)
- [To run ClickHouse setup](#41-to-run-clickhouse-setup-recommended-for-local-development)
- [Contribute to SigNoz Helm Chart](#5-contribute-to-signoz-helm-chart-)
- [To run helm chart for local development](#51-to-run-helm-chart-for-local-development)
- [Other Ways to Contribute](#other-ways-to-contribute)
- `git clone https://github.com/SigNoz/signoz.git && cd signoz`
- comment out frontend service section at `deploy/docker/clickhouse-setup/docker-compose.yaml#L62`
- run `cd deploy` to move to deploy directory
- Install signoz locally without the frontend
- Add below configuration to query-service section at `docker/clickhouse-setup/docker-compose.yaml#L38`
# 1. General Instructions 📝
```docker
## 1.1 For Creating Issue(s)
Before making any significant changes and before filing a new issue, please check [existing open](https://github.com/SigNoz/signoz/issues?q=is%3Aopen+is%3Aissue), or [recently closed](https://github.com/SigNoz/signoz/issues?q=is%3Aissue+is%3Aclosed) issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can.
**Issue Types** - [Bug Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=bug_report.md&title=) | [Feature Request](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=feature_request.md&title=) | [Performance Issue Report](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=performance-issue-report.md&title=) | [Report a Security Vulnerability](https://github.com/SigNoz/signoz/security/policy)
#### Details like these are incredibly useful:
- **Requirement** - what kind of use case are you trying to solve?
- **Proposal** - what do you suggest to solve the problem or improve the existing
situation?
- Any open questions to address❓
#### If you are reporting a bug, details like these are incredibly useful:
- A reproducible test case or series of steps.
- The version of our code being used.
- Any modifications you've made relevant to the bug🐞.
- Anything unusual about your environment or deployment.
Discussing your proposed changes ahead of time will make the contribution
process smooth for everyone 🙌.
**[`^top^`](#)**
<hr>
## 1.2 For Pull Request(s)
Contributions via pull requests are much appreciated. Once the approach is agreed upon ✅, make your changes and open a Pull Request(s).
Before sending us a pull request, please ensure that,
- Fork the SigNoz repo on GitHub, clone it on your machine.
- Create a branch with your changes.
- You are working against the latest source on the `develop` branch.
- Modify the source; please focus only on the specific change you are contributing.
- Ensure local tests pass.
- Commit to your fork using clear commit messages.
- Send us a pull request, answering any default questions in the pull request interface.
- Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation
- Once you've pushed your commits to GitHub, make sure that your branch can be auto-merged (there are no merge conflicts). If not, on your computer, merge main into your branch, resolve any merge conflicts, make sure everything still runs correctly and passes all the tests, and then push up those changes.
- Once the change has been approved and merged, we will inform you in a comment.
GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
[creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
**Note:** Unless your change is small, **please** consider submitting different Pull Rrequest(s):
* 1⃣ First PR should include the overall structure of the new component:
* Readme, configuration, interfaces or base classes, etc...
* This PR is usually trivial to review, so the size limit does not apply to
it.
* 2⃣ Second PR should include the concrete implementation of the component. If the
size of this PR is larger than the recommended size, consider **splitting** ⚔️ it into
multiple PRs.
* If there are multiple sub-component then ideally each one should be implemented as
a **separate** pull request.
* Last PR should include changes to **any user-facing documentation.** And should include
end-to-end tests if applicable. The component must be enabled
only after sufficient testing, and there is enough confidence in the
stability and quality of the component.
You can always reach out to `ankit@signoz.io` to understand more about the repo and product. We are very responsive over email and [SLACK](https://signoz.io/slack).
### Pointers:
- If you find any **bugs** → please create an [**issue.**](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=bug_report.md&title=)
- If you find anything **missing** in documentation → you can create an issue with the label **`documentation`**.
- If you want to build any **new feature** → please create an [issue with the label **`enhancement`**.](https://github.com/SigNoz/signoz/issues/new?assignees=&labels=&template=feature_request.md&title=)
- If you want to **discuss** something about the product, start a new [**discussion**.](https://github.com/SigNoz/signoz/discussions)
<hr>
### Conventions to follow when submitting Commits and Pull Request(s).
We try to follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/), more specifically the commits and PRs **should have type specifiers** prefixed in the name. [This](https://www.conventionalcommits.org/en/v1.0.0/#specification) should give you a better idea.
e.g. If you are submitting a fix for an issue in frontend, the PR name should be prefixed with **`fix(FE):`**
- Follow [GitHub Flow](https://guides.github.com/introduction/flow/) guidelines for your contribution flows.
- Feel free to ping us on [`#contributing`](https://signoz-community.slack.com/archives/C01LWQ8KS7M) or [`#contributing-frontend`](https://signoz-community.slack.com/archives/C027134DM8B) on our slack community if you need any help on this :)
**[`^top^`](#)**
<hr>
# 2. How to Contribute 🙋🏻‍♂️
#### There are primarily 2 areas in which you can contribute to SigNoz
- [**Frontend**](#3-develop-frontend-) (Written in Typescript, React)
- [**Backend**](#4-contribute-to-backend-query-service-) (Query Service, written in Go)
Depending upon your area of expertise & interest, you can choose one or more to contribute. Below are detailed instructions to contribute in each area.
**Please note:** If you want to work on an issue, please ask the maintainers to assign the issue to you before starting work on it. This would help us understand who is working on an issue and prevent duplicate work. 🙏🏻
⚠️ If you just raise a PR, without the corresponding issue being assigned to you - it may not be accepted.
**[`^top^`](#)**
<hr>
# 3. Develop Frontend 🌚
**Need to Update: [https://github.com/SigNoz/signoz/tree/develop/frontend](https://github.com/SigNoz/signoz/tree/develop/frontend)**
Also, have a look at [Frontend README.md](https://github.com/SigNoz/signoz/blob/develop/frontend/README.md) sections for more info on how to setup SigNoz frontend locally (with and without Docker).
## 3.1 Contribute to Frontend with Docker installation of SigNoz
- Clone the SigNoz repository and cd into signoz directory,
```
git clone https://github.com/SigNoz/signoz.git && cd signoz
```
- Comment out `frontend` service section at [`deploy/docker/clickhouse-setup/docker-compose.yaml#L68`](https://github.com/SigNoz/signoz/blob/develop/deploy/docker/clickhouse-setup/docker-compose.yaml#L68)
![develop-frontend](https://user-images.githubusercontent.com/52788043/179009217-6692616b-17dc-4d27-b587-9d007098d739.jpeg)
- run `cd deploy` to move to deploy directory,
- Install signoz locally **without** the frontend,
- Add / Uncomment the below configuration to query-service section at [`deploy/docker/clickhouse-setup/docker-compose.yaml#L47`](https://github.com/SigNoz/signoz/blob/develop/deploy/docker/clickhouse-setup/docker-compose.yaml#L47)
```
ports:
- "8080:8080"
```
- If you are using x86_64 processors (All Intel/AMD processors) run `sudo docker-compose -f docker/clickhouse-setup/docker-compose.yaml up -d`
- If you are on arm64 processors (Apple M1 Macbooks) run `sudo docker-compose -f docker/clickhouse-setup/docker-compose.arm.yaml up -d`
- `cd ../frontend` and change baseURL to `http://localhost:8080` in file `src/constants/env.ts`
- `yarn install`
- `yarn dev`
<img width="869" alt="query service" src="https://user-images.githubusercontent.com/52788043/179010251-8489be31-04ca-42f8-b30d-ef0bb6accb6b.png">
- Next run,
```
sudo docker-compose -f docker/clickhouse-setup/docker-compose.yaml up -d
```
- `cd ../frontend` and change baseURL in file [`frontend/src/constants/env.ts#L2`](https://github.com/SigNoz/signoz/blob/develop/frontend/src/constants/env.ts#L2) and for that, you need to create a `.env` file in the `frontend` directory with the following environment variable (`FRONTEND_API_ENDPOINT`) matching your configuration.
> Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh`
If you have backend api exposed via frontend nginx:
```
FRONTEND_API_ENDPOINT=http://localhost:3301
```
If not:
```
FRONTEND_API_ENDPOINT=http://localhost:8080
```
### Contribute to Frontend without installing SigNoz backend
- Next,
```
yarn install
yarn dev
```
If you don't want to install SigNoz backend just for doing frontend development, we can provide you with test environments which you can use as the backend. Please ping us in #contributing channel in our [slack community](https://signoz.io/slack) and we will DM you with `<test environment URL>`
### Important Notes:
The Maintainers / Contributors who will change Line Numbers of `Frontend` & `Query-Section`, please update line numbers in [`/.scripts/commentLinesForSetup.sh`](https://github.com/SigNoz/signoz/blob/develop/.scripts/commentLinesForSetup.sh)
- `git clone https://github.com/SigNoz/signoz.git && cd signoz/frontend`
- Create a file `.env` with `FRONTEND_API_ENDPOINT=<test environment URL>`
- `yarn install`
- `yarn dev`
**[`^top^`](#)**
**_Frontend should now be accessible at `http://localhost:3301/application`_**
## 3.2 Contribute to Frontend without installing SigNoz backend
# Contribute to Query-Service
If you don't want to install the SigNoz backend just for doing frontend development, we can provide you with test environments that you can use as the backend.
Need to update [https://github.com/SigNoz/signoz/tree/main/pkg/query-service](https://github.com/SigNoz/signoz/tree/main/pkg/query-service)
- Clone the SigNoz repository and cd into signoz/frontend directory,
```
git clone https://github.com/SigNoz/signoz.git && cd signoz/frontend
````
- Create a file `.env` in the `frontend` directory with `FRONTEND_API_ENDPOINT=<test environment URL>`
- Next,
```
yarn install
yarn dev
```
### To run ClickHouse setup (recommended for local development)
Please ping us in the [`#contributing`](https://signoz-community.slack.com/archives/C01LWQ8KS7M) channel or ask `@Prashant Shahi` in our [Slack Community](https://signoz.io/slack) and we will DM you with `<test environment URL>`.
- git clone https://github.com/SigNoz/signoz.git
- run `cd signoz` to move to signoz directory
- run `sudo make dev-setup` to configure local setup to run query-service
- comment out frontend service section at `docker/clickhouse-setup/docker-compose.yaml`
- comment out query-service section at `docker/clickhouse-setup/docker-compose.yaml`
- add below configuration to clickhouse section at `docker/clickhouse-setup/docker-compose.yaml`
```docker
expose:
- 9000
ports:
- 9001:9000
**Frontend should now be accessible at** [`http://localhost:3301/application`](http://localhost:3301/application)
**[`^top^`](#)**
<hr>
# 4. Contribute to Backend (Query-Service) 🌑
**Need to Update:** [**https://github.com/SigNoz/signoz/tree/develop/pkg/query-service**](https://github.com/SigNoz/signoz/tree/develop/pkg/query-service)
## 4.1 To run ClickHouse setup (recommended for local development)
- Clone the SigNoz repository and cd into signoz directory,
```
git clone https://github.com/SigNoz/signoz.git && cd signoz
```
- run `sudo make dev-setup` to configure local setup to run query-service,
- Comment out `frontend` service section at [`deploy/docker/clickhouse-setup/docker-compose.yaml#L68`](https://github.com/SigNoz/signoz/blob/develop/deploy/docker/clickhouse-setup/docker-compose.yaml#L68)
<img width="982" alt="develop-frontend" src="https://user-images.githubusercontent.com/52788043/179043977-012be8b0-a2ed-40d1-b2e6-2ab72d7989c0.png">
- Comment out `query-service` section at [`deploy/docker/clickhouse-setup/docker-compose.yaml#L41`,](https://github.com/SigNoz/signoz/blob/develop/deploy/docker/clickhouse-setup/docker-compose.yaml#L41)
<img width="1068" alt="Screenshot 2022-07-14 at 22 48 07" src="https://user-images.githubusercontent.com/52788043/179044151-a65ba571-db0b-4a16-b64b-ca3fadcf3af0.png">
- add below configuration to `clickhouse` section at [`deploy/docker/clickhouse-setup/docker-compose.yaml`,](https://github.com/SigNoz/signoz/blob/develop/deploy/docker/clickhouse-setup/docker-compose.yaml)
```
ports:
- 9001:9000
```
<img width="1013" alt="Screenshot 2022-07-14 at 22 50 37" src="https://user-images.githubusercontent.com/52788043/179044544-a293d3bc-4c4f-49ea-a276-505a381de67d.png">
- run `cd pkg/query-service/` to move to `query-service` directory,
- Then, you need to create a `.env` file with the following environment variable
```
SIGNOZ_LOCAL_DB_PATH="./signoz.db"
```
to set your local environment with the right `RELATIONAL_DATASOURCE_PATH` as mentioned in [`./constants/constants.go#L38`,](https://github.com/SigNoz/signoz/blob/develop/pkg/query-service/constants/constants.go#L38)
- Now, install SigNoz locally **without** the `frontend` and `query-service`,
- If you are using `x86_64` processors (All Intel/AMD processors) run `sudo make run-x86`
- If you are on `arm64` processors (Apple M1 Macs) run `sudo make run-arm`
#### Run locally,
```
- run `cd pkg/query-service/` to move to query-service directory
- Open ./constants/constants.go
- Replace ```const RELATIONAL_DATASOURCE_PATH = "/var/lib/signoz/signoz.db"``` \
with ```const RELATIONAL_DATASOURCE_PATH = "./signoz.db".```
- Install signoz locally without the frontend and query-service
- If you are using x86_64 processors (All Intel/AMD processors) run `sudo make run-x86`
- If you are on arm64 processors (Apple M1 Macbooks) run `sudo make run-arm`
#### Run locally
```console
ClickHouseUrl=tcp://localhost:9001 STORAGE=clickhouse go run main.go
```
> Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh`
#### Build and Run locally
```
cd pkg/query-service
go build -o build/query-service main.go
ClickHouseUrl=tcp://localhost:9001 STORAGE=clickhouse build/query-service
```
**_Query Service should now be available at `http://localhost:8080`_**
#### Docker Images
The docker images of query-service is available at https://hub.docker.com/r/signoz/query-service
> If you want to see how, frontend plays with query service, you can run frontend also in you local env with the baseURL changed to `http://localhost:8080` in file `src/constants/env.ts` as the query-service is now running at port `8080`
```
docker pull signoz/query-service
```
```
docker pull signoz/query-service:latest
```
```
docker pull signoz/query-service:develop
```
### Important Note:
The Maintainers / Contributors who will change Line Numbers of `Frontend` & `Query-Section`, please update line numbers in [`/.scripts/commentLinesForSetup.sh`](https://github.com/SigNoz/signoz/blob/develop/.scripts/commentLinesForSetup.sh)
**Query Service should now be available at** [`http://localhost:8080`](http://localhost:8080)
If you want to see how the frontend plays with query service, you can run the frontend also in your local env with the baseURL changed to `http://localhost:8080` in file [`frontend/src/constants/env.ts`](https://github.com/SigNoz/signoz/blob/develop/frontend/src/constants/env.ts) as the `query-service` is now running at port `8080`.
---
<!-- Instead of configuring a local setup, you can also use [Gitpod](https://www.gitpod.io/), a VSCode-based Web IDE.
Click the button below. A workspace with all required environments will be created.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/SigNoz/signoz)
> To use it on your forked repo, edit the 'Open in Gitpod' button url to `https://gitpod.io/#https://github.com/<your-github-username>/signoz` -->
> To use it on your forked repo, edit the 'Open in Gitpod' button URL to `https://gitpod.io/#https://github.com/<your-github-username>/signoz` -->
# Contribute to SigNoz Helm Chart
**[`^top^`](#)**
<hr>
Need to update [https://github.com/SigNoz/charts](https://github.com/SigNoz/charts).
# 5. Contribute to SigNoz Helm Chart 📊
### To run helm chart for local development
**Need to Update: [https://github.com/SigNoz/charts](https://github.com/SigNoz/charts).**
- run `git clone https://github.com/SigNoz/charts.git` followed by `cd charts`
- it is recommended to use lightweight kubernetes (k8s) cluster for local development:
## 5.1 To run helm chart for local development
- Clone the SigNoz repository and cd into charts directory,
```
git clone https://github.com/SigNoz/charts.git && cd charts
```
- It is recommended to use lightweight kubernetes (k8s) cluster for local development:
- [kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation)
- [k3d](https://k3d.io/#installation)
- [minikube](https://minikube.sigs.k8s.io/docs/start/)
- create a k8s cluster and make sure `kubectl` points to the locally created k8s cluster
- run `make dev-install` to install SigNoz chart with `my-release` release name in `platform` namespace.
- run `kubectl -n platform port-forward svc/my-release-signoz-frontend 3301:3301` to make SigNoz UI available at [localhost:3301](http://localhost:3301)
- create a k8s cluster and make sure `kubectl` points to the locally created k8s cluster,
- run `make dev-install` to install SigNoz chart with `my-release` release name in `platform` namespace,
- next run,
```
kubectl -n platform port-forward svc/my-release-signoz-frontend 3301:3301
```
to make SigNoz UI available at [localhost:3301](http://localhost:3301)
**To install HotROD sample app:**
**5.1.1 To install the HotROD sample app:**
```bash
curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-install.sh \
| HELM_RELEASE=my-release SIGNOZ_NAMESPACE=platform bash
```
**To load data with HotROD sample app:**
**5.1.2 To load data with the HotROD sample app:**
```bash
kubectl -n sample-application run strzal --image=djbingham/curl \
@@ -124,7 +333,7 @@ kubectl -n sample-application run strzal --image=djbingham/curl \
'locust_count=6' -F 'hatch_rate=2' http://locust-master:8089/swarm
```
**To stop the load generation:**
**5.1.3 To stop the load generation:**
```bash
kubectl -n sample-application run strzal --image=djbingham/curl \
@@ -132,59 +341,32 @@ kubectl -n sample-application run strzal --image=djbingham/curl \
http://locust-master:8089/stop
```
**To delete HotROD sample app:**
**5.1.4 To delete the HotROD sample app:**
```bash
curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-delete.sh \
| HOTROD_NAMESPACE=sample-application bash
```
**[`^top^`](#)**
---
## General Instructions
## Other Ways to Contribute
**Before making any significant changes, please open an issue**. Each issue
should describe the following:
There are many other ways to get involved with the community and to participate in this project:
* Requirement - what kind of use case are you trying to solve?
* Proposal - what do you suggest to solve the problem or improve the existing
situation?
* Any open questions to address
Discussing your proposed changes ahead of time will make the contribution
process smooth for everyone. Once the approach is agreed upon, make your changes
and open a pull request(s). Unless your change is small, Please consider submitting different PRs:
* First PR should include the overall structure of the new component:
* Readme, configuration, interfaces or base classes etc...
* This PR is usually trivial to review, so the size limit does not apply to
it.
* Second PR should include the concrete implementation of the component. If the
size of this PR is larger than the recommended size consider splitting it in
multiple PRs.
* If there are multiple sub-component then ideally each one should be implemented as
a separate pull request.
* Last PR should include changes to any user facing documentation. And should include
end to end tests if applicable. The component must be enabled
only after sufficient testing, and there is enough confidence in the
stability and quality of the component.
- Use the product, submitting GitHub issues when a problem is found.
- Help code review pull requests and participate in issue threads.
- Submit a new feature request as an issue.
- Help answer questions on forums such as Stack Overflow and [SigNoz Community Slack Channel](https://signoz.io/slack).
- Tell others about the project on Twitter, your blog, etc.
You can always reach out to `ankit@signoz.io` to understand more about the repo and product. We are very responsive over email and [slack](https://signoz.io/slack).
## License
- If you find any bugs, please create an issue
- If you find anything missing in documentation, you can create an issue with label **documentation**
- If you want to build any new feature, please create an issue with label `enhancement`
- If you want to discuss something about the product, start a new [discussion](https://github.com/SigNoz/signoz/discussions)
By contributing to SigNoz, you agree that your contributions will be licensed under its MIT license.
### Conventions to follow when submitting commits, PRs
Again, Feel free to ping us on [`#contributing`](https://signoz-community.slack.com/archives/C01LWQ8KS7M) or [`#contributing-frontend`](https://signoz-community.slack.com/archives/C027134DM8B) on our slack community if you need any help on this :)
1. We try to follow https://www.conventionalcommits.org/en/v1.0.0/
More specifically the commits and PRs should have type specifiers prefixed in the name. [This](https://www.conventionalcommits.org/en/v1.0.0/#specification) should give you a better idea.
e.g. If you are submitting a fix for an issue in frontend - PR name should be prefixed with `fix(FE):`
2. Follow [GitHub Flow](https://guides.github.com/introduction/flow/) guidelines for your contribution flows
3. Feel free to ping us on `#contributing` or `#contributing-frontend` on our slack community if you need any help on this :)
Thank You!

View File

@@ -82,15 +82,9 @@ dev-setup:
run-x86:
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml up -d
run-arm:
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.arm.yaml up -d
down-x86:
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml down -v
down-arm:
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.arm.yaml down -v
clear-standalone-data:
@docker run --rm -v "$(PWD)/$(STANDALONE_DIRECTORY)/data:/pwd" busybox \
sh -c "cd /pwd && rm -rf alertmanager/* clickhouse/* signoz/*"

View File

@@ -32,7 +32,6 @@ SigNoz helps developers monitor applications and troubleshoot problems in their
👉 Run aggregates on trace data to get business relevant metrics
![screenzy-1644432902955](https://user-images.githubusercontent.com/504541/153270713-1b2156e6-ec03-42de-975b-3c02b8ec1836.png)
<br />
![screenzy-1644432986784](https://user-images.githubusercontent.com/504541/153270725-0efb73b3-06ed-4207-bf13-9b7e2e17c4b8.png)
@@ -88,8 +87,7 @@ You can find the complete list of languages here - https://opentelemetry.io/docs
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/Philosophy.svg" width="50px" />
## Getting Started
### Deploy using Docker
Please follow the steps listed [here](https://signoz.io/docs/deployment/docker/) to install using docker
@@ -102,7 +100,6 @@ The [troubleshooting instructions](https://signoz.io/docs/deployment/troubleshoo
### Deploy in Kubernetes using Helm
Please follow the steps listed [here](https://signoz.io/docs/deployment/helm_chart) to install using helm charts
<br /><br />
@@ -112,7 +109,7 @@ Please follow the steps listed [here](https://signoz.io/docs/deployment/helm_cha
### SigNoz vs Prometheus
Prometheus is good if you want to do just metrics. But if you want to have a seamless experience between metrics and traces, then current experience of stitching together Prometheus & Jaeger is not great.
Prometheus is good if you want to do just metrics. But if you want to have a seamless experience between metrics and traces, then current experience of stitching together Prometheus & Jaeger is not great.
Our goal is to provide an integrated UI between metrics & traces - similar to what SaaS vendors like Datadog provides - and give advanced filtering and aggregation over traces, something which Jaeger currently lack.
@@ -133,11 +130,28 @@ Moreover, SigNoz has few more advanced features wrt Jaeger:
## Contributing
We ❤️ contributions big or small. Please read [CONTRIBUTING.md](CONTRIBUTING.md) to get started with making contributions to SigNoz.
We ❤️ contributions big or small. Please read [CONTRIBUTING.md](CONTRIBUTING.md) to get started with making contributions to SigNoz.
Not sure how to get started? Just ping us on `#contributing` in our [slack community](https://signoz.io/slack)
### Project maintainers
#### Backend
- [Ankit Nayan](https://github.com/ankitnayan)
- [Nityananda Gohain](https://github.com/nityanandagohain)
- [Srikanth Chekuri](https://github.com/srikanthccv)
- [Vishal Sharma](https://github.com/makeavish)
#### Frontend
- [Palash Gupta](https://github.com/palashgdev)
- [Pranshu Chittora](https://github.com/pranshuchittora)
#### DevOps
- [Prashant Shahi](https://github.com/prashant-shahi)
<br /><br />
<img align="left" src="https://signoz-public.s3.us-east-2.amazonaws.com/DevelopingLocally.svg" width="50px" />
@@ -156,11 +170,8 @@ Join the [slack community](https://signoz.io/slack) to know more about distribut
If you have any ideas, questions, or any feedback, please share on our [Github Discussions](https://github.com/SigNoz/signoz/discussions)
As always, thanks to our amazing contributors!
As always, thanks to our amazing contributors!
<a href="https://github.com/signoz/signoz/graphs/contributors">
<img src="https://contrib.rocks/image?repo=signoz/signoz" />
</a>

18
SECURITY.md Normal file
View File

@@ -0,0 +1,18 @@
# Security Policy
SigNoz is looking forward to working with security researchers across the world to keep SigNoz and our users safe. If you have found an issue in our systems/applications, please reach out to us.
## Supported Versions
We always recommend using the latest version of SigNoz to ensure you get all security updates
## Reporting a Vulnerability
If you believe you have found a security vulnerability within SigNoz, please let us know right away. We'll try and fix the problem as soon as possible.
**Do not report vulnerabilities using public GitHub issues**. Instead, email <security@signoz.io> with a detailed account of the issue. Please submit one issue per email, this helps us triage vulnerabilities.
Once we've received your email we'll keep you updated as we fix the vulnerability.
## Thanks
Thank you for keeping SigNoz and our users safe. 🙇

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
<?xml version="1.0"?>
<clickhouse>
<storage_configuration>
<disks>
<default>
<keep_free_space_bytes>10485760</keep_free_space_bytes>
</default>
<s3>
<type>s3</type>
<endpoint>https://BUCKET-NAME.s3.amazonaws.com/data/</endpoint>
<access_key_id>ACCESS-KEY-ID</access_key_id>
<secret_access_key>SECRET-ACCESS-KEY</secret_access_key>
</s3>
</disks>
<policies>
<tiered>
<volumes>
<default>
<disk>default</disk>
</default>
<s3>
<disk>s3</disk>
</s3>
</volumes>
</tiered>
</policies>
</storage_configuration>
</clickhouse>

View File

@@ -0,0 +1,123 @@
<?xml version="1.0"?>
<clickhouse>
<!-- See also the files in users.d directory where the settings can be overridden. -->
<!-- Profiles of settings. -->
<profiles>
<!-- Default settings. -->
<default>
<!-- Maximum memory usage for processing single query, in bytes. -->
<max_memory_usage>10000000000</max_memory_usage>
<!-- How to choose between replicas during distributed query processing.
random - choose random replica from set of replicas with minimum number of errors
nearest_hostname - from set of replicas with minimum number of errors, choose replica
with minimum number of different symbols between replica's hostname and local hostname
(Hamming distance).
in_order - first live replica is chosen in specified order.
first_or_random - if first replica one has higher number of errors, pick a random one from replicas with minimum number of errors.
-->
<load_balancing>random</load_balancing>
</default>
<!-- Profile that allows only read queries. -->
<readonly>
<readonly>1</readonly>
</readonly>
</profiles>
<!-- Users and ACL. -->
<users>
<!-- If user name was not specified, 'default' user is used. -->
<default>
<!-- See also the files in users.d directory where the password can be overridden.
Password could be specified in plaintext or in SHA256 (in hex format).
If you want to specify password in plaintext (not recommended), place it in 'password' element.
Example: <password>qwerty</password>.
Password could be empty.
If you want to specify SHA256, place it in 'password_sha256_hex' element.
Example: <password_sha256_hex>65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5</password_sha256_hex>
Restrictions of SHA256: impossibility to connect to ClickHouse using MySQL JS client (as of July 2019).
If you want to specify double SHA1, place it in 'password_double_sha1_hex' element.
Example: <password_double_sha1_hex>e395796d6546b1b65db9d665cd43f0e858dd4303</password_double_sha1_hex>
If you want to specify a previously defined LDAP server (see 'ldap_servers' in the main config) for authentication,
place its name in 'server' element inside 'ldap' element.
Example: <ldap><server>my_ldap_server</server></ldap>
If you want to authenticate the user via Kerberos (assuming Kerberos is enabled, see 'kerberos' in the main config),
place 'kerberos' element instead of 'password' (and similar) elements.
The name part of the canonical principal name of the initiator must match the user name for authentication to succeed.
You can also place 'realm' element inside 'kerberos' element to further restrict authentication to only those requests
whose initiator's realm matches it.
Example: <kerberos />
Example: <kerberos><realm>EXAMPLE.COM</realm></kerberos>
How to generate decent password:
Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha256sum | tr -d '-'
In first line will be password and in second - corresponding SHA256.
How to generate double SHA1:
Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha1sum | tr -d '-' | xxd -r -p | sha1sum | tr -d '-'
In first line will be password and in second - corresponding double SHA1.
-->
<password></password>
<!-- List of networks with open access.
To open access from everywhere, specify:
<ip>::/0</ip>
To open access only from localhost, specify:
<ip>::1</ip>
<ip>127.0.0.1</ip>
Each element of list has one of the following forms:
<ip> IP-address or network mask. Examples: 213.180.204.3 or 10.0.0.1/8 or 10.0.0.1/255.255.255.0
2a02:6b8::3 or 2a02:6b8::3/64 or 2a02:6b8::3/ffff:ffff:ffff:ffff::.
<host> Hostname. Example: server01.clickhouse.com.
To check access, DNS query is performed, and all received addresses compared to peer address.
<host_regexp> Regular expression for host names. Example, ^server\d\d-\d\d-\d\.clickhouse\.com$
To check access, DNS PTR query is performed for peer address and then regexp is applied.
Then, for result of PTR query, another DNS query is performed and all received addresses compared to peer address.
Strongly recommended that regexp is ends with $
All results of DNS requests are cached till server restart.
-->
<networks>
<ip>::/0</ip>
</networks>
<!-- Settings profile for user. -->
<profile>default</profile>
<!-- Quota for user. -->
<quota>default</quota>
<!-- User can create other users and grant rights to them. -->
<!-- <access_management>1</access_management> -->
</default>
</users>
<!-- Quotas. -->
<quotas>
<!-- Name of quota. -->
<default>
<!-- Limits for time interval. You could specify many intervals with different limits. -->
<interval>
<!-- Length of interval. -->
<duration>3600</duration>
<!-- No limits. Just calculate resource usage for time interval. -->
<queries>0</queries>
<errors>0</errors>
<result_rows>0</result_rows>
<read_rows>0</read_rows>
<execution_time>0</execution_time>
</interval>
</default>
</quotas>
</clickhouse>

View File

@@ -2,12 +2,15 @@ version: "3.9"
services:
clickhouse:
image: yandex/clickhouse-server:21.12.3.32
image: clickhouse/clickhouse-server:22.4.5-alpine
# ports:
# - "9000:9000"
# - "8123:8123"
tty: true
volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
- ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
# - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
- ./data/clickhouse/:/var/lib/clickhouse/
deploy:
restart_policy:
@@ -28,7 +31,7 @@ services:
volumes:
- ./data/alertmanager:/data
command:
- --queryService.url=http://query-service:8080
- --queryService.url=http://query-service:8085
- --storage.path=/data
depends_on:
- query-service
@@ -37,7 +40,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.8.0
image: signoz/query-service:0.10.0
command: ["-config=/root/config/prometheus.yml"]
# ports:
# - "6060:6060" # pprof port
@@ -65,7 +68,7 @@ services:
- clickhouse
frontend:
image: signoz/frontend:0.8.0
image: signoz/frontend:0.10.0
deploy:
restart_policy:
condition: on-failure
@@ -78,20 +81,24 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/otelcontribcol:0.43.0-0.1
image: signoz/otelcontribcol:0.45.1-1.1
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
# - "1777:1777" # pprof extension
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
# - "8889:8889" # Prometheus metrics exposed by the agent
# - "13133:13133" # health_check
# - "14268:14268" # Jaeger receiver
# - "8888:8888" # OtelCollector internal metrics
# - "8889:8889" # signoz spanmetrics exposed by the agent
# - "9411:9411" # Zipkin port
# - "13133:13133" # Health check extension
# - "14250:14250" # Jaeger gRPC
# - "14268:14268" # Jaeger thrift HTTP
# - "55678:55678" # OpenCensus receiver
# - "55679:55679" # zpages extension
# - "55680:55680" # OTLP gRPC legacy receiver
# - "55681:55681" # OTLP HTTP legacy receiver
# - "55679:55679" # zPages extension
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
deploy:
mode: replicated
replicas: 3
@@ -104,10 +111,15 @@ services:
- clickhouse
otel-collector-metrics:
image: signoz/otelcontribcol:0.43.0-0.1
image: signoz/otelcontribcol:0.45.1-1.1
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
# ports:
# - "1777:1777" # pprof extension
# - "8888:8888" # OtelCollector internal metrics
# - "13133:13133" # Health check extension
# - "55679:55679" # zPages extension
deploy:
restart_policy:
condition: on-failure

View File

@@ -1,29 +1,46 @@
receivers:
opencensus:
endpoint: 0.0.0.0:55678
otlp/spanmetrics:
protocols:
grpc:
endpoint: "localhost:12345"
endpoint: localhost:12345
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
jaeger:
protocols:
grpc:
endpoint: 0.0.0.0:14250
thrift_http:
endpoint: 0.0.0.0:14268
# thrift_compact:
# endpoint: 0.0.0.0:6831
# thrift_binary:
# endpoint: 0.0.0.0:6832
hostmetrics:
collection_interval: 30s
collection_interval: 60s
scrapers:
cpu:
load:
memory:
disk:
filesystem:
network:
cpu: {}
load: {}
memory: {}
disk: {}
filesystem: {}
network: {}
processors:
batch:
send_batch_size: 1000
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
resourcedetection:
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
detectors: [env, system] # include ec2 for AWS, gce for GCP and azure for Azure.
timeout: 2s
override: false
signozspanmetrics/prometheus:
metrics_exporter: prometheus
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
@@ -48,29 +65,44 @@ processors:
# num_workers: 4
# queue_size: 100
# retry_on_failure: true
extensions:
health_check: {}
zpages: {}
exporters:
clickhouse:
clickhousetraces:
datasource: tcp://clickhouse:9000/?database=signoz_traces
clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
resource_to_telemetry_conversion:
enabled: true
prometheus:
endpoint: "0.0.0.0:8889"
endpoint: 0.0.0.0:8889
# logging: {}
extensions:
health_check:
endpoint: 0.0.0.0:13133
zpages:
endpoint: 0.0.0.0:55679
pprof:
endpoint: 0.0.0.0:1777
service:
extensions: [health_check, zpages]
telemetry:
metrics:
address: 0.0.0.0:8888
extensions: [health_check, zpages, pprof]
pipelines:
traces:
receivers: [jaeger, otlp]
processors: [signozspanmetrics/prometheus, batch]
exporters: [clickhouse]
exporters: [clickhousetraces]
metrics:
receivers: [otlp, hostmetrics]
receivers: [otlp]
processors: [batch]
exporters: [clickhousemetricswrite]
metrics/hostmetrics:
receivers: [hostmetrics]
processors: [resourcedetection, batch]
exporters: [clickhousemetricswrite]
metrics/spanmetrics:
receivers: [otlp/spanmetrics]
exporters: [prometheus]
exporters: [prometheus]

View File

@@ -1,20 +1,30 @@
receivers:
otlp:
protocols:
grpc:
http:
# Data sources: metrics
prometheus:
config:
scrape_configs:
# otel-collector internal metrics
- job_name: "otel-collector"
scrape_interval: 30s
scrape_interval: 60s
static_configs:
- targets: ["otel-collector:8889"]
- targets:
- otel-collector:8888
# otel-collector-metrics internal metrics
- job_name: "otel-collector-metrics"
scrape_interval: 60s
static_configs:
- targets:
- localhost:8888
# SigNoz span metrics
- job_name: "signozspanmetrics-collector"
scrape_interval: 60s
static_configs:
- targets:
- otel-collector:8889
processors:
batch:
send_batch_size: 1000
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
# memory_limiter:
# # 80% of maximum memory up to 2G
@@ -31,17 +41,26 @@ processors:
# num_workers: 4
# queue_size: 100
# retry_on_failure: true
extensions:
health_check: {}
zpages: {}
exporters:
clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
extensions:
health_check:
endpoint: 0.0.0.0:13133
zpages:
endpoint: 0.0.0.0:55679
pprof:
endpoint: 0.0.0.0:1777
service:
extensions: [health_check, zpages]
telemetry:
metrics:
address: 0.0.0.0:8888
extensions: [health_check, zpages, pprof]
pipelines:
metrics:
receivers: [otlp, prometheus]
receivers: [prometheus]
processors: [batch]
exporters: [clickhousemetricswrite]
exporters: [clickhousemetricswrite]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
<?xml version="1.0"?>
<clickhouse>
<storage_configuration>
<disks>
<default>
<keep_free_space_bytes>10485760</keep_free_space_bytes>
</default>
<s3>
<type>s3</type>
<endpoint>https://BUCKET-NAME.s3.amazonaws.com/data/</endpoint>
<access_key_id>ACCESS-KEY-ID</access_key_id>
<secret_access_key>SECRET-ACCESS-KEY</secret_access_key>
</s3>
</disks>
<policies>
<tiered>
<volumes>
<default>
<disk>default</disk>
</default>
<s3>
<disk>s3</disk>
</s3>
</volumes>
</tiered>
</policies>
</storage_configuration>
</clickhouse>

View File

@@ -0,0 +1,123 @@
<?xml version="1.0"?>
<clickhouse>
<!-- See also the files in users.d directory where the settings can be overridden. -->
<!-- Profiles of settings. -->
<profiles>
<!-- Default settings. -->
<default>
<!-- Maximum memory usage for processing single query, in bytes. -->
<max_memory_usage>10000000000</max_memory_usage>
<!-- How to choose between replicas during distributed query processing.
random - choose random replica from set of replicas with minimum number of errors
nearest_hostname - from set of replicas with minimum number of errors, choose replica
with minimum number of different symbols between replica's hostname and local hostname
(Hamming distance).
in_order - first live replica is chosen in specified order.
first_or_random - if first replica one has higher number of errors, pick a random one from replicas with minimum number of errors.
-->
<load_balancing>random</load_balancing>
</default>
<!-- Profile that allows only read queries. -->
<readonly>
<readonly>1</readonly>
</readonly>
</profiles>
<!-- Users and ACL. -->
<users>
<!-- If user name was not specified, 'default' user is used. -->
<default>
<!-- See also the files in users.d directory where the password can be overridden.
Password could be specified in plaintext or in SHA256 (in hex format).
If you want to specify password in plaintext (not recommended), place it in 'password' element.
Example: <password>qwerty</password>.
Password could be empty.
If you want to specify SHA256, place it in 'password_sha256_hex' element.
Example: <password_sha256_hex>65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5</password_sha256_hex>
Restrictions of SHA256: impossibility to connect to ClickHouse using MySQL JS client (as of July 2019).
If you want to specify double SHA1, place it in 'password_double_sha1_hex' element.
Example: <password_double_sha1_hex>e395796d6546b1b65db9d665cd43f0e858dd4303</password_double_sha1_hex>
If you want to specify a previously defined LDAP server (see 'ldap_servers' in the main config) for authentication,
place its name in 'server' element inside 'ldap' element.
Example: <ldap><server>my_ldap_server</server></ldap>
If you want to authenticate the user via Kerberos (assuming Kerberos is enabled, see 'kerberos' in the main config),
place 'kerberos' element instead of 'password' (and similar) elements.
The name part of the canonical principal name of the initiator must match the user name for authentication to succeed.
You can also place 'realm' element inside 'kerberos' element to further restrict authentication to only those requests
whose initiator's realm matches it.
Example: <kerberos />
Example: <kerberos><realm>EXAMPLE.COM</realm></kerberos>
How to generate decent password:
Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha256sum | tr -d '-'
In first line will be password and in second - corresponding SHA256.
How to generate double SHA1:
Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha1sum | tr -d '-' | xxd -r -p | sha1sum | tr -d '-'
In first line will be password and in second - corresponding double SHA1.
-->
<password></password>
<!-- List of networks with open access.
To open access from everywhere, specify:
<ip>::/0</ip>
To open access only from localhost, specify:
<ip>::1</ip>
<ip>127.0.0.1</ip>
Each element of list has one of the following forms:
<ip> IP-address or network mask. Examples: 213.180.204.3 or 10.0.0.1/8 or 10.0.0.1/255.255.255.0
2a02:6b8::3 or 2a02:6b8::3/64 or 2a02:6b8::3/ffff:ffff:ffff:ffff::.
<host> Hostname. Example: server01.clickhouse.com.
To check access, DNS query is performed, and all received addresses compared to peer address.
<host_regexp> Regular expression for host names. Example, ^server\d\d-\d\d-\d\.clickhouse\.com$
To check access, DNS PTR query is performed for peer address and then regexp is applied.
Then, for result of PTR query, another DNS query is performed and all received addresses compared to peer address.
Strongly recommended that regexp is ends with $
All results of DNS requests are cached till server restart.
-->
<networks>
<ip>::/0</ip>
</networks>
<!-- Settings profile for user. -->
<profile>default</profile>
<!-- Quota for user. -->
<quota>default</quota>
<!-- User can create other users and grant rights to them. -->
<!-- <access_management>1</access_management> -->
</default>
</users>
<!-- Quotas. -->
<quotas>
<!-- Name of quota. -->
<default>
<!-- Limits for time interval. You could specify many intervals with different limits. -->
<interval>
<!-- Length of interval. -->
<duration>3600</duration>
<!-- No limits. Just calculate resource usage for time interval. -->
<queries>0</queries>
<errors>0</errors>
<result_rows>0</result_rows>
<read_rows>0</read_rows>
<execution_time>0</execution_time>
</interval>
</default>
</quotas>
</clickhouse>

View File

@@ -1,134 +0,0 @@
version: "2.4"
services:
clickhouse:
image: altinity/clickhouse-server:21.12.3.32.altinitydev.arm
# ports:
# - "9000:9000"
# - "8123:8123"
volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
- ./data/clickhouse/:/var/lib/clickhouse/
restart: on-failure
logging:
options:
max-size: 50m
max-file: "3"
healthcheck:
# "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'"
test: ["CMD", "wget", "--spider", "-q", "localhost:8123/ping"]
interval: 30s
timeout: 5s
retries: 3
alertmanager:
image: signoz/alertmanager:0.23.0-0.1
volumes:
- ./data/alertmanager:/data
depends_on:
query-service:
condition: service_healthy
restart: on-failure
command:
- --queryService.url=http://query-service:8080
- --storage.path=/data
# 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:0.8.0
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
# ports:
# - "6060:6060" # pprof port
# - "8080:8080" # query-service port
volumes:
- ./prometheus.yml:/root/config/prometheus.yml
- ../dashboards:/root/config/dashboards
- ./data/signoz/:/var/lib/signoz/
environment:
- ClickHouseUrl=tcp://clickhouse:9000/?database=signoz_traces
- STORAGE=clickhouse
- GODEBUG=netdns=go
- TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-standalone-arm
restart: on-failure
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
interval: 30s
timeout: 5s
retries: 3
depends_on:
clickhouse:
condition: service_healthy
frontend:
image: signoz/frontend:0.8.0
container_name: frontend
restart: on-failure
depends_on:
- alertmanager
- query-service
ports:
- "3301:3301"
volumes:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/otelcontribcol:0.43.0-0.1
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
# - "8889:8889" # Prometheus metrics exposed by the agent
# - "13133:13133" # health_check
# - "14268:14268" # Jaeger receiver
# - "55678:55678" # OpenCensus receiver
# - "55679:55679" # zpages extension
# - "55680:55680" # OTLP gRPC legacy receiver
# - "55681:55681" # OTLP HTTP legacy receiver
mem_limit: 2000m
restart: on-failure
depends_on:
clickhouse:
condition: service_healthy
otel-collector-metrics:
image: signoz/otelcontribcol:0.43.0-0.1
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
restart: on-failure
depends_on:
clickhouse:
condition: service_healthy
hotrod:
image: jaegertracing/example-hotrod:1.30
container_name: hotrod
logging:
options:
max-size: 50m
max-file: "3"
command: ["all"]
environment:
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
load-hotrod:
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
container_name: load-hotrod
hostname: load-hotrod
environment:
ATTACKED_HOST: http://hotrod:8080
LOCUST_MODE: standalone
NO_PROXY: standalone
TASK_DELAY_FROM: 5
TASK_DELAY_TO: 30
QUIET_MODE: "${QUIET_MODE:-false}"
LOCUST_OPTS: "--headless -u 10 -r 1"
volumes:
- ../common/locust-scripts:/locust

View File

@@ -2,12 +2,15 @@ version: "2.4"
services:
clickhouse:
image: yandex/clickhouse-server:21.12.3.32
image: clickhouse/clickhouse-server:22.4.5-alpine
# ports:
# - "9000:9000"
# - "8123:8123"
tty: true
volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
- ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
# - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
- ./data/clickhouse/:/var/lib/clickhouse/
restart: on-failure
logging:
@@ -30,13 +33,13 @@ services:
condition: service_healthy
restart: on-failure
command:
- --queryService.url=http://query-service:8080
- --queryService.url=http://query-service:8085
- --storage.path=/data
# 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:0.8.0
image: signoz/query-service:0.10.0
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
# ports:
@@ -63,7 +66,7 @@ services:
condition: service_healthy
frontend:
image: signoz/frontend:0.8.0
image: signoz/frontend:0.10.0
container_name: frontend
restart: on-failure
depends_on:
@@ -75,20 +78,24 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/otelcontribcol:0.43.0-0.1
image: signoz/otelcontribcol:0.45.1-1.1
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
ports:
# - "1777:1777" # pprof extension
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
# - "8889:8889" # Prometheus metrics exposed by the agent
# - "13133:13133" # health_check
# - "14268:14268" # Jaeger receiver
# - "8888:8888" # OtelCollector internal metrics
# - "8889:8889" # signoz spanmetrics exposed by the agent
# - "9411:9411" # Zipkin port
# - "13133:13133" # health check extension
# - "14250:14250" # Jaeger gRPC
# - "14268:14268" # Jaeger thrift HTTP
# - "55678:55678" # OpenCensus receiver
# - "55679:55679" # zpages extension
# - "55680:55680" # OTLP gRPC legacy receiver
# - "55681:55681" # OTLP HTTP legacy receiver
# - "55679:55679" # zPages extension
mem_limit: 2000m
restart: on-failure
depends_on:
@@ -96,10 +103,15 @@ services:
condition: service_healthy
otel-collector-metrics:
image: signoz/otelcontribcol:0.43.0-0.1
image: signoz/otelcontribcol:0.45.1-1.1
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
# ports:
# - "1777:1777" # pprof extension
# - "8888:8888" # OtelCollector internal metrics
# - "13133:13133" # Health check extension
# - "55679:55679" # zPages extension
restart: on-failure
depends_on:
clickhouse:

View File

@@ -1,28 +1,40 @@
receivers:
opencensus:
endpoint: 0.0.0.0:55678
otlp/spanmetrics:
protocols:
grpc:
endpoint: "localhost:12345"
endpoint: localhost:12345
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
jaeger:
protocols:
grpc:
endpoint: 0.0.0.0:14250
thrift_http:
endpoint: 0.0.0.0:14268
# thrift_compact:
# endpoint: 0.0.0.0:6831
# thrift_binary:
# endpoint: 0.0.0.0:6832
hostmetrics:
collection_interval: 30s
collection_interval: 60s
scrapers:
cpu:
load:
memory:
disk:
filesystem:
network:
cpu: {}
load: {}
memory: {}
disk: {}
filesystem: {}
network: {}
processors:
batch:
send_batch_size: 1000
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
signozspanmetrics/prometheus:
metrics_exporter: prometheus
@@ -48,29 +60,52 @@ processors:
# num_workers: 4
# queue_size: 100
# retry_on_failure: true
resourcedetection:
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
detectors: [env, system] # include ec2 for AWS, gce for GCP and azure for Azure.
timeout: 2s
override: false
extensions:
health_check: {}
zpages: {}
health_check:
endpoint: 0.0.0.0:13133
zpages:
endpoint: 0.0.0.0:55679
pprof:
endpoint: 0.0.0.0:1777
exporters:
clickhouse:
clickhousetraces:
datasource: tcp://clickhouse:9000/?database=signoz_traces
clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
resource_to_telemetry_conversion:
enabled: true
prometheus:
endpoint: "0.0.0.0:8889"
endpoint: 0.0.0.0:8889
# logging: {}
service:
extensions: [health_check, zpages]
telemetry:
metrics:
address: 0.0.0.0:8888
extensions:
- health_check
- zpages
- pprof
pipelines:
traces:
receivers: [jaeger, otlp]
processors: [signozspanmetrics/prometheus, batch]
exporters: [clickhouse]
exporters: [clickhousetraces]
metrics:
receivers: [otlp, hostmetrics]
receivers: [otlp]
processors: [batch]
exporters: [clickhousemetricswrite]
metrics/hostmetrics:
receivers: [hostmetrics]
processors: [resourcedetection, batch]
exporters: [clickhousemetricswrite]
metrics/spanmetrics:
receivers: [otlp/spanmetrics]
exporters: [prometheus]

View File

@@ -3,18 +3,32 @@ receivers:
protocols:
grpc:
http:
# Data sources: metrics
prometheus:
config:
scrape_configs:
# otel-collector internal metrics
- job_name: "otel-collector"
scrape_interval: 30s
scrape_interval: 60s
static_configs:
- targets: ["otel-collector:8889"]
- targets:
- otel-collector:8888
# otel-collector-metrics internal metrics
- job_name: "otel-collector-metrics"
scrape_interval: 60s
static_configs:
- targets:
- localhost:8888
# SigNoz span metrics
- job_name: "signozspanmetrics-collector"
scrape_interval: 60s
static_configs:
- targets:
- otel-collector:8889
processors:
batch:
send_batch_size: 1000
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
# memory_limiter:
# # 80% of maximum memory up to 2G
@@ -31,17 +45,29 @@ processors:
# num_workers: 4
# queue_size: 100
# retry_on_failure: true
extensions:
health_check: {}
zpages: {}
health_check:
endpoint: 0.0.0.0:13133
zpages:
endpoint: 0.0.0.0:55679
pprof:
endpoint: 0.0.0.0:1777
exporters:
clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/?database=signoz_metrics
service:
extensions: [health_check, zpages]
telemetry:
metrics:
address: 0.0.0.0:8888
extensions:
- health_check
- zpages
- pprof
pipelines:
metrics:
receivers: [otlp, prometheus]
receivers: [prometheus]
processors: [batch]
exporters: [clickhousemetricswrite]

View File

@@ -11,6 +11,11 @@ server {
gzip_buffers 16 8k;
gzip_http_version 1.1;
# to handle uri issue 414 from nginx
client_max_body_size 24M;
large_client_header_buffers 8 16k;
location / {
if ( $uri = '/index.html' ) {
add_header Cache-Control no-store always;

View File

@@ -36,9 +36,9 @@ is_mac() {
[[ $OSTYPE == darwin* ]]
}
is_arm64(){
[[ `uname -m` == 'arm64' ]]
}
# is_arm64(){
# [[ `uname -m` == 'arm64' ]]
# }
check_os() {
if is_mac; then
@@ -237,11 +237,7 @@ bye() { # Prints a friendly good bye message and exits the script.
echo "🔴 The containers didn't seem to start correctly. Please run the following command to check containers that may have errored out:"
echo ""
if is_arm64; then
echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.arm.yaml ps -a"
else
echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml ps -a"
fi
echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml ps -a"
# echo "Please read our troubleshooting guide https://signoz.io/docs/deployment/docker#troubleshooting"
echo "or reach us for support in #help channel in our Slack Community https://signoz.io/slack"
@@ -466,22 +462,14 @@ start_docker
echo ""
echo -e "\n🟡 Pulling the latest container images for SigNoz.\n"
if is_arm64; then
$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.arm.yaml pull
else
$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml pull
fi
$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml pull
echo ""
echo "🟡 Starting the SigNoz containers. It may take a few minutes ..."
echo
# The docker-compose command does some nasty stuff for the `--detach` functionality. So we add a `|| true` so that the
# script doesn't exit because this command looks like it failed to do it's thing.
if is_arm64; then
$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.arm.yaml up --detach --remove-orphans || true
else
$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml up --detach --remove-orphans || true
fi
$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml up --detach --remove-orphans || true
wait_for_containers_start 60
echo ""
@@ -510,11 +498,7 @@ else
echo -e "🟢 Your frontend is running on http://localhost:3301"
echo ""
if is_arm64; then
echo " To bring down SigNoz and clean volumes : $sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.arm.yaml down -v"
else
echo " To bring down SigNoz and clean volumes : $sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml down -v"
fi
echo " To bring down SigNoz and clean volumes : $sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml down -v"
echo ""
echo "+++++++++++++++++++++++++++++++++++++++++++++++++"

View File

@@ -1 +0,0 @@
module.exports = { extends: ['@commitlint/config-conventional'] };

View File

@@ -0,0 +1 @@
export default { extends: ['@commitlint/config-conventional'] };

View File

@@ -2,3 +2,4 @@
* Adds custom matchers from the react testing library to all tests
*/
import '@testing-library/jest-dom';
import 'jest-styled-components';

View File

@@ -13,8 +13,9 @@
"jest:coverage": "jest --coverage",
"jest:watch": "jest --watch",
"postinstall": "is-ci || yarn husky:configure",
"playwright": "playwright test --config=./playwright.config.ts",
"playwright": "NODE_ENV=testing playwright test --config=./playwright.config.ts",
"playwright:local:debug": "PWDEBUG=console yarn playwright --headed --browser=chromium",
"playwright:codegen:local":"playwright codegen http://localhost:3301",
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
"commitlint": "commitlint --edit $1"
},
@@ -43,6 +44,7 @@
"babel-preset-react-app": "^10.0.0",
"chart.js": "^3.4.0",
"chartjs-adapter-date-fns": "^2.0.0",
"chartjs-plugin-annotation": "^1.4.0",
"color": "^4.2.1",
"cross-env": "^7.0.3",
"css-loader": "4.3.0",
@@ -68,7 +70,7 @@
"react-dom": "17.0.0",
"react-force-graph": "^1.41.0",
"react-graph-vis": "^1.0.5",
"react-grid-layout": "^1.2.5",
"react-grid-layout": "^1.3.4",
"react-i18next": "^11.16.1",
"react-query": "^3.34.19",
"react-redux": "^7.2.2",
@@ -81,6 +83,7 @@
"style-loader": "1.3.0",
"styled-components": "^5.2.1",
"terser-webpack-plugin": "^5.2.5",
"timestamp-nano": "^1.0.0",
"ts-node": "^10.2.1",
"tsconfig-paths-webpack-plugin": "^3.5.1",
"typescript": "^4.0.5",
@@ -159,6 +162,7 @@
"husky": "^7.0.4",
"is-ci": "^3.0.1",
"jest-playwright-preset": "^1.7.0",
"jest-styled-components": "^7.0.8",
"less-plugin-npm-import": "^2.1.0",
"lint-staged": "^12.3.7",
"portfinder-sync": "^0.0.2",

View File

@@ -11,11 +11,13 @@ const config: PlaywrightTestConfig = {
testDir: './tests',
use: {
trace: 'retain-on-failure',
baseURL: process.env.FRONTEND_API_ENDPOINT,
baseURL: process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3301',
},
updateSnapshots: 'all',
fullyParallel: false,
quiet: true,
testMatch: ['**/*.spec.ts'],
reporter: process.env.CI ? 'github' : 'list',
};
export default config;

View File

@@ -0,0 +1,85 @@
{
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
"preview_chart_threshold_label": "Threshold",
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
"button_yes": "Yes",
"button_no": "No",
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
"remove_label_success": "Labels cleared",
"alert_form_step1": "Step 1 - Define the metric",
"alert_form_step2": "Step 2 - Define Alert Conditions",
"alert_form_step3": "Step 3 - Alert Configuration",
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
"confirm_save_title": "Save Changes",
"confirm_save_content_part1": "Your alert built with",
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
"rule_created": "Rule created successfully",
"rule_edited": "Rule edited successfully",
"expression_missing": "expression is missing in {{where}}",
"metricname_missing": "metric name is missing in {{where}}",
"condition_required": "at least one metric condition is required",
"alertname_required": "alert name is required",
"promql_required": "promql expression is required when query format is set to PromQL",
"button_savechanges": "Save Rule",
"button_createrule": "Create Rule",
"button_returntorules": "Return to rules",
"button_cancelchanges": "Cancel",
"button_discard": "Discard",
"text_condition1": "Send a notification when the metric is",
"text_condition2": "the threshold",
"text_condition3": "during the last",
"option_5min": "5 mins",
"option_10min": "10 mins",
"option_15min": "15 mins",
"option_60min": "60 mins",
"option_4hours": "4 hours",
"option_24hours": "24 hours",
"field_threshold": "Alert Threshold",
"option_allthetimes": "all the times",
"option_atleastonce": "at least once",
"option_onaverage": "on average",
"option_intotal": "in total",
"option_above": "above",
"option_below": "below",
"option_equal": "is equal to",
"option_notequal": "not equal to",
"button_query": "Query",
"button_formula": "Formula",
"tab_qb": "Query Builder",
"tab_promql": "PromQL",
"title_confirm": "Confirm",
"button_ok": "Yes",
"button_cancel": "No",
"field_promql_expr": "PromQL Expression",
"field_alert_name": "Alert Name",
"field_alert_desc": "Alert Description",
"field_labels": "Labels",
"field_severity": "Severity",
"option_critical": "Critical",
"option_error": "Error",
"option_warning": "Warning",
"option_info": "Info",
"user_guide_headline": "Steps to create an Alert",
"user_guide_qb_step1": "Step 1 - Define the metric",
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
"user_guide_qb_step2b": "Enter the Alert threshold",
"user_guide_qb_step3": "Step 3 -Alert Configuration",
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
"user_guide_pql_step1": "Step 1 - Define the metric",
"user_guide_pql_step1a": "Write a PromQL query for the metric",
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
"user_guide_pql_step2b": "Enter the Alert threshold",
"user_guide_pql_step3": "Step 3 -Alert Configuration",
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
"user_tooltip_more_help": "More details on how to create alerts"
}

View File

@@ -0,0 +1,85 @@
{
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
"preview_chart_threshold_label": "Threshold",
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
"button_yes": "Yes",
"button_no": "No",
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
"remove_label_success": "Labels cleared",
"alert_form_step1": "Step 1 - Define the metric",
"alert_form_step2": "Step 2 - Define Alert Conditions",
"alert_form_step3": "Step 3 - Alert Configuration",
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
"confirm_save_title": "Save Changes",
"confirm_save_content_part1": "Your alert built with",
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
"rule_created": "Rule created successfully",
"rule_edited": "Rule edited successfully",
"expression_missing": "expression is missing in {{where}}",
"metricname_missing": "metric name is missing in {{where}}",
"condition_required": "at least one metric condition is required",
"alertname_required": "alert name is required",
"promql_required": "promql expression is required when query format is set to PromQL",
"button_savechanges": "Save Rule",
"button_createrule": "Create Rule",
"button_returntorules": "Return to rules",
"button_cancelchanges": "Cancel",
"button_discard": "Discard",
"text_condition1": "Send a notification when the metric is",
"text_condition2": "the threshold",
"text_condition3": "during the last",
"option_5min": "5 mins",
"option_10min": "10 mins",
"option_15min": "15 mins",
"option_60min": "60 mins",
"option_4hours": "4 hours",
"option_24hours": "24 hours",
"field_threshold": "Alert Threshold",
"option_allthetimes": "all the times",
"option_atleastonce": "at least once",
"option_onaverage": "on average",
"option_intotal": "in total",
"option_above": "above",
"option_below": "below",
"option_equal": "is equal to",
"option_notequal": "not equal to",
"button_query": "Query",
"button_formula": "Formula",
"tab_qb": "Query Builder",
"tab_promql": "PromQL",
"title_confirm": "Confirm",
"button_ok": "Yes",
"button_cancel": "No",
"field_promql_expr": "PromQL Expression",
"field_alert_name": "Alert Name",
"field_alert_desc": "Alert Description",
"field_labels": "Labels",
"field_severity": "Severity",
"option_critical": "Critical",
"option_error": "Error",
"option_warning": "Warning",
"option_info": "Info",
"user_guide_headline": "Steps to create an Alert",
"user_guide_qb_step1": "Step 1 - Define the metric",
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
"user_guide_qb_step2b": "Enter the Alert threshold",
"user_guide_qb_step3": "Step 3 -Alert Configuration",
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
"user_guide_pql_step1": "Step 1 - Define the metric",
"user_guide_pql_step1a": "Write a PromQL query for the metric",
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
"user_guide_pql_step2b": "Enter the Alert threshold",
"user_guide_pql_step3": "Step 3 -Alert Configuration",
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
"user_tooltip_more_help": "More details on how to create alerts"
}

View File

@@ -0,0 +1,85 @@
{
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
"preview_chart_threshold_label": "Threshold",
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
"button_yes": "Yes",
"button_no": "No",
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
"remove_label_success": "Labels cleared",
"alert_form_step1": "Step 1 - Define the metric",
"alert_form_step2": "Step 2 - Define Alert Conditions",
"alert_form_step3": "Step 3 - Alert Configuration",
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
"confirm_save_title": "Save Changes",
"confirm_save_content_part1": "Your alert built with",
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
"rule_created": "Rule created successfully",
"rule_edited": "Rule edited successfully",
"expression_missing": "expression is missing in {{where}}",
"metricname_missing": "metric name is missing in {{where}}",
"condition_required": "at least one metric condition is required",
"alertname_required": "alert name is required",
"promql_required": "promql expression is required when query format is set to PromQL",
"button_savechanges": "Save Rule",
"button_createrule": "Create Rule",
"button_returntorules": "Return to rules",
"button_cancelchanges": "Cancel",
"button_discard": "Discard",
"text_condition1": "Send a notification when the metric is",
"text_condition2": "the threshold",
"text_condition3": "during the last",
"option_5min": "5 mins",
"option_10min": "10 mins",
"option_15min": "15 mins",
"option_60min": "60 mins",
"option_4hours": "4 hours",
"option_24hours": "24 hours",
"field_threshold": "Alert Threshold",
"option_allthetimes": "all the times",
"option_atleastonce": "at least once",
"option_onaverage": "on average",
"option_intotal": "in total",
"option_above": "above",
"option_below": "below",
"option_equal": "is equal to",
"option_notequal": "not equal to",
"button_query": "Query",
"button_formula": "Formula",
"tab_qb": "Query Builder",
"tab_promql": "PromQL",
"title_confirm": "Confirm",
"button_ok": "Yes",
"button_cancel": "No",
"field_promql_expr": "PromQL Expression",
"field_alert_name": "Alert Name",
"field_alert_desc": "Alert Description",
"field_labels": "Labels",
"field_severity": "Severity",
"option_critical": "Critical",
"option_error": "Error",
"option_warning": "Warning",
"option_info": "Info",
"user_guide_headline": "Steps to create an Alert",
"user_guide_qb_step1": "Step 1 - Define the metric",
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
"user_guide_qb_step2b": "Enter the Alert threshold",
"user_guide_qb_step3": "Step 3 -Alert Configuration",
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
"user_guide_pql_step1": "Step 1 - Define the metric",
"user_guide_pql_step1a": "Write a PromQL query for the metric",
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
"user_guide_pql_step2b": "Enter the Alert threshold",
"user_guide_pql_step3": "Step 3 -Alert Configuration",
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
"user_tooltip_more_help": "More details on how to create alerts"
}

View File

@@ -0,0 +1,85 @@
{
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
"preview_chart_threshold_label": "Threshold",
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
"button_yes": "Yes",
"button_no": "No",
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
"remove_label_success": "Labels cleared",
"alert_form_step1": "Step 1 - Define the metric",
"alert_form_step2": "Step 2 - Define Alert Conditions",
"alert_form_step3": "Step 3 - Alert Configuration",
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
"confirm_save_title": "Save Changes",
"confirm_save_content_part1": "Your alert built with",
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
"rule_created": "Rule created successfully",
"rule_edited": "Rule edited successfully",
"expression_missing": "expression is missing in {{where}}",
"metricname_missing": "metric name is missing in {{where}}",
"condition_required": "at least one metric condition is required",
"alertname_required": "alert name is required",
"promql_required": "promql expression is required when query format is set to PromQL",
"button_savechanges": "Save Rule",
"button_createrule": "Create Rule",
"button_returntorules": "Return to rules",
"button_cancelchanges": "Cancel",
"button_discard": "Discard",
"text_condition1": "Send a notification when the metric is",
"text_condition2": "the threshold",
"text_condition3": "during the last",
"option_5min": "5 mins",
"option_10min": "10 mins",
"option_15min": "15 mins",
"option_60min": "60 mins",
"option_4hours": "4 hours",
"option_24hours": "24 hours",
"field_threshold": "Alert Threshold",
"option_allthetimes": "all the times",
"option_atleastonce": "at least once",
"option_onaverage": "on average",
"option_intotal": "in total",
"option_above": "above",
"option_below": "below",
"option_equal": "is equal to",
"option_notequal": "not equal to",
"button_query": "Query",
"button_formula": "Formula",
"tab_qb": "Query Builder",
"tab_promql": "PromQL",
"title_confirm": "Confirm",
"button_ok": "Yes",
"button_cancel": "No",
"field_promql_expr": "PromQL Expression",
"field_alert_name": "Alert Name",
"field_alert_desc": "Alert Description",
"field_labels": "Labels",
"field_severity": "Severity",
"option_critical": "Critical",
"option_error": "Error",
"option_warning": "Warning",
"option_info": "Info",
"user_guide_headline": "Steps to create an Alert",
"user_guide_qb_step1": "Step 1 - Define the metric",
"user_guide_qb_step1a": "Choose a metric which you want to create an alert on",
"user_guide_qb_step1b": "Filter it based on WHERE field or GROUPBY if needed",
"user_guide_qb_step1c": "Apply an aggregatiion function like COUNT, SUM, etc. or choose NOOP to plot the raw metric",
"user_guide_qb_step1d": "Create a formula based on Queries if needed",
"user_guide_qb_step2": "Step 2 - Define Alert Conditions",
"user_guide_qb_step2a": "Select the evaluation interval, threshold type and whether you want to alert above/below a value",
"user_guide_qb_step2b": "Enter the Alert threshold",
"user_guide_qb_step3": "Step 3 -Alert Configuration",
"user_guide_qb_step3a": "Set alert severity, name and descriptions",
"user_guide_qb_step3b": "Add tags to the alert in the Label field if needed",
"user_guide_pql_step1": "Step 1 - Define the metric",
"user_guide_pql_step1a": "Write a PromQL query for the metric",
"user_guide_pql_step1b": "Format the legends based on labels you want to highlight",
"user_guide_pql_step2": "Step 2 - Define Alert Conditions",
"user_guide_pql_step2a": "Select the threshold type and whether you want to alert above/below a value",
"user_guide_pql_step2b": "Enter the Alert threshold",
"user_guide_pql_step3": "Step 3 -Alert Configuration",
"user_guide_pql_step3a": "Set alert severity, name and descriptions",
"user_guide_pql_step3b": "Add tags to the alert in the Label field if needed",
"user_tooltip_more_help": "More details on how to create alerts"
}

View File

@@ -9,7 +9,7 @@ const create = async (
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/rules', {
data: props.query,
...props.data,
});
return {

View File

@@ -14,7 +14,7 @@ const get = async (
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);

View File

@@ -2,14 +2,14 @@ import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/put';
import { PayloadProps, Props } from 'types/api/alerts/save';
const put = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.put(`/rules/${props.id}`, {
data: props.data,
...props.data,
});
return {

View File

@@ -0,0 +1,17 @@
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/save';
import create from './create';
import put from './put';
const save = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
if (props.id && props.id > 0) {
return put({ ...props });
}
return create({ ...props });
};
export default save;

View File

@@ -10,9 +10,8 @@ const getAll = async (
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/errors?${createQueryParams({
start: props.start.toString(),
end: props.end.toString(),
`/listErrors?${createQueryParams({
...props,
})}`,
);

View File

@@ -10,11 +10,8 @@ const getByErrorType = async (
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/errorWithType?${createQueryParams({
start: props.start.toString(),
end: props.end.toString(),
serviceName: props.serviceName,
errorType: props.errorType,
`/errorFromGroupID?${createQueryParams({
...props,
})}`,
);

View File

@@ -3,17 +3,15 @@ import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/errors/getById';
import { PayloadProps, Props } from 'types/api/errors/getByErrorId';
const getById = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/errorWithId?${createQueryParams({
start: props.start.toString(),
end: props.end.toString(),
errorId: props.errorId,
`/errorFromErrorID?${createQueryParams({
...props,
})}`,
);

View File

@@ -0,0 +1,29 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/errors/getErrorCounts';
const getErrorCounts = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/countErrors?${createQueryParams({
...props,
})}`,
);
return {
statusCode: 200,
error: null,
message: response.data.message,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getErrorCounts;

View File

@@ -0,0 +1,29 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/errors/getNextPrevId';
const getErrorCounts = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/nextPrevErrorIDs?${createQueryParams({
...props,
})}`,
);
return {
statusCode: 200,
error: null,
message: response.data.message,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getErrorCounts;

View File

@@ -102,6 +102,11 @@ export const AxiosAlertManagerInstance = axios.create({
export const ApiV2Instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV2}`,
});
ApiV2Instance.interceptors.response.use(
interceptorsResponse,
interceptorRejected,
);
ApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
AxiosAlertManagerInstance.interceptors.response.use(
interceptorsResponse,

View File

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

View File

@@ -0,0 +1,25 @@
import { ApiV2Instance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
MetricRangePayloadProps,
MetricsRangeProps,
} from 'types/api/metrics/getQueryRange';
export const getMetricsQueryRange = async (
props: MetricsRangeProps,
): Promise<SuccessResponse<MetricRangePayloadProps> | ErrorResponse> => {
try {
const response = await axios.post(`/metrics/query_range`, props);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -3,17 +3,20 @@ import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
TagKeyProps,
TagKeysPayloadProps,
TagValueProps,
TagValuesPayloadProps,
} from 'types/api/metrics/getResourceAttributes';
export const getResourceAttributesTagKeys = async (): Promise<
SuccessResponse<TagKeysPayloadProps> | ErrorResponse
> => {
export const getResourceAttributesTagKeys = async (
props: TagKeyProps,
): Promise<SuccessResponse<TagKeysPayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
'/metrics/autocomplete/tagKey?metricName=signoz_calls_total&match=resource_',
`/metrics/autocomplete/tagKey?metricName=${props.metricName}${
props.match ? `&match=${props.match}` : ''
}`,
);
return {
@@ -32,7 +35,7 @@ export const getResourceAttributesTagValues = async (
): Promise<SuccessResponse<TagValuesPayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/metrics/autocomplete/tagValue?metricName=signoz_calls_total&tagKey=${props}`,
`/metrics/autocomplete/tagValue?metricName=${props.metricName}&tagKey=${props.tagKey}`,
);
return {

View File

@@ -1,14 +1,15 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { getVersion } from 'constants/api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/user/getVersion';
const getVersion = async (): Promise<
const getVersionApi = async (): Promise<
SuccessResponse<PayloadProps> | ErrorResponse
> => {
try {
const response = await axios.get(`/version`);
const response = await axios.get(`/${getVersion}`);
return {
statusCode: 200,
@@ -21,4 +22,4 @@ const getVersion = async (): Promise<
}
};
export default getVersion;
export default getVersionApi;

View File

@@ -1,38 +1,46 @@
import MEditor from '@monaco-editor/react';
import MEditor, { EditorProps } from '@monaco-editor/react';
import React from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
function Editor({
value,
language = 'yaml',
language,
onChange,
readOnly = false,
}: EditorProps): JSX.Element {
readOnly,
height,
options,
}: MEditorProps): JSX.Element {
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
return (
<MEditor
theme="vs-dark"
theme={isDarkMode ? 'vs-dark' : 'vs-light'}
language={language}
value={value}
options={{ fontSize: 16, automaticLayout: true, readOnly }}
height="40vh"
options={{ fontSize: 16, automaticLayout: true, readOnly, ...options }}
height={height}
onChange={(newValue): void => {
if (newValue) {
onChange(newValue);
}
if (typeof newValue === 'string') onChange(newValue);
}}
/>
);
}
interface EditorProps {
interface MEditorProps {
value: string;
language?: string;
onChange: (value: string) => void;
readOnly?: boolean;
height?: string;
options?: EditorProps['options'];
}
Editor.defaultProps = {
language: undefined,
language: 'yaml',
readOnly: false,
height: '40vh',
options: {},
};
export default Editor;

View File

@@ -22,7 +22,6 @@ const getOrCreateLegendList = (
listContainer.style.height = '100%';
listContainer.style.flexWrap = 'wrap';
listContainer.style.justifyContent = 'center';
legendContainer?.appendChild(listContainer);
}

View File

@@ -22,6 +22,7 @@ import {
Tooltip,
} from 'chart.js';
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
import annotationPlugin from 'chartjs-plugin-annotation';
import React, { useCallback, useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -50,6 +51,7 @@ Chart.register(
SubTitle,
BarController,
BarElement,
annotationPlugin,
);
function Graph({
@@ -62,6 +64,7 @@ function Graph({
name,
yAxisUnit = 'short',
forceReRender,
staticLine,
}: GraphProps): JSX.Element {
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const chartRef = useRef<HTMLCanvasElement>(null);
@@ -99,6 +102,30 @@ function Graph({
intersect: false,
},
plugins: {
annotation: staticLine
? {
annotations: [
{
type: 'line',
yMin: staticLine.yMin,
yMax: staticLine.yMax,
borderColor: staticLine.borderColor,
borderWidth: staticLine.borderWidth,
label: {
content: staticLine.lineText,
enabled: true,
font: {
size: 10,
},
borderWidth: 0,
position: 'start',
backgroundColor: 'transparent',
color: staticLine.textColor,
},
},
],
}
: undefined,
title: {
display: title !== undefined,
text: title,
@@ -180,13 +207,13 @@ function Graph({
}
},
};
const chartHasData = hasData(data);
const chartPlugins = [];
if (chartHasData) {
chartPlugins.push(legend(name, data.datasets.length > 3));
} else {
chartPlugins.push(emptyGraph);
}
if (!chartHasData) chartPlugins.push(emptyGraph);
chartPlugins.push(legend(name, data.datasets.length > 3));
lineChartRef.current = new Chart(chartRef.current, {
type,
data,
@@ -206,6 +233,7 @@ function Graph({
name,
yAxisUnit,
onClickHandler,
staticLine,
]);
useEffect(() => {
@@ -230,6 +258,16 @@ interface GraphProps {
name: string;
yAxisUnit?: string;
forceReRender?: boolean | null | number;
staticLine?: StaticLineProps | undefined;
}
export interface StaticLineProps {
yMin: number | undefined;
yMax: number | undefined;
borderColor: string;
borderWidth: number;
lineText: string;
textColor: string;
}
export type GraphOnClickHandler = (
@@ -246,5 +284,6 @@ Graph.defaultProps = {
onClickHandler: undefined,
yAxisUnit: undefined,
forceReRender: undefined,
staticLine: undefined,
};
export default Graph;

View File

@@ -109,14 +109,14 @@ export const useXAxisTimeUnit = (data: Chart['data']): IAxisTimeConfig => {
let minTime = Number.POSITIVE_INFINITY;
let maxTime = Number.NEGATIVE_INFINITY;
data?.labels?.forEach((timeStamp: unknown): void => {
const getTimeStamp = (time: string | number): Date | number | string => {
if (typeof timeStamp === 'string') {
return Date.parse(timeStamp);
const getTimeStamp = (time: Date | number): Date | number | string => {
if (time instanceof Date) {
return Date.parse(time.toString());
}
return time;
};
const time = getTimeStamp(timeStamp as string | number);
const time = getTimeStamp(timeStamp as Date | number);
minTime = Math.min(parseInt(time.toString(), 10), minTime);
maxTime = Math.max(parseInt(time.toString(), 10), maxTime);

View File

@@ -2,8 +2,102 @@
exports[`Not Found page test should render Not Found page without errors 1`] = `
<DocumentFragment>
<div
class="sc-gsDKAQ cLXpIa"
.c3 {
border: 2px solid #2f80ed;
box-sizing: border-box;
border-radius: 10px;
width: 400px;
background: inherit;
font-style: normal;
font-weight: normal;
font-size: 24px;
line-height: 20px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
padding-top: 14px;
padding-bottom: 14px;
color: #2f80ed;
}
.c0 {
min-height: 80vh;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c2 {
font-style: normal;
font-weight: 300;
font-size: 18px;
line-height: 20px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
text-align: center;
color: #828282;
text-align: center;
margin: 0;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c1 {
min-height: 50px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
margin-bottom: 30px;
margin-top: 20px;
}
<div
class="c0"
>
<svg
fill="none"
@@ -272,21 +366,21 @@ exports[`Not Found page test should render Not Found page without errors 1`] = `
</defs>
</svg>
<div
class="sc-hKwDye foaleg"
class="c1"
>
<p
class="sc-dkPtRN fcyVIq"
class="c2"
>
Ah, seems like we reached a dead end!
</p>
<p
class="sc-dkPtRN fcyVIq"
class="c2"
>
Page Not Found
</p>
</div>
<a
class="sc-bdvvtL dbTZkj"
class="c3"
href="/application"
tabindex="0"
>

View File

@@ -10,9 +10,11 @@ function TextToolTip({ text, url }: TextToolTipProps): JSX.Element {
return (
<div>
{`${text} `}
<a href={url} rel="noopener noreferrer" target="_blank">
here
</a>
{url && (
<a href={url} rel="noopener noreferrer" target="_blank">
here
</a>
)}
</div>
);
}}
@@ -22,8 +24,11 @@ function TextToolTip({ text, url }: TextToolTipProps): JSX.Element {
);
}
TextToolTip.defaultProps = {
url: '',
};
interface TextToolTipProps {
url: string;
url?: string;
text: string;
}

View File

@@ -0,0 +1,3 @@
const getVersion = 'version';
export { getVersion };

View File

@@ -0,0 +1,33 @@
import { EAggregateOperator, EReduceOperator } from 'types/common/dashboard';
export const PromQLQueryTemplate = {
query: '',
legend: '',
disabled: false,
};
export const ClickHouseQueryTemplate = {
rawQuery: '',
legend: '',
disabled: false,
};
export const QueryBuilderQueryTemplate = {
metricName: null,
aggregateOperator: EAggregateOperator.NOOP,
tagFilters: {
op: 'AND',
items: [],
},
legend: '',
disabled: false,
// Specific to TIME_SERIES type graph
groupBy: [],
// Specific to VALUE type graph
reduceTo: EReduceOperator['Latest of values in timeframe'],
};
export const QueryBuilderFormulaTemplate = {
expression: '',
disabled: false,
};

View File

@@ -12,7 +12,7 @@ const ROUTES = {
ALL_DASHBOARD: '/dashboard',
DASHBOARD: '/dashboard/:dashboardId',
DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId',
EDIT_ALERTS: '/alerts/edit/:ruleId',
EDIT_ALERTS: '/alerts/edit',
LIST_ALL_ALERT: '/alerts',
ALERTS_NEW: '/alerts/new',
ALL_CHANNELS: '/settings/channels',

View File

@@ -1,31 +1,85 @@
import { notification, Table, Tooltip, Typography } from 'antd';
import { notification, Table, TableProps, Tooltip, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import getAll from 'api/errors/getAll';
import getErrorCounts from 'api/errors/getErrorCounts';
import ROUTES from 'constants/routes';
import dayjs from 'dayjs';
import React, { useEffect } from 'react';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import React, { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { Link, useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Exception } from 'types/api/errors/getAll';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Exception, PayloadProps } from 'types/api/errors/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import {
getDefaultOrder,
getNanoSeconds,
getOffSet,
getOrder,
getOrderParams,
getUpdatePageSize,
urlKey,
} from './utils';
function AllErrors(): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
const { maxTime, minTime, loading } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { search, pathname } = useLocation();
const params = useMemo(() => new URLSearchParams(search), [search]);
const { t } = useTranslation(['common']);
const { isLoading, data } = useQuery(['getAllError', [maxTime, minTime]], {
queryFn: () =>
getAll({
end: maxTime,
start: minTime,
}),
});
const updatedOrder = getOrder(params.get(urlKey.order));
const getUpdatedOffset = getOffSet(params.get(urlKey.offset));
const getUpdatedParams = getOrderParams(params.get(urlKey.orderParam));
const getUpdatedPageSize = getUpdatePageSize(params.get(urlKey.pageSize));
const updatedPath = useMemo(
() =>
`${pathname}?${createQueryParams({
order: updatedOrder,
offset: getUpdatedOffset,
orderParam: getUpdatedParams,
pageSize: getUpdatedPageSize,
})}`,
[
pathname,
updatedOrder,
getUpdatedOffset,
getUpdatedParams,
getUpdatedPageSize,
],
);
const [{ isLoading, data }, errorCountResponse] = useQueries([
{
queryKey: ['getAllErrors', updatedPath, maxTime, minTime],
queryFn: (): Promise<SuccessResponse<PayloadProps> | ErrorResponse> =>
getAll({
end: maxTime,
start: minTime,
order: updatedOrder,
limit: getUpdatedPageSize,
offset: getUpdatedOffset,
orderParam: getUpdatedParams,
}),
enabled: !loading,
},
{
queryKey: ['getErrorCounts', maxTime, minTime],
queryFn: (): Promise<ErrorResponse | SuccessResponse<number>> =>
getErrorCounts({
end: maxTime,
start: minTime,
}),
},
]);
useEffect(() => {
if (data?.error) {
@@ -35,11 +89,9 @@ function AllErrors(): JSX.Element {
}
}, [data?.error, data?.payload, t]);
const getDateValue = (value: string): JSX.Element => {
return (
<Typography>{dayjs(value).format('DD/MM/YYYY HH:mm:ss A')}</Typography>
);
};
const getDateValue = (value: string): JSX.Element => (
<Typography>{dayjs(value).format('DD/MM/YYYY HH:mm:ss A')}</Typography>
);
const columns: ColumnsType<Exception> = [
{
@@ -49,14 +101,20 @@ function AllErrors(): JSX.Element {
render: (value, record): JSX.Element => (
<Tooltip overlay={(): JSX.Element => value}>
<Link
to={`${ROUTES.ERROR_DETAIL}?serviceName=${record.serviceName}&errorType=${record.exceptionType}`}
to={`${ROUTES.ERROR_DETAIL}?groupId=${
record.groupID
}&timestamp=${getNanoSeconds(record.lastSeen)}`}
>
{value}
</Link>
</Tooltip>
),
sorter: (a, b): number =>
a.exceptionType.charCodeAt(0) - b.exceptionType.charCodeAt(0),
sorter: true,
defaultSortOrder: getDefaultOrder(
getUpdatedParams,
updatedOrder,
'exceptionType',
),
},
{
title: 'Error Message',
@@ -78,39 +136,86 @@ function AllErrors(): JSX.Element {
title: 'Count',
dataIndex: 'exceptionCount',
key: 'exceptionCount',
sorter: (a, b): number => a.exceptionCount - b.exceptionCount,
sorter: true,
defaultSortOrder: getDefaultOrder(
getUpdatedParams,
updatedOrder,
'exceptionCount',
),
},
{
title: 'Last Seen',
dataIndex: 'lastSeen',
key: 'lastSeen',
render: getDateValue,
sorter: (a, b): number =>
dayjs(b.lastSeen).isBefore(dayjs(a.lastSeen)) === true ? 1 : 0,
sorter: true,
defaultSortOrder: getDefaultOrder(
getUpdatedParams,
updatedOrder,
'lastSeen',
),
},
{
title: 'First Seen',
dataIndex: 'firstSeen',
key: 'firstSeen',
render: getDateValue,
sorter: (a, b): number =>
dayjs(b.firstSeen).isBefore(dayjs(a.firstSeen)) === true ? 1 : 0,
sorter: true,
defaultSortOrder: getDefaultOrder(
getUpdatedParams,
updatedOrder,
'firstSeen',
),
},
{
title: 'Application',
dataIndex: 'serviceName',
key: 'serviceName',
sorter: (a, b): number =>
a.serviceName.charCodeAt(0) - b.serviceName.charCodeAt(0),
sorter: true,
defaultSortOrder: getDefaultOrder(
getUpdatedParams,
updatedOrder,
'serviceName',
),
},
];
const onChangeHandler: TableProps<Exception>['onChange'] = (
paginations,
_,
sorter,
) => {
if (!Array.isArray(sorter)) {
const { pageSize = 0, current = 0 } = paginations;
const { columnKey = '', order } = sorter;
const updatedOrder = order === 'ascend' ? 'ascending' : 'descending';
history.replace(
`${pathname}?${createQueryParams({
order: updatedOrder,
offset: (current - 1) * pageSize,
orderParam: columnKey,
pageSize,
})}`,
);
}
};
return (
<Table
tableLayout="fixed"
dataSource={data?.payload as Exception[]}
columns={columns}
loading={isLoading || false}
rowKey="firstSeen"
loading={isLoading || false || errorCountResponse.status === 'loading'}
pagination={{
pageSize: getUpdatedPageSize,
responsive: true,
current: getUpdatedOffset / 10 + 1,
position: ['bottomLeft'],
total: errorCountResponse.data?.payload || 0,
}}
onChange={onChangeHandler}
/>
);
}

View File

@@ -0,0 +1,109 @@
import { Order, OrderBy } from 'types/api/errors/getAll';
import {
getDefaultOrder,
getLimit,
getOffSet,
getOrder,
getOrderParams,
getUpdatePageSize,
isOrder,
isOrderParams,
} from './utils';
describe('Error utils', () => {
test('Valid OrderBy Params', () => {
expect(isOrderParams('serviceName')).toBe(true);
expect(isOrderParams('exceptionCount')).toBe(true);
expect(isOrderParams('lastSeen')).toBe(true);
expect(isOrderParams('firstSeen')).toBe(true);
expect(isOrderParams('exceptionType')).toBe(true);
});
test('Invalid OrderBy Params', () => {
expect(isOrderParams('invalid')).toBe(false);
expect(isOrderParams(null)).toBe(false);
expect(isOrderParams('')).toBe(false);
});
test('Valid Order', () => {
expect(isOrder('ascending')).toBe(true);
expect(isOrder('descending')).toBe(true);
});
test('Invalid Order', () => {
expect(isOrder('invalid')).toBe(false);
expect(isOrder(null)).toBe(false);
expect(isOrder('')).toBe(false);
});
test('Default Order', () => {
const OrderBy: OrderBy[] = [
'exceptionCount',
'exceptionType',
'firstSeen',
'lastSeen',
'serviceName',
];
const order: Order[] = ['ascending', 'descending'];
const ascOrd = order[0];
const desOrd = order[1];
OrderBy.forEach((order) => {
expect(getDefaultOrder(order, ascOrd, order)).toBe('ascend');
expect(getDefaultOrder(order, desOrd, order)).toBe('descend');
});
});
test('Limit', () => {
expect(getLimit(null)).toBe(10);
expect(getLimit('')).toBe(10);
expect(getLimit('0')).toBe(0);
expect(getLimit('1')).toBe(1);
expect(getLimit('10')).toBe(10);
expect(getLimit('11')).toBe(11);
expect(getLimit('100')).toBe(100);
expect(getLimit('101')).toBe(101);
});
test('Update Page Size', () => {
expect(getUpdatePageSize(null)).toBe(10);
expect(getUpdatePageSize('')).toBe(10);
expect(getUpdatePageSize('0')).toBe(0);
expect(getUpdatePageSize('1')).toBe(1);
expect(getUpdatePageSize('10')).toBe(10);
expect(getUpdatePageSize('11')).toBe(11);
expect(getUpdatePageSize('100')).toBe(100);
expect(getUpdatePageSize('101')).toBe(101);
});
test('Order Params', () => {
expect(getOrderParams(null)).toBe('serviceName');
expect(getOrderParams('')).toBe('serviceName');
expect(getOrderParams('serviceName')).toBe('serviceName');
expect(getOrderParams('exceptionCount')).toBe('exceptionCount');
expect(getOrderParams('lastSeen')).toBe('lastSeen');
expect(getOrderParams('firstSeen')).toBe('firstSeen');
expect(getOrderParams('exceptionType')).toBe('exceptionType');
});
test('OffSet', () => {
expect(getOffSet(null)).toBe(0);
expect(getOffSet('')).toBe(0);
expect(getOffSet('0')).toBe(0);
expect(getOffSet('1')).toBe(1);
expect(getOffSet('10')).toBe(10);
expect(getOffSet('11')).toBe(11);
expect(getOffSet('100')).toBe(100);
expect(getOffSet('101')).toBe(101);
});
test('Order', () => {
expect(getOrder(null)).toBe('ascending');
expect(getOrder('')).toBe('ascending');
expect(getOrder('ascending')).toBe('ascending');
expect(getOrder('descending')).toBe('descending');
});
});

View File

@@ -0,0 +1,89 @@
import { SortOrder } from 'antd/lib/table/interface';
import Timestamp from 'timestamp-nano';
import { Order, OrderBy } from 'types/api/errors/getAll';
export const isOrder = (order: string | null): order is Order =>
!!(order === 'ascending' || order === 'descending');
export const urlKey = {
order: 'order',
offset: 'offset',
orderParam: 'orderParam',
pageSize: 'pageSize',
};
export const isOrderParams = (orderBy: string | null): orderBy is OrderBy => {
return !!(
orderBy === 'serviceName' ||
orderBy === 'exceptionCount' ||
orderBy === 'lastSeen' ||
orderBy === 'firstSeen' ||
orderBy === 'exceptionType'
);
};
export const getOrder = (order: string | null): Order => {
if (isOrder(order)) {
return order;
}
return 'ascending';
};
export const getLimit = (limit: string | null): number => {
if (limit) {
return parseInt(limit, 10);
}
return 10;
};
export const getOffSet = (offset: string | null): number => {
if (offset && typeof offset === 'string') {
return parseInt(offset, 10);
}
return 0;
};
export const getOrderParams = (order: string | null): OrderBy => {
if (isOrderParams(order)) {
return order;
}
return 'serviceName';
};
export const getDefaultOrder = (
orderBy: OrderBy,
order: Order,
data: OrderBy,
// eslint-disable-next-line sonarjs/cognitive-complexity
): SortOrder | undefined => {
if (orderBy === 'exceptionType' && data === 'exceptionType') {
return order === 'ascending' ? 'ascend' : 'descend';
}
if (orderBy === 'serviceName' && data === 'serviceName') {
return order === 'ascending' ? 'ascend' : 'descend';
}
if (orderBy === 'exceptionCount' && data === 'exceptionCount') {
return order === 'ascending' ? 'ascend' : 'descend';
}
if (orderBy === 'lastSeen' && data === 'lastSeen') {
return order === 'ascending' ? 'ascend' : 'descend';
}
if (orderBy === 'firstSeen' && data === 'firstSeen') {
return order === 'ascending' ? 'ascend' : 'descend';
}
return undefined;
};
export const getNanoSeconds = (date: string): string => {
return (
Math.floor(new Date(date).getTime() / 1e3).toString() +
Timestamp.fromString(date).getNano().toString()
);
};
export const getUpdatePageSize = (pageSize: string | null): number => {
if (pageSize) {
return parseInt(pageSize, 10);
}
return 10;
};

View File

@@ -0,0 +1,22 @@
import { Form } from 'antd';
import FormAlertRules from 'container/FormAlertRules';
import React from 'react';
import { AlertDef } from 'types/api/alerts/def';
function CreateRules({ initialValue }: CreateRulesProps): JSX.Element {
const [formInstance] = Form.useForm();
return (
<FormAlertRules
formInstance={formInstance}
initialValue={initialValue}
ruleId={0}
/>
);
}
interface CreateRulesProps {
initialValue: AlertDef;
}
export default CreateRules;

View File

@@ -1,102 +1,23 @@
import { SaveFilled } from '@ant-design/icons';
import { Button, notification } from 'antd';
import put from 'api/alerts/put';
import Editor from 'components/Editor';
import ROUTES from 'constants/routes';
import { State } from 'hooks/useFetch';
import history from 'lib/history';
import React, { useCallback, useState } from 'react';
import { PayloadProps } from 'types/api/alerts/get';
import { PayloadProps as PutPayloadProps } from 'types/api/alerts/put';
import { Form } from 'antd';
import FormAlertRules from 'container/FormAlertRules';
import React from 'react';
import { AlertDef } from 'types/api/alerts/def';
import { ButtonContainer } from './styles';
function EditRules({ initialData, ruleId }: EditRulesProps): JSX.Element {
const [value, setEditorValue] = useState<string>(initialData);
const [notifications, Element] = notification.useNotification();
const [editButtonState, setEditButtonState] = useState<State<PutPayloadProps>>(
{
error: false,
errorMessage: '',
loading: false,
success: false,
payload: undefined,
},
);
const onClickHandler = useCallback(async () => {
try {
setEditButtonState((state) => ({
...state,
loading: true,
}));
const response = await put({
data: value,
id: parseInt(ruleId, 10),
});
if (response.statusCode === 200) {
setEditButtonState((state) => ({
...state,
loading: false,
payload: response.payload,
}));
notifications.success({
message: 'Success',
description: 'Congrats. The alert was Edited correctly.',
});
setTimeout(() => {
history.push(ROUTES.LIST_ALL_ALERT);
}, 2000);
} else {
setEditButtonState((state) => ({
...state,
loading: false,
errorMessage: response.error || 'Something went wrong',
error: true,
}));
notifications.error({
message: 'Error',
description:
response.error ||
'Oops! Some issue occured in editing the alert please try again or contact support@signoz.io',
});
}
} catch (error) {
notifications.error({
message: 'Error',
description:
'Oops! Some issue occured in editing the alert please try again or contact support@signoz.io',
});
}
}, [value, ruleId, notifications]);
function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
const [formInstance] = Form.useForm();
return (
<>
{Element}
<Editor onChange={(value): void => setEditorValue(value)} value={value} />
<ButtonContainer>
<Button
loading={editButtonState.loading || false}
disabled={editButtonState.loading || false}
icon={<SaveFilled />}
onClick={onClickHandler}
>
Save
</Button>
</ButtonContainer>
</>
<FormAlertRules
formInstance={formInstance}
initialValue={initialValue}
ruleId={ruleId}
/>
);
}
interface EditRulesProps {
initialData: PayloadProps['data'];
ruleId: string;
initialValue: AlertDef;
ruleId: number;
}
export default EditRules;

View File

@@ -1,25 +1,49 @@
import { Button, Divider, notification, Space, Table, Typography } from 'antd';
import getNextPrevId from 'api/errors/getNextPrevId';
import Editor from 'components/Editor';
import { getNanoSeconds } from 'container/AllError/utils';
import dayjs from 'dayjs';
import history from 'lib/history';
import { urlKey } from 'pages/ErrorDetails/utils';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useLocation } from 'react-router-dom';
import { PayloadProps as GetByErrorTypeAndServicePayload } from 'types/api/errors/getByErrorTypeAndService';
import { PayloadProps } from 'types/api/errors/getById';
import { DashedContainer, EditorContainer, EventContainer } from './styles';
function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
const { idPayload } = props;
const [isLoading, setLoading] = useState<boolean>(false);
const { t } = useTranslation(['errorDetails', 'common']);
const { search } = useLocation();
const params = new URLSearchParams(search);
const queryErrorId = params.get('errorId');
const serviceName = params.get('serviceName');
const errorType = params.get('errorType');
const params = useMemo(() => new URLSearchParams(search), [search]);
const errorId = params.get(urlKey.errorId);
const serviceName = params.get(urlKey.serviceName);
const errorType = params.get(urlKey.exceptionType);
const timestamp = params.get(urlKey.timestamp);
const { data: nextPrevData, status: nextPrevStatus } = useQuery(
[
idPayload.errorId,
idPayload.groupID,
idPayload.timestamp,
errorId,
serviceName,
errorType,
timestamp,
],
{
queryFn: () =>
getNextPrevId({
errorID: errorId || idPayload.errorId,
groupID: idPayload.groupID,
timestamp: timestamp || getNanoSeconds(idPayload.timestamp),
}),
},
);
const errorDetail = idPayload;
@@ -48,34 +72,32 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
'errorId',
'timestamp',
'exceptionMessage',
'newerErrorId',
'olderErrorId',
'exceptionEscaped',
],
[],
);
const onClickErrorIdHandler = async (id: string): Promise<void> => {
const onClickErrorIdHandler = async (
id: string,
timestamp: string,
): Promise<void> => {
try {
setLoading(true);
if (id.length === 0) {
notification.error({
message: 'Error Id cannot be empty',
});
setLoading(false);
return;
}
setLoading(false);
history.push(
`${history.location.pathname}?errorId=${id}&serviceName=${serviceName}&errorType=${errorType}`,
history.replace(
`${history.location.pathname}?&groupId=${
idPayload.groupID
}&timestamp=${getNanoSeconds(timestamp)}&errorId=${id}`,
);
} catch (error) {
notification.error({
message: t('something_went_wrong'),
});
setLoading(false);
}
};
@@ -106,25 +128,25 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
<div>
<Space align="end" direction="horizontal">
<Button
loading={isLoading}
disabled={
errorDetail.olderErrorId.length === 0 ||
queryErrorId === errorDetail.olderErrorId
}
loading={nextPrevStatus === 'loading'}
disabled={nextPrevData?.payload?.prevErrorID.length === 0}
onClick={(): Promise<void> =>
onClickErrorIdHandler(errorDetail.olderErrorId)
onClickErrorIdHandler(
nextPrevData?.payload?.prevErrorID || '',
nextPrevData?.payload?.prevTimestamp || '',
)
}
>
{t('older')}
</Button>
<Button
loading={isLoading}
disabled={
errorDetail.newerErrorId.length === 0 ||
queryErrorId === errorDetail.newerErrorId
}
loading={nextPrevStatus === 'loading'}
disabled={nextPrevData?.payload?.nextErrorID.length === 0}
onClick={(): Promise<void> =>
onClickErrorIdHandler(errorDetail.newerErrorId)
onClickErrorIdHandler(
nextPrevData?.payload?.nextErrorID || '',
nextPrevData?.payload?.nextTimestamp || '',
)
}
>
{t('newer')}
@@ -153,7 +175,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
}
interface ErrorDetailsProps {
idPayload: PayloadProps;
idPayload: GetByErrorTypeAndServicePayload;
}
export default ErrorDetails;

View File

@@ -0,0 +1,101 @@
import { Select } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { AlertDef, Labels } from 'types/api/alerts/def';
import LabelSelect from './labels';
import {
FormContainer,
InputSmall,
SeveritySelect,
StepHeading,
TextareaMedium,
} from './styles';
const { Option } = Select;
interface BasicInfoProps {
alertDef: AlertDef;
setAlertDef: (a: AlertDef) => void;
}
function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('alerts');
return (
<>
<StepHeading> {t('alert_form_step3')} </StepHeading>
<FormContainer>
<FormItem
label={t('field_severity')}
labelAlign="left"
name={['labels', 'severity']}
>
<SeveritySelect
defaultValue="critical"
onChange={(value: unknown | string): void => {
const s = (value as string) || 'critical';
setAlertDef({
...alertDef,
labels: {
...alertDef.labels,
severity: s,
},
});
}}
>
<Option value="critical">{t('option_critical')}</Option>
<Option value="error">{t('option_error')}</Option>
<Option value="warning">{t('option_warning')}</Option>
<Option value="info">{t('option_info')}</Option>
</SeveritySelect>
</FormItem>
<FormItem label={t('field_alert_name')} labelAlign="left" name="alert">
<InputSmall
onChange={(e): void => {
setAlertDef({
...alertDef,
alert: e.target.value,
});
}}
/>
</FormItem>
<FormItem
label={t('field_alert_desc')}
labelAlign="left"
name={['annotations', 'description']}
>
<TextareaMedium
onChange={(e): void => {
setAlertDef({
...alertDef,
annotations: {
...alertDef.annotations,
description: e.target.value,
},
});
}}
/>
</FormItem>
<FormItem label={t('field_labels')}>
<LabelSelect
onSetLabels={(l: Labels): void => {
setAlertDef({
...alertDef,
labels: {
...l,
},
});
}}
initialValues={alertDef.labels}
/>
</FormItem>
</FormContainer>
</>
);
}
export default BasicInfo;

View File

@@ -0,0 +1,119 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import { StaticLineProps } from 'components/Graph';
import GridGraphComponent from 'container/GridGraphComponent';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import getChartData from 'lib/getChartData';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
import { Query } from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard';
import { ChartContainer, FailedMessageContainer } from './styles';
export interface ChartPreviewProps {
name: string;
query: Query | undefined;
graphType?: GRAPH_TYPES;
selectedTime?: timePreferenceType;
selectedInterval?: Time;
headline?: JSX.Element;
threshold?: number;
}
function ChartPreview({
name,
query,
graphType = 'TIME_SERIES',
selectedTime = 'GLOBAL_TIME',
selectedInterval = '5min',
headline,
threshold,
}: ChartPreviewProps): JSX.Element | null {
const { t } = useTranslation('alerts');
const staticLine: StaticLineProps | undefined =
threshold && threshold > 0
? {
yMin: threshold,
yMax: threshold,
borderColor: '#f14',
borderWidth: 1,
lineText: `${t('preview_chart_threshold_label')} (y=${threshold})`,
textColor: '#f14',
}
: undefined;
const queryKey = JSON.stringify(query);
const queryResponse = useQuery({
queryKey: ['chartPreview', queryKey, selectedInterval],
queryFn: () =>
GetMetricQueryRange({
query: query || {
queryType: 1,
promQL: [],
metricsBuilder: {
formulas: [],
queryBuilder: [],
},
clickHouse: [],
},
globalSelectedInterval: selectedInterval,
graphType,
selectedTime,
}),
enabled:
query != null &&
(query.queryType !== EQueryType.PROM ||
(query.promQL?.length > 0 && query.promQL[0].query !== '')),
});
const chartDataSet = queryResponse.isError
? null
: getChartData({
queryData: [
{
queryData: queryResponse?.data?.payload?.data?.result
? queryResponse?.data?.payload?.data?.result
: [],
},
],
});
return (
<ChartContainer>
{headline}
{(queryResponse?.data?.error || queryResponse?.isError) && (
<FailedMessageContainer color="red" title="Failed to refresh the chart">
<InfoCircleOutlined />{' '}
{queryResponse?.data?.error ||
queryResponse?.error ||
t('preview_chart_unexpected_error')}
</FailedMessageContainer>
)}
{chartDataSet && !queryResponse.isError && (
<GridGraphComponent
title={name}
data={chartDataSet}
isStacked
GRAPH_TYPES={graphType || 'TIME_SERIES'}
name={name || 'Chart Preview'}
staticLine={staticLine}
/>
)}
</ChartContainer>
);
}
ChartPreview.defaultProps = {
graphType: 'TIME_SERIES',
selectedTime: 'GLOBAL_TIME',
selectedInterval: '5min',
headline: undefined,
threshold: 0,
};
export default ChartPreview;

View File

@@ -0,0 +1,28 @@
import { Card, Tooltip } from 'antd';
import styled from 'styled-components';
export const NotFoundContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
min-height: 55vh;
`;
export const FailedMessageContainer = styled(Tooltip)`
position: absolute;
top: 10px;
left: 10px;
`;
export const ChartContainer = styled(Card)`
border-radius: 4px;
&&& {
position: relative;
}
.ant-card-body {
padding: 1.5rem 0;
height: 57vh;
/* padding-bottom: 2rem; */
}
`;

View File

@@ -0,0 +1,49 @@
import PromQLQueryBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/query';
import { IPromQLQueryHandleChange } from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/types';
import React from 'react';
import { IPromQueries } from 'types/api/alerts/compositeQuery';
function PromqlSection({
promQueries,
setPromQueries,
}: PromqlSectionProps): JSX.Element {
const handlePromQLQueryChange = ({
query,
legend,
toggleDelete,
}: IPromQLQueryHandleChange): void => {
let promQuery = promQueries.A;
// todo(amol): how to remove query, make it null?
if (query) promQuery.query = query;
if (legend) promQuery.legend = legend;
if (toggleDelete) {
promQuery = {
query: '',
legend: '',
name: 'A',
disabled: false,
};
}
setPromQueries({
A: {
...promQuery,
},
});
};
return (
<PromQLQueryBuilder
key="A"
queryIndex="A"
queryData={{ ...promQueries?.A, name: 'A' }}
handleQueryChange={handlePromQLQueryChange}
/>
);
}
interface PromqlSectionProps {
promQueries: IPromQueries;
setPromQueries: (p: IPromQueries) => void;
}
export default PromqlSection;

View File

@@ -0,0 +1,288 @@
import { PlusOutlined } from '@ant-design/icons';
import { notification, Tabs } from 'antd';
import MetricsBuilderFormula from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/formula';
import MetricsBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/query';
import {
IQueryBuilderFormulaHandleChange,
IQueryBuilderQueryHandleChange,
} from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/types';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import {
IFormulaQueries,
IMetricQueries,
IPromQueries,
} from 'types/api/alerts/compositeQuery';
import { EAggregateOperator, EQueryType } from 'types/common/dashboard';
import PromqlSection from './PromqlSection';
import { FormContainer, QueryButton, StepHeading } from './styles';
import { toIMetricsBuilderQuery } from './utils';
const { TabPane } = Tabs;
function QuerySection({
queryCategory,
setQueryCategory,
metricQueries,
setMetricQueries,
formulaQueries,
setFormulaQueries,
promQueries,
setPromQueries,
}: QuerySectionProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('alerts');
const handleQueryCategoryChange = (s: string): void => {
if (
parseInt(s, 10) === EQueryType.PROM &&
(!promQueries || Object.keys(promQueries).length === 0)
) {
setPromQueries({
A: {
query: '',
stats: '',
name: 'A',
legend: '',
disabled: false,
},
});
}
setQueryCategory(parseInt(s, 10));
};
const getNextQueryLabel = useCallback((): string => {
let maxAscii = 0;
Object.keys(metricQueries).forEach((key) => {
const n = key.charCodeAt(0);
if (n > maxAscii) {
maxAscii = n - 64;
}
});
return String.fromCharCode(64 + maxAscii + 1);
}, [metricQueries]);
const handleFormulaChange = ({
formulaIndex,
expression,
toggleDisable,
toggleDelete,
}: IQueryBuilderFormulaHandleChange): void => {
const allFormulas = formulaQueries;
const current = allFormulas[formulaIndex];
if (expression) {
current.expression = expression;
}
if (toggleDisable) {
current.disabled = !current.disabled;
}
if (toggleDelete) {
delete allFormulas[formulaIndex];
} else {
allFormulas[formulaIndex] = current;
}
setFormulaQueries({
...allFormulas,
});
};
const handleMetricQueryChange = ({
queryIndex,
aggregateFunction,
metricName,
tagFilters,
groupBy,
legend,
toggleDisable,
toggleDelete,
}: IQueryBuilderQueryHandleChange): void => {
const allQueries = metricQueries;
const current = metricQueries[queryIndex];
if (aggregateFunction) {
current.aggregateOperator = aggregateFunction;
}
if (metricName) {
current.metricName = metricName;
}
if (tagFilters && current.tagFilters) {
current.tagFilters.items = tagFilters;
}
if (legend) {
current.legend = legend;
}
if (groupBy) {
current.groupBy = groupBy;
}
if (toggleDisable) {
current.disabled = !current.disabled;
}
if (toggleDelete) {
delete allQueries[queryIndex];
} else {
allQueries[queryIndex] = current;
}
setMetricQueries({
...allQueries,
});
};
const addMetricQuery = useCallback(() => {
if (Object.keys(metricQueries).length > 5) {
notification.error({
message: t('metric_query_max_limit'),
});
return;
}
const queryLabel = getNextQueryLabel();
const queries = metricQueries;
queries[queryLabel] = {
name: queryLabel,
queryName: queryLabel,
metricName: '',
formulaOnly: false,
aggregateOperator: EAggregateOperator.NOOP,
legend: '',
tagFilters: {
op: 'AND',
items: [],
},
groupBy: [],
disabled: false,
expression: queryLabel,
};
setMetricQueries({ ...queries });
}, [t, getNextQueryLabel, metricQueries, setMetricQueries]);
const addFormula = useCallback(() => {
// defaulting to F1 as only one formula is supported
// in alert definition
const queryLabel = 'F1';
const formulas = formulaQueries;
formulas[queryLabel] = {
queryName: queryLabel,
name: queryLabel,
formulaOnly: true,
expression: 'A',
disabled: false,
};
setFormulaQueries({ ...formulas });
}, [formulaQueries, setFormulaQueries]);
const renderPromqlUI = (): JSX.Element => {
return (
<PromqlSection promQueries={promQueries} setPromQueries={setPromQueries} />
);
};
const renderFormulaButton = (): JSX.Element => {
return (
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
{t('button_formula')}
</QueryButton>
);
};
const renderQueryButton = (): JSX.Element => {
return (
<QueryButton onClick={addMetricQuery} icon={<PlusOutlined />}>
{t('button_query')}
</QueryButton>
);
};
const renderMetricUI = (): JSX.Element => {
return (
<div>
{metricQueries &&
Object.keys(metricQueries).map((key: string) => {
// todo(amol): need to handle this in fetch
const current = metricQueries[key];
current.name = key;
return (
<MetricsBuilder
key={key}
queryIndex={key}
queryData={toIMetricsBuilderQuery(current)}
selectedGraph="TIME_SERIES"
handleQueryChange={handleMetricQueryChange}
/>
);
})}
{queryCategory !== EQueryType.PROM && renderQueryButton()}
<div style={{ marginTop: '1rem' }}>
{formulaQueries &&
Object.keys(formulaQueries).map((key: string) => {
// todo(amol): need to handle this in fetch
const current = formulaQueries[key];
current.name = key;
return (
<MetricsBuilderFormula
key={key}
formulaIndex={key}
formulaData={current}
handleFormulaChange={handleFormulaChange}
/>
);
})}
{queryCategory === EQueryType.QUERY_BUILDER &&
(!formulaQueries || Object.keys(formulaQueries).length === 0) &&
metricQueries &&
Object.keys(metricQueries).length > 0 &&
renderFormulaButton()}
</div>
</div>
);
};
return (
<>
<StepHeading> {t('alert_form_step1')}</StepHeading>
<FormContainer>
<div style={{ display: 'flex' }}>
<Tabs
type="card"
style={{ width: '100%' }}
defaultActiveKey={EQueryType.QUERY_BUILDER.toString()}
activeKey={queryCategory.toString()}
onChange={handleQueryCategoryChange}
>
<TabPane tab={t('tab_qb')} key={EQueryType.QUERY_BUILDER.toString()} />
<TabPane tab={t('tab_promql')} key={EQueryType.PROM.toString()} />
</Tabs>
</div>
{queryCategory === EQueryType.PROM ? renderPromqlUI() : renderMetricUI()}
</FormContainer>
</>
);
}
interface QuerySectionProps {
queryCategory: EQueryType;
setQueryCategory: (n: EQueryType) => void;
metricQueries: IMetricQueries;
setMetricQueries: (b: IMetricQueries) => void;
formulaQueries: IFormulaQueries;
setFormulaQueries: (b: IFormulaQueries) => void;
promQueries: IPromQueries;
setPromQueries: (p: IPromQueries) => void;
}
export default QuerySection;

View File

@@ -0,0 +1,175 @@
import { Select, Typography } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
AlertDef,
defaultCompareOp,
defaultEvalWindow,
defaultMatchType,
} from 'types/api/alerts/def';
import { EQueryType } from 'types/common/dashboard';
import {
FormContainer,
InlineSelect,
StepHeading,
ThresholdInput,
} from './styles';
const { Option } = Select;
function RuleOptions({
alertDef,
setAlertDef,
queryCategory,
}: RuleOptionsProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('alerts');
const handleMatchOptChange = (value: string | unknown): void => {
const m = (value as string) || alertDef.condition?.matchType;
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
matchType: m,
},
});
};
const renderCompareOps = (): JSX.Element => {
return (
<InlineSelect
defaultValue={defaultCompareOp}
value={alertDef.condition?.op}
style={{ minWidth: '120px' }}
onChange={(value: string | unknown): void => {
const newOp = (value as string) || '';
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
op: newOp,
},
});
}}
>
<Option value="1">{t('option_above')}</Option>
<Option value="2">{t('option_below')}</Option>
<Option value="3">{t('option_equal')}</Option>
<Option value="4">{t('option_notequal')}</Option>
</InlineSelect>
);
};
const renderThresholdMatchOpts = (): JSX.Element => {
return (
<InlineSelect
defaultValue={defaultMatchType}
style={{ minWidth: '130px' }}
value={alertDef.condition?.matchType}
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
>
<Option value="1">{t('option_atleastonce')}</Option>
<Option value="2">{t('option_allthetimes')}</Option>
<Option value="3">{t('option_onaverage')}</Option>
<Option value="4">{t('option_intotal')}</Option>
</InlineSelect>
);
};
const renderPromMatchOpts = (): JSX.Element => {
return (
<InlineSelect
defaultValue={defaultMatchType}
style={{ minWidth: '130px' }}
value={alertDef.condition?.matchType}
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
>
<Option value="1">{t('option_atleastonce')}</Option>
</InlineSelect>
);
};
const renderEvalWindows = (): JSX.Element => {
return (
<InlineSelect
defaultValue={defaultEvalWindow}
style={{ minWidth: '120px' }}
value={alertDef.evalWindow}
onChange={(value: string | unknown): void => {
const ew = (value as string) || alertDef.evalWindow;
setAlertDef({
...alertDef,
evalWindow: ew,
});
}}
>
{' '}
<Option value="5m0s">{t('option_5min')}</Option>
<Option value="10m0s">{t('option_10min')}</Option>
<Option value="15m0s">{t('option_15min')}</Option>
<Option value="60m0s">{t('option_60min')}</Option>
<Option value="4h0m0s">{t('option_4hours')}</Option>
<Option value="24h0m0s">{t('option_24hours')}</Option>
</InlineSelect>
);
};
const renderThresholdRuleOpts = (): JSX.Element => {
return (
<FormItem>
<Typography.Text>
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
{renderThresholdMatchOpts()} {t('text_condition3')} {renderEvalWindows()}
</Typography.Text>
</FormItem>
);
};
const renderPromRuleOptions = (): JSX.Element => {
return (
<FormItem>
<Typography.Text>
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
{renderPromMatchOpts()}
</Typography.Text>
</FormItem>
);
};
return (
<>
<StepHeading>{t('alert_form_step2')}</StepHeading>
<FormContainer>
{queryCategory === EQueryType.PROM
? renderPromRuleOptions()
: renderThresholdRuleOpts()}
<div style={{ display: 'flex', alignItems: 'center' }}>
<ThresholdInput
controls={false}
addonBefore={t('field_threshold')}
value={alertDef?.condition?.target}
onChange={(value: number | unknown): void => {
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
target: (value as number) || undefined,
},
});
}}
/>
</div>
</FormContainer>
</>
);
}
interface RuleOptionsProps {
alertDef: AlertDef;
setAlertDef: (a: AlertDef) => void;
queryCategory: EQueryType;
}
export default RuleOptions;

View File

@@ -0,0 +1,132 @@
import { Col, Row, Typography } from 'antd';
import TextToolTip from 'components/TextToolTip';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { EQueryType } from 'types/common/dashboard';
import {
StyledList,
StyledListItem,
StyledMainContainer,
StyledTopic,
} from './styles';
function UserGuide({ queryType }: UserGuideProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('alerts');
const renderStep1QB = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_qb_step1')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step1a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1b')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1c')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step1d')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep2QB = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_qb_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step2b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep3QB = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_qb_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_qb_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_qb_step3b')}</StyledListItem>
</StyledList>
</>
);
};
const renderGuideForQB = (): JSX.Element => {
return (
<>
{renderStep1QB()}
{renderStep2QB()}
{renderStep3QB()}
</>
);
};
const renderStep1PQL = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_pql_step1')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step1a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step1b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep2PQL = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_pql_step2')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step2a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step2b')}</StyledListItem>
</StyledList>
</>
);
};
const renderStep3PQL = (): JSX.Element => {
return (
<>
<StyledTopic>{t('user_guide_pql_step3')}</StyledTopic>
<StyledList>
<StyledListItem>{t('user_guide_pql_step3a')}</StyledListItem>
<StyledListItem>{t('user_guide_pql_step3b')}</StyledListItem>
</StyledList>
</>
);
};
const renderGuideForPQL = (): JSX.Element => {
return (
<>
{renderStep1PQL()}
{renderStep2PQL()}
{renderStep3PQL()}
</>
);
};
return (
<StyledMainContainer>
<Row>
<Col flex="auto">
<Typography.Paragraph> {t('user_guide_headline')} </Typography.Paragraph>
</Col>
<Col flex="none">
<TextToolTip
text={t('user_tooltip_more_help')}
url="https://signoz.io/docs/userguide/alerts-management/#create-alert-rules"
/>
</Col>
</Row>
{queryType === EQueryType.QUERY_BUILDER && renderGuideForQB()}
{queryType === EQueryType.PROM && renderGuideForPQL()}
</StyledMainContainer>
);
}
interface UserGuideProps {
queryType: EQueryType;
}
export default UserGuide;

View File

@@ -0,0 +1,17 @@
import { Card, Typography } from 'antd';
import styled from 'styled-components';
export const StyledMainContainer = styled(Card)``;
export const StyledTopic = styled(Typography.Paragraph)`
font-weight: 600;
`;
export const StyledList = styled.ul`
padding-left: 18px;
`;
export const StyledListItem = styled.li`
font-style: italic;
padding-bottom: 0.5rem;
`;

View File

@@ -0,0 +1,381 @@
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
import { FormInstance, Modal, notification, Typography } from 'antd';
import saveAlertApi from 'api/alerts/save';
import ROUTES from 'constants/routes';
import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
import history from 'lib/history';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
import {
IFormulaQueries,
IMetricQueries,
IPromQueries,
} from 'types/api/alerts/compositeQuery';
import {
AlertDef,
defaultEvalWindow,
defaultMatchType,
} from 'types/api/alerts/def';
import { Query as StagedQuery } from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard';
import BasicInfo from './BasicInfo';
import ChartPreview from './ChartPreview';
import QuerySection from './QuerySection';
import RuleOptions from './RuleOptions';
import {
ActionButton,
ButtonContainer,
MainFormContainer,
PanelContainer,
StyledLeftContainer,
StyledRightContainer,
} from './styles';
import useDebounce from './useDebounce';
import UserGuide from './UserGuide';
import {
prepareBuilderQueries,
prepareStagedQuery,
toChartInterval,
toFormulaQueries,
toMetricQueries,
} from './utils';
function FormAlertRules({
formInstance,
initialValue,
ruleId,
}: FormAlertRuleProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('alerts');
// use query client
const ruleCache = useQueryClient();
const [loading, setLoading] = useState(false);
// alertDef holds the form values to be posted
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
// initQuery contains initial query when component was mounted
const initQuery = initialValue?.condition?.compositeMetricQuery;
const [queryCategory, setQueryCategory] = useState<EQueryType>(
initQuery?.queryType,
);
// local state to handle metric queries
const [metricQueries, setMetricQueries] = useState<IMetricQueries>(
toMetricQueries(initQuery?.builderQueries),
);
// local state to handle formula queries
const [formulaQueries, setFormulaQueries] = useState<IFormulaQueries>(
toFormulaQueries(initQuery?.builderQueries),
);
// local state to handle promql queries
const [promQueries, setPromQueries] = useState<IPromQueries>({
...initQuery?.promQueries,
});
// staged query is used to display chart preview
const [stagedQuery, setStagedQuery] = useState<StagedQuery>();
const debouncedStagedQuery = useDebounce(stagedQuery, 500);
// this use effect initiates staged query and
// other queries based on server data.
// useful when fetching of initial values (from api)
// is delayed
useEffect(() => {
const initQuery = initialValue?.condition?.compositeMetricQuery;
const typ = initQuery?.queryType;
// extract metric query from builderQueries
const mq = toMetricQueries(initQuery?.builderQueries);
// extract formula query from builderQueries
const fq = toFormulaQueries(initQuery?.builderQueries);
// prepare staged query
const sq = prepareStagedQuery(typ, mq, fq, initQuery?.promQueries);
const pq = initQuery?.promQueries;
setQueryCategory(typ);
setMetricQueries(mq);
setFormulaQueries(fq);
setPromQueries(pq);
setStagedQuery(sq);
setAlertDef(initialValue);
}, [initialValue]);
// this useEffect updates staging query when
// any of its sub-parameters changes
useEffect(() => {
// prepare staged query
const sq: StagedQuery = prepareStagedQuery(
queryCategory,
metricQueries,
formulaQueries,
promQueries,
);
setStagedQuery(sq);
}, [queryCategory, metricQueries, formulaQueries, promQueries]);
const onCancelHandler = useCallback(() => {
history.replace(ROUTES.LIST_ALL_ALERT);
}, []);
// onQueryCategoryChange handles changes to query category
// in state as well as sets additional defaults
const onQueryCategoryChange = (val: EQueryType): void => {
setQueryCategory(val);
if (val === EQueryType.PROM) {
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
matchType: defaultMatchType,
},
evalWindow: defaultEvalWindow,
});
}
};
const isFormValid = useCallback((): boolean => {
let retval = true;
if (!alertDef.alert || alertDef.alert === '') {
notification.error({
message: 'Error',
description: t('alertname_required'),
});
return false;
}
if (
queryCategory === EQueryType.PROM &&
(!promQueries || Object.keys(promQueries).length === 0)
) {
notification.error({
message: 'Error',
description: t('promql_required'),
});
return false;
}
if (
(queryCategory === EQueryType.QUERY_BUILDER && !metricQueries) ||
Object.keys(metricQueries).length === 0
) {
notification.error({
message: 'Error',
description: t('condition_required'),
});
return false;
}
Object.keys(metricQueries).forEach((key) => {
if (metricQueries[key].metricName === '') {
retval = false;
notification.error({
message: 'Error',
description: t('metricname_missing', { where: metricQueries[key].name }),
});
}
});
Object.keys(formulaQueries).forEach((key) => {
if (formulaQueries[key].expression === '') {
retval = false;
notification.error({
message: 'Error',
description: t('expression_missing', formulaQueries[key].name),
});
}
});
return retval;
}, [t, alertDef, queryCategory, metricQueries, formulaQueries, promQueries]);
const saveRule = useCallback(async () => {
if (!isFormValid()) {
return;
}
const postableAlert: AlertDef = {
...alertDef,
source: window?.location.toString(),
ruleType:
queryCategory === EQueryType.PROM ? 'promql_rule' : 'threshold_rule',
condition: {
...alertDef.condition,
compositeMetricQuery: {
builderQueries: prepareBuilderQueries(metricQueries, formulaQueries),
promQueries,
queryType: queryCategory,
},
},
};
setLoading(true);
try {
const apiReq =
ruleId && ruleId > 0
? { data: postableAlert, id: ruleId }
: { data: postableAlert };
const response = await saveAlertApi(apiReq);
if (response.statusCode === 200) {
notification.success({
message: 'Success',
description:
!ruleId || ruleId === 0 ? t('rule_created') : t('rule_edited'),
});
console.log('invalidting cache');
// invalidate rule in cache
ruleCache.invalidateQueries(['ruleId', ruleId]);
setTimeout(() => {
history.replace(ROUTES.LIST_ALL_ALERT);
}, 2000);
} else {
notification.error({
message: 'Error',
description: response.error || t('unexpected_error'),
});
}
} catch (e) {
console.log('save alert api failed:', e);
notification.error({
message: 'Error',
description: t('unexpected_error'),
});
}
setLoading(false);
}, [
t,
isFormValid,
queryCategory,
ruleId,
alertDef,
metricQueries,
formulaQueries,
promQueries,
ruleCache,
]);
const onSaveHandler = useCallback(async () => {
const content = (
<Typography.Text>
{' '}
{t('confirm_save_content_part1')} <QueryTypeTag queryType={queryCategory} />{' '}
{t('confirm_save_content_part2')}
</Typography.Text>
);
Modal.confirm({
icon: <ExclamationCircleOutlined />,
title: t('confirm_save_title'),
centered: true,
content,
onOk() {
saveRule();
},
});
}, [t, saveRule, queryCategory]);
const renderBasicInfo = (): JSX.Element => (
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
);
const renderQBChartPreview = (): JSX.Element => {
return (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name=""
threshold={alertDef.condition?.target}
query={debouncedStagedQuery}
selectedInterval={toChartInterval(alertDef.evalWindow)}
/>
);
};
const renderPromChartPreview = (): JSX.Element => {
return (
<ChartPreview
headline={<PlotTag queryType={queryCategory} />}
name="Chart Preview"
threshold={alertDef.condition?.target}
query={debouncedStagedQuery}
/>
);
};
return (
<>
{Element}
<PanelContainer>
<StyledLeftContainer flex="5 1 600px">
<MainFormContainer
initialValues={initialValue}
layout="vertical"
form={formInstance}
>
{queryCategory === EQueryType.QUERY_BUILDER && renderQBChartPreview()}
{queryCategory === EQueryType.PROM && renderPromChartPreview()}
<QuerySection
queryCategory={queryCategory}
setQueryCategory={onQueryCategoryChange}
metricQueries={metricQueries}
setMetricQueries={setMetricQueries}
formulaQueries={formulaQueries}
setFormulaQueries={setFormulaQueries}
promQueries={promQueries}
setPromQueries={setPromQueries}
/>
<RuleOptions
queryCategory={queryCategory}
alertDef={alertDef}
setAlertDef={setAlertDef}
/>
{renderBasicInfo()}
<ButtonContainer>
<ActionButton
loading={loading || false}
type="primary"
onClick={onSaveHandler}
icon={<SaveOutlined />}
>
{ruleId > 0 ? t('button_savechanges') : t('button_createrule')}
</ActionButton>
<ActionButton
disabled={loading || false}
type="default"
onClick={onCancelHandler}
>
{ruleId === 0 && t('button_cancelchanges')}
{ruleId > 0 && t('button_discard')}
</ActionButton>
</ButtonContainer>
</MainFormContainer>
</StyledLeftContainer>
<StyledRightContainer flex="1 1 300px">
<UserGuide queryType={queryCategory} />
</StyledRightContainer>
</PanelContainer>
</>
);
}
interface FormAlertRuleProps {
formInstance: FormInstance;
initialValue: AlertDef;
ruleId: number;
}
export default FormAlertRules;

View File

@@ -0,0 +1,49 @@
import { createMachine } from 'xstate';
export const ResourceAttributesFilterMachine =
/** @xstate-layout N4IgpgJg5mDOIC5QBECGsAWAjA9qgThAAQDKYBAxhkQIIB2xAYgJYA2ALmPgHQAqqUANJgAngGIAcgFEAGr0SgADjljN2zHHQUgAHogAcAFgAM3AOz6ATAEYAzJdsA2Y4cOWAnABoQIxAFpDR2tuQ319AFYTcKdbFycAX3jvNExcAmIySmp6JjZOHn4hUTFNACFWAFd8bWVVdU1tPQQzY1MXY2tDdzNHM3dHd0NvXwR7biMTa313S0i+63DE5PRsPEJScnwqWgYiFg4uPgFhcQAlKRIpeSQQWrUNLRumx3Czbg8TR0sbS31jfUcw38fW47gBHmm4XCVms3SWIBSq3SGyyO1yBx4AHlFFxUOwcPhJLJrkoVPcGk9ENYFuF3i5YR0wtEHECEAEgiEmV8zH1DLYzHZ4Yi0utMltsrt9vluNjcfjCWVKtUbnd6o9QE1rMYBtxbGFvsZ3NrZj1WdYOfotUZLX0XEFHEKViKMpttjk9nlDrL8HiCWJzpcSbcyWrGoh3NCQj0zK53P1ph1WeFLLqnJZ2s5vmZLA6kginWsXaj3VLDoUAGqoSpgEp0cpVGohh5hhDWDy0sz8zruakzamWVm-Qyg362V5-AZOayO1KFlHitEejFHKCV6v+i5XRt1ZuU1s52zjNOOaZfdOWIY+RDZ0Hc6ZmKEXqyLPPCudit2Sz08ACSEFYNbSHI27kuquiIOEjiONwjJgrM3RWJYZisgEIJgnYPTmuEdi2OaiR5nQOAQHA2hvsiH4Sui0qFCcIGhnuLSmP0YJuJ2xjJsmKELG8XZTK0tjdHG06vgW5GupRS7St6vrKqSO4UhqVL8TBWp8o4eqdl0A5Xmy3G6gK56-B4uERDOSKiuJi6lgUAhrhUYB0buimtrEKZBDYrxaS0OZca8+ltheybOI4hivGZzrzp+VGHH+AGOQp4EIHy+ghNYnawtG4TsbYvk8QKfHGAJfQ9uF76WSW37xWBTSGJ0qXpd0vRZdEKGPqC2YeO2-zfO4+HxEAA */
createMachine({
tsTypes: {} as import('./Labels.machine.typegen').Typegen0,
initial: 'Idle',
states: {
LabelKey: {
on: {
NEXT: {
actions: 'onSelectLabelValue',
target: 'LabelValue',
},
onBlur: {
actions: 'onSelectLabelValue',
target: 'LabelValue',
},
RESET: {
target: 'Idle',
},
},
},
LabelValue: {
on: {
NEXT: {
actions: ['onValidateQuery'],
},
onBlur: {
actions: ['onValidateQuery'],
// target: 'Idle',
},
RESET: {
target: 'Idle',
},
},
},
Idle: {
on: {
NEXT: {
actions: 'onSelectLabelKey',
description: 'Enter a label key',
target: 'LabelKey',
},
},
},
},
id: 'Label Key Values',
});

View File

@@ -0,0 +1,25 @@
// This file was automatically generated. Edits will be overwritten
export interface Typegen0 {
'@@xstate/typegen': true;
eventsCausingActions: {
onSelectLabelValue: 'NEXT' | 'onBlur';
onValidateQuery: 'NEXT' | 'onBlur';
onSelectLabelKey: 'NEXT';
};
internalEvents: {
'xstate.init': { type: 'xstate.init' };
};
invokeSrcNameMap: {};
missingImplementations: {
actions: 'onSelectLabelValue' | 'onValidateQuery' | 'onSelectLabelKey';
services: never;
guards: never;
delays: never;
};
eventsCausingServices: {};
eventsCausingGuards: {};
eventsCausingDelays: {};
matchesStates: 'LabelKey' | 'LabelValue' | 'Idle';
tags: never;
}

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { QueryChipContainer, QueryChipItem } from './styles';
import { ILabelRecord } from './types';
interface QueryChipProps {
queryData: ILabelRecord;
onRemove: (id: string) => void;
}
export default function QueryChip({
queryData,
onRemove,
}: QueryChipProps): JSX.Element {
const { key, value } = queryData;
return (
<QueryChipContainer>
<QueryChipItem
closable={key !== 'severity' && key !== 'description'}
onClose={(): void => onRemove(key)}
>
{key}: {value}
</QueryChipItem>
</QueryChipContainer>
);
}

View File

@@ -0,0 +1,164 @@
import {
CloseCircleFilled,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import { useMachine } from '@xstate/react';
import { Button, Input, message, Modal } from 'antd';
import { map } from 'lodash-es';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { Labels } from 'types/api/alerts/def';
import AppReducer from 'types/reducer/app';
import { v4 as uuid } from 'uuid';
import { ResourceAttributesFilterMachine } from './Labels.machine';
import QueryChip from './QueryChip';
import { QueryChipItem, SearchContainer } from './styles';
import { ILabelRecord } from './types';
import { createQuery, flattenLabels, prepareLabels } from './utils';
interface LabelSelectProps {
onSetLabels: (q: Labels) => void;
initialValues: Labels | undefined;
}
function LabelSelect({
onSetLabels,
initialValues,
}: LabelSelectProps): JSX.Element | null {
const { t } = useTranslation('alerts');
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const [currentVal, setCurrentVal] = useState('');
const [staging, setStaging] = useState<string[]>([]);
const [queries, setQueries] = useState<ILabelRecord[]>(
initialValues ? flattenLabels(initialValues) : [],
);
const dispatchChanges = (updatedRecs: ILabelRecord[]): void => {
onSetLabels(prepareLabels(updatedRecs, initialValues));
setQueries(updatedRecs);
};
const [state, send] = useMachine(ResourceAttributesFilterMachine, {
actions: {
onSelectLabelKey: () => {},
onSelectLabelValue: () => {
if (currentVal !== '') {
setStaging((prevState) => [...prevState, currentVal]);
} else {
return;
}
setCurrentVal('');
},
onValidateQuery: (): void => {
if (currentVal === '') {
return;
}
const generatedQuery = createQuery([...staging, currentVal]);
if (generatedQuery) {
dispatchChanges([...queries, generatedQuery]);
setStaging([]);
setCurrentVal('');
send('RESET');
}
},
},
});
const handleFocus = (): void => {
if (state.value === 'Idle') {
send('NEXT');
}
};
const handleBlur = useCallback((): void => {
if (staging.length === 1 && staging[0] !== undefined) {
send('onBlur');
}
}, [send, staging]);
useEffect(() => {
handleBlur();
}, [handleBlur]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
setCurrentVal(e.target?.value);
};
const handleClose = (key: string): void => {
dispatchChanges(queries.filter((queryData) => queryData.key !== key));
};
const handleClearAll = (): void => {
Modal.confirm({
title: 'Confirm',
icon: <ExclamationCircleOutlined />,
content: t('remove_label_confirm'),
onOk() {
send('RESET');
dispatchChanges([]);
setStaging([]);
message.success(t('remove_label_success'));
},
okText: t('button_yes'),
cancelText: t('button_no'),
});
};
const renderPlaceholder = useCallback((): string => {
if (state.value === 'LabelKey') return 'Enter a label key then press ENTER.';
if (state.value === 'LabelValue')
return `Enter a value for label key(${staging[0]}) then press ENTER.`;
return t('placeholder_label_key_pair');
}, [t, state, staging]);
return (
<SearchContainer isDarkMode={isDarkMode} disabled={false}>
<div style={{ display: 'inline-flex', flexWrap: 'wrap' }}>
{queries.length > 0 &&
map(
queries,
(query): JSX.Element => {
return (
<QueryChip key={query.key} queryData={query} onRemove={handleClose} />
);
},
)}
</div>
<div>
{map(staging, (item) => {
return <QueryChipItem key={uuid()}>{item}</QueryChipItem>;
})}
</div>
<div style={{ display: 'flex', width: '100%' }}>
<Input
placeholder={renderPlaceholder()}
onChange={handleChange}
onKeyUp={(e): void => {
if (e.key === 'Enter' || e.code === 'Enter') {
send('NEXT');
}
}}
bordered={false}
value={currentVal as never}
style={{ flex: 1 }}
onFocus={handleFocus}
onBlur={handleBlur}
/>
{queries.length || staging.length || currentVal ? (
<Button
onClick={handleClearAll}
icon={<CloseCircleFilled />}
type="text"
/>
) : null}
</div>
</SearchContainer>
);
}
export default LabelSelect;

View File

@@ -0,0 +1,35 @@
import { grey } from '@ant-design/colors';
import { Tag } from 'antd';
import styled from 'styled-components';
interface SearchContainerProps {
isDarkMode: boolean;
disabled: boolean;
}
export const SearchContainer = styled.div<SearchContainerProps>`
width: 70%;
border-radisu: 4px;
background: ${({ isDarkMode }): string => (isDarkMode ? '#000' : '#fff')};
flex: 1;
display: flex;
flex-direction: column;
padding: 0.2rem;
border: 1px solid #ccc5;
${({ disabled }): string => (disabled ? `cursor: not-allowed;` : '')}
`;
export const QueryChipContainer = styled.span`
display: flex;
align-items: center;
margin-right: 0.5rem;
&:hover {
& > * {
background: ${grey.primary}44;
}
}
`;
export const QueryChipItem = styled(Tag)`
margin-right: 0.1rem;
`;

View File

@@ -0,0 +1,9 @@
export interface ILabelRecord {
key: string;
value: string;
}
export interface IOption {
label: string;
value: string;
}

View File

@@ -0,0 +1,54 @@
import { Labels } from 'types/api/alerts/def';
import { ILabelRecord } from './types';
const hiddenLabels = ['severity', 'description'];
export const createQuery = (
selectedItems: Array<string | string[]> = [],
): ILabelRecord | null => {
if (selectedItems.length === 2) {
return {
key: selectedItems[0] as string,
value: selectedItems[1] as string,
};
}
return null;
};
export const flattenLabels = (labels: Labels): ILabelRecord[] => {
const recs: ILabelRecord[] = [];
Object.keys(labels).forEach((key) => {
if (!hiddenLabels.includes(key)) {
recs.push({
key,
value: labels[key],
});
}
});
return recs;
};
export const prepareLabels = (
recs: ILabelRecord[],
alertLabels: Labels | undefined,
): Labels => {
const labels: Labels = {};
recs.forEach((rec) => {
if (!hiddenLabels.includes(rec.key)) {
labels[rec.key] = rec.value;
}
});
if (alertLabels) {
Object.keys(alertLabels).forEach((key) => {
if (hiddenLabels.includes(key)) {
labels[key] = alertLabels[key];
}
});
}
return labels;
};

View File

@@ -0,0 +1,101 @@
import { Button, Card, Col, Form, Input, InputNumber, Row, Select } from 'antd';
import TextArea from 'antd/lib/input/TextArea';
import styled from 'styled-components';
export const PanelContainer = styled(Row)`
flex-wrap: nowrap;
`;
export const StyledRightContainer = styled(Col)`
&&& {
}
`;
export const StyledLeftContainer = styled(Col)`
&&& {
margin-right: 1rem;
}
`;
export const MainFormContainer = styled(Form)``;
export const ButtonContainer = styled.div`
&&& {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 1rem;
margin-bottom: 3rem;
}
`;
export const ActionButton = styled(Button)`
margin-right: 1rem;
`;
export const QueryButton = styled(Button)`
&&& {
display: flex;
align-items: center;
margin-right: 1rem;
}
`;
export const QueryContainer = styled(Card)`
&&& {
margin-top: 1rem;
min-height: 23.5%;
}
`;
export const Container = styled.div`
margin-top: 1rem;
display: flex;
flex-direction: column;
`;
export const StepHeading = styled.p`
margin-top: 1rem;
font-weight: bold;
`;
export const InlineSelect = styled(Select)`
display: inline-block;
width: 10% !important;
margin-left: 0.2em;
margin-right: 0.2em;
`;
export const SeveritySelect = styled(Select)`
width: 15% !important;
`;
export const InputSmall = styled(Input)`
width: 40% !important;
`;
export const FormContainer = styled(Card)`
padding: 2em;
margin-top: 1rem;
display: flex;
flex-direction: column;
border-radius: 4px;
`;
export const ThresholdInput = styled(InputNumber)`
& > div {
display: flex;
align-items: center;
& > .ant-input-number-group-addon {
width: 130px;
}
& > .ant-input-number {
width: 50%;
margin-left: 1em;
}
}
`;
export const TextareaMedium = styled(TextArea)`
width: 70%;
`;

View File

@@ -0,0 +1,31 @@
/* eslint-disable */
// @ts-ignore
// @ts-nocheck
import { useEffect, useState } from 'react';
// see https://github.com/tannerlinsley/react-query/issues/293
// see https://usehooks.com/useDebounce/
export default function useDebounce(value, delay) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
}

View File

@@ -0,0 +1,136 @@
import { Time } from 'container/TopNav/DateTimeSelection/config';
import {
IBuilderQueries,
IFormulaQueries,
IFormulaQuery,
IMetricQueries,
IMetricQuery,
IPromQueries,
IPromQuery,
} from 'types/api/alerts/compositeQuery';
import {
IMetricsBuilderQuery,
Query as IStagedQuery,
} from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard';
export const toFormulaQueries = (b: IBuilderQueries): IFormulaQueries => {
const f: IFormulaQueries = {};
if (!b) return f;
Object.keys(b).forEach((key) => {
if (key === 'F1') {
f[key] = b[key] as IFormulaQuery;
}
});
return f;
};
export const toMetricQueries = (b: IBuilderQueries): IMetricQueries => {
const m: IMetricQueries = {};
if (!b) return m;
Object.keys(b).forEach((key) => {
if (key !== 'F1') {
m[key] = b[key] as IMetricQuery;
}
});
return m;
};
export const toIMetricsBuilderQuery = (
q: IMetricQuery,
): IMetricsBuilderQuery => {
return {
name: q.name,
metricName: q.metricName,
tagFilters: q.tagFilters,
groupBy: q.groupBy,
aggregateOperator: q.aggregateOperator,
disabled: q.disabled,
legend: q.legend,
};
};
export const prepareBuilderQueries = (
m: IMetricQueries,
f: IFormulaQueries,
): IBuilderQueries => {
if (!m) return {};
const b: IBuilderQueries = {
...m,
};
Object.keys(f).forEach((key) => {
b[key] = {
...f[key],
aggregateOperator: undefined,
metricName: '',
};
});
return b;
};
export const prepareStagedQuery = (
t: EQueryType,
m: IMetricQueries,
f: IFormulaQueries,
p: IPromQueries,
): IStagedQuery => {
const qbList: IMetricQuery[] = [];
const formulaList: IFormulaQuery[] = [];
const promList: IPromQuery[] = [];
// convert map[string]IMetricQuery to IMetricQuery[]
if (m) {
Object.keys(m).forEach((key) => {
qbList.push(m[key]);
});
}
// convert map[string]IFormulaQuery to IFormulaQuery[]
if (f) {
Object.keys(f).forEach((key) => {
formulaList.push(f[key]);
});
}
// convert map[string]IPromQuery to IPromQuery[]
if (p) {
Object.keys(p).forEach((key) => {
promList.push({ ...p[key], name: key });
});
}
return {
queryType: t,
promQL: promList,
metricsBuilder: {
formulas: formulaList,
queryBuilder: qbList,
},
clickHouse: [],
};
};
// toChartInterval converts eval window to chart selection time interval
export const toChartInterval = (evalWindow: string | undefined): Time => {
switch (evalWindow) {
case '5m0s':
return '5min';
case '10m0s':
return '10min';
case '15m0s':
return '15min';
case '30m0s':
return '30min';
case '60m0s':
return '30min';
case '4h0m0s':
return '4hr';
case '24h0m0s':
return '1day';
default:
return '5min';
}
};

View File

@@ -10,7 +10,7 @@ function SpanNameComponent({
<Container title={`${name} ${serviceName}`}>
<SpanWrapper>
<Span ellipsis>{name}</Span>
<Service>{serviceName}</Service>
<Service ellipsis>{serviceName}</Service>
</SpanWrapper>
</Container>
);

View File

@@ -9,7 +9,7 @@ export const Span = styled(Typography.Paragraph)`
}
`;
export const Service = styled(Typography)`
export const Service = styled(Typography.Paragraph)`
&&& {
color: #acacac;
font-size: 0.75rem;

View File

@@ -39,6 +39,7 @@ function Trace(props: TraceProps): JSX.Element {
isExpandAll,
intervalUnit,
children,
isMissing,
} = props;
const { isDarkMode } = useThemeMode();
@@ -125,7 +126,7 @@ function Trace(props: TraceProps): JSX.Element {
isDarkMode={isDarkMode}
/>
<CardContainer onClick={onClick}>
<CardContainer isMissing={isMissing} onClick={onClick}>
<StyledCol flex={`${panelWidth}px`} styledclass={[styles.overFlowHidden]}>
<StyledRow styledclass={[styles.flexNoWrap]}>
<Col>
@@ -174,6 +175,7 @@ function Trace(props: TraceProps): JSX.Element {
activeSpanPath={activeSpanPath}
isExpandAll={isExpandAll}
intervalUnit={intervalUnit}
isMissing={child.isMissing}
/>
))}
</>
@@ -182,6 +184,10 @@ function Trace(props: TraceProps): JSX.Element {
);
}
Trace.defaultProps = {
isMissing: false,
};
interface ITraceGlobal {
globalSpread: ITraceMetaData['spread'];
globalStart: ITraceMetaData['globalStart'];
@@ -196,6 +202,7 @@ interface TraceProps extends ITraceTree, ITraceGlobal {
activeSpanPath: string[];
isExpandAll: boolean;
intervalUnit: IIntervalUnit;
isMissing?: boolean;
}
export default Trace;

View File

@@ -1,3 +1,4 @@
import { volcano } from '@ant-design/colors';
import styled, {
css,
DefaultTheme,
@@ -15,7 +16,6 @@ export const Wrapper = styled.ul<Props>`
padding-top: 0.5rem;
position: relative;
z-index: 1;
ul {
border-left: ${({ isOnlyChild }): StyledCSS =>
isOnlyChild && 'none'} !important;
@@ -36,10 +36,14 @@ export const Wrapper = styled.ul<Props>`
}
`;
export const CardContainer = styled.li`
export const CardContainer = styled.li<{ isMissing?: boolean }>`
display: flex;
width: 100%;
cursor: pointer;
border-radius: 0.25rem;
z-index: 2;
${({ isMissing }): string =>
isMissing ? `border: 1px dashed ${volcano[6]} !important;` : ''}
`;
interface Props {

View File

@@ -3,7 +3,7 @@ import { IIntervalUnit } from 'container/TraceDetail/utils';
import React, { useEffect, useState } from 'react';
import { ITraceTree } from 'types/api/trace/getTraceItem';
import { CardContainer, CardWrapper, CollapseButton, Wrapper } from './styles';
import { CardContainer, CardWrapper, CollapseButton } from './styles';
import Trace from './Trace';
import { getSpanPath } from './utils';
@@ -36,35 +36,33 @@ function GanttChart(props: GanttChartProps): JSX.Element {
setIsExpandAll((prev) => !prev);
};
return (
<Wrapper>
<CardContainer>
<CollapseButton
onClick={handleCollapse}
title={isExpandAll ? 'Collapse All' : 'Expand All'}
>
{isExpandAll ? <MinusSquareOutlined /> : <PlusSquareOutlined />}
</CollapseButton>
<CardWrapper>
<Trace
activeHoverId={activeHoverId}
activeSpanPath={activeSpanPath}
setActiveHoverId={setActiveHoverId}
key={data.id}
// eslint-disable-next-line react/jsx-props-no-spreading
{...{
...data,
globalSpread,
globalStart,
setActiveSelectedId,
activeSelectedId,
}}
level={0}
isExpandAll={isExpandAll}
intervalUnit={intervalUnit}
/>
</CardWrapper>
</CardContainer>
</Wrapper>
<CardContainer>
<CollapseButton
onClick={handleCollapse}
title={isExpandAll ? 'Collapse All' : 'Expand All'}
>
{isExpandAll ? <MinusSquareOutlined /> : <PlusSquareOutlined />}
</CollapseButton>
<CardWrapper>
<Trace
activeHoverId={activeHoverId}
activeSpanPath={activeSpanPath}
setActiveHoverId={setActiveHoverId}
key={data.id}
// eslint-disable-next-line react/jsx-props-no-spreading
{...{
...data,
globalSpread,
globalStart,
setActiveSelectedId,
activeSelectedId,
}}
level={0}
isExpandAll={isExpandAll}
intervalUnit={intervalUnit}
/>
</CardWrapper>
</CardContainer>
);
}

View File

@@ -38,6 +38,7 @@ export const CardWrapper = styled.div`
export const CardContainer = styled.li`
display: flex;
width: 100%;
position: relative;
`;
export const CollapseButton = styled.div`

View File

@@ -1,4 +1,5 @@
import { ITraceTree } from 'types/api/trace/getTraceItem';
import { set } from 'lodash-es';
import { ITraceForest, ITraceTree } from 'types/api/trace/getTraceItem';
interface GetTraceMetaData {
globalStart: number;
@@ -65,25 +66,48 @@ export function getTopLeftFromBody(
export const getNodeById = (
searchingId: string,
treeData: ITraceTree,
): ITraceTree | undefined => {
let foundNode: ITraceTree | undefined;
const traverse = (treeNode: ITraceTree, level = 0): void => {
treesData: ITraceForest | undefined,
): ITraceForest => {
const newtreeData: ITraceForest = {} as ITraceForest;
const traverse = (
treeNode: ITraceTree,
setCallBack: (arg0: ITraceTree) => void,
level = 0,
): void => {
if (!treeNode) {
return;
}
if (searchingId === treeNode.id) {
foundNode = treeNode;
setCallBack(treeNode);
}
treeNode.children.forEach((childNode) => {
traverse(childNode, level + 1);
traverse(childNode, setCallBack, level + 1);
});
};
traverse(treeData, 1);
return foundNode;
const spanTreeSetCallback = (
path: (keyof ITraceForest)[],
value: ITraceTree,
): ITraceForest => set(newtreeData, path, [value]);
if (treesData?.spanTree)
treesData.spanTree.forEach((tree) => {
traverse(tree, (value) => spanTreeSetCallback(['spanTree'], value), 1);
});
if (treesData?.missingSpanTree)
treesData.missingSpanTree.forEach((tree) => {
traverse(
tree,
(value) => spanTreeSetCallback(['missingSpanTree'], value),
1,
);
});
return newtreeData;
};
const getSpanWithoutChildren = (

View File

@@ -1,6 +1,6 @@
import { Typography } from 'antd';
import { ChartData } from 'chart.js';
import Graph, { GraphOnClickHandler } from 'components/Graph';
import Graph, { GraphOnClickHandler, StaticLineProps } from 'components/Graph';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import ValueGraph from 'components/ValueGraph';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
@@ -18,6 +18,7 @@ function GridGraphComponent({
onClickHandler,
name,
yAxisUnit,
staticLine,
}: GridGraphComponentProps): JSX.Element | null {
const location = history.location.pathname;
@@ -36,6 +37,7 @@ function GridGraphComponent({
onClickHandler,
name,
yAxisUnit,
staticLine,
}}
/>
);
@@ -82,6 +84,7 @@ export interface GridGraphComponentProps {
onClickHandler?: GraphOnClickHandler;
name: string;
yAxisUnit?: string;
staticLine?: StaticLineProps;
}
GridGraphComponent.defaultProps = {
@@ -90,6 +93,7 @@ GridGraphComponent.defaultProps = {
isStacked: undefined,
onClickHandler: undefined,
yAxisUnit: undefined,
staticLine: undefined,
};
export default GridGraphComponent;

View File

@@ -1,53 +0,0 @@
import { PlusOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import React, { useCallback } from 'react';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import {
ToggleAddWidget,
ToggleAddWidgetProps,
} from 'store/actions/dashboard/toggleAddWidget';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import DashboardReducer from 'types/reducer/dashboards';
import { Button, Container } from './styles';
function AddWidget({ toggleAddWidget }: Props): JSX.Element {
const { isAddWidget } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const onToggleHandler = useCallback(() => {
toggleAddWidget(true);
}, [toggleAddWidget]);
return (
<Container>
{!isAddWidget ? (
<Button onClick={onToggleHandler} icon={<PlusOutlined />}>
Add Widgets
</Button>
) : (
<Typography>Click a widget icon to add it here</Typography>
)}
</Container>
);
}
interface DispatchProps {
toggleAddWidget: (
props: ToggleAddWidgetProps,
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
toggleAddWidget: bindActionCreators(ToggleAddWidget, dispatch),
});
type Props = DispatchProps;
export default connect(null, mapDispatchToProps)(AddWidget);

View File

@@ -1,18 +0,0 @@
import { Button as ButtonComponent } from 'antd';
import styled from 'styled-components';
export const Button = styled(ButtonComponent)`
&&& {
display: flex;
justify-content: center;
align-items: center;
border: none;
}
`;
export const Container = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 100%;
`;

View File

@@ -0,0 +1,16 @@
import { Typography } from 'antd';
import React from 'react';
import { Container } from './styles';
function EmptyWidget(): JSX.Element {
return (
<Container>
<Typography.Paragraph>
Click one of the widget types above (Time Series / Value) to add here
</Typography.Paragraph>
</Container>
);
}
export default EmptyWidget;

View File

@@ -0,0 +1,8 @@
import styled from 'styled-components';
export const Container = styled.div`
height: 100%;
display: flex;
justify-content: center;
align-items: center;
`;

View File

@@ -0,0 +1,132 @@
import { Button, Typography } from 'antd';
import { GraphOnClickHandler } from 'components/Graph';
import Spinner from 'components/Spinner';
import TimePreference from 'components/TimePreferenceDropDown';
import GridGraphComponent from 'container/GridGraphComponent';
import {
timeItems,
timePreferance,
} from 'container/NewWidget/RightContainer/timeItems';
import getChartData from 'lib/getChartData';
import React, { useCallback, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { GlobalReducer } from 'types/reducer/globalTime';
import { NotFoundContainer, TimeContainer } from './styles';
function FullView({
widget,
fullViewOptions = true,
onClickHandler,
name,
yAxisUnit,
}: FullViewProps): JSX.Element {
const { selectedTime: globalSelectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const getSelectedTime = useCallback(
() =>
timeItems.find((e) => e.enum === (widget?.timePreferance || 'GLOBAL_TIME')),
[widget],
);
const [selectedTime, setSelectedTime] = useState<timePreferance>({
name: getSelectedTime()?.name || '',
enum: widget?.timePreferance || 'GLOBAL_TIME',
});
const response = useQuery<
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
>(
`FullViewGetMetricsQueryRange-${selectedTime.enum}-${globalSelectedTime}`,
() =>
GetMetricQueryRange({
selectedTime: selectedTime.enum,
graphType: widget.panelTypes,
query: widget.query,
globalSelectedInterval: globalSelectedTime,
}),
);
const isError = response?.error;
const isLoading = response.isLoading === true;
const errorMessage = isError instanceof Error ? isError?.message : '';
if (isLoading) {
return <Spinner height="100%" size="large" tip="Loading..." />;
}
if (isError || !response?.data?.payload?.data?.result) {
return (
<NotFoundContainer>
<Typography>{errorMessage}</Typography>
</NotFoundContainer>
);
}
return (
<>
{fullViewOptions && (
<TimeContainer>
<TimePreference
{...{
selectedTime,
setSelectedTime,
}}
/>
<Button
onClick={(): void => {
response.refetch();
}}
type="primary"
>
Refresh
</Button>
</TimeContainer>
)}
<GridGraphComponent
{...{
GRAPH_TYPES: widget.panelTypes,
data: getChartData({
queryData: [
{
queryData: response.data?.payload?.data?.result
? response.data?.payload?.data?.result
: [],
},
],
}),
isStacked: widget.isStacked,
opacity: widget.opacity,
title: widget.title,
onClickHandler,
name,
yAxisUnit,
}}
/>
</>
);
}
interface FullViewProps {
widget: Widgets;
fullViewOptions?: boolean;
onClickHandler?: GraphOnClickHandler;
name: string;
yAxisUnit?: string;
}
FullView.defaultProps = {
fullViewOptions: undefined,
onClickHandler: undefined,
yAxisUnit: undefined,
};
export default FullView;

View File

@@ -19,7 +19,7 @@ import React, { useCallback, useState } from 'react';
import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { PromQLWidgets } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { NotFoundContainer, TimeContainer } from './styles';
@@ -57,7 +57,10 @@ function FullView({
time: timePreferenceType,
): { min: string | number; max: string | number } => {
if (time === 'GLOBAL_TIME') {
const minMax = GetMinMax(globalSelectedTime);
const minMax = GetMinMax(globalSelectedTime, [
minTime / 1000000,
maxTime / 1000000,
]);
return {
min: convertToNanoSecondsToSecond(minMax.minTime / 1000),
max: convertToNanoSecondsToSecond(minMax.maxTime / 1000),
@@ -170,7 +173,7 @@ function FullView({
}
interface FullViewProps {
widget: Widgets;
widget: PromQLWidgets;
fullViewOptions?: boolean;
onClickHandler?: GraphOnClickHandler;
name: string;

View File

@@ -1,13 +1,12 @@
import { Typography } from 'antd';
import getQueryResult from 'api/widgets/getQuery';
import { AxiosError } from 'axios';
import { ChartData } from 'chart.js';
import Spinner from 'components/Spinner';
import GridGraphComponent from 'container/GridGraphComponent';
import getChartData from 'lib/getChartData';
import GetMaxMinTime from 'lib/getMaxMinTime';
import GetStartAndEndTime from 'lib/getStartAndEndTime';
import React, { useCallback, useEffect, useState } from 'react';
import isEmpty from 'lodash-es/isEmpty';
import React, { memo, useCallback, useEffect, useState } from 'react';
import { Layout } from 'react-grid-layout';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
@@ -15,21 +14,26 @@ import {
DeleteWidget,
DeleteWidgetProps,
} from 'store/actions/dashboard/deleteWidget';
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { GlobalTime } from 'types/actions/globalTime';
import { Widgets } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { LayoutProps } from '..';
import EmptyWidget from '../EmptyWidget';
import WidgetHeader from '../WidgetHeader';
import FullView from './FullView';
import FullView from './FullView/index.metricsBuilder';
import { ErrorContainer, FullViewContainer, Modal } from './styles';
function GridCardGraph({
widget,
deleteWidget,
isDeleted,
name,
yAxisUnit,
layout = [],
setLayout,
}: GridCardGraphProps): JSX.Element {
const [state, setState] = useState<GridCardGraphState>({
loading: true,
@@ -39,61 +43,92 @@ function GridCardGraph({
});
const [hovered, setHovered] = useState(false);
const [modal, setModal] = useState(false);
const [deleteModal, setDeleteModal] = useState(false);
const { minTime, maxTime } = useSelector<AppState, GlobalTime>(
(state) => state.globalTime,
);
const [deleteModal, setDeletModal] = useState(false);
const { selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
// const getMaxMinTime = GetMaxMinTime({
// graphType: widget?.panelTypes,
// maxTime,
// minTime,
// });
// const { start, end } = GetStartAndEndTime({
// type: widget?.timePreferance,
// maxTime: getMaxMinTime.maxTime,
// minTime: getMaxMinTime.minTime,
// });
// const queryLength = widget?.query?.filter((e) => e.query.length !== 0) || [];
// const response = useQueries(
// queryLength?.map((query) => {
// return {
// // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
// queryFn: () => {
// return getQueryResult({
// end,
// query: query?.query,
// start,
// step: '60',
// });
// },
// queryHash: `${query?.query}-${query?.legend}-${start}-${end}`,
// retryOnMount: false,
// };
// }),
// );
// const isError =
// response.find((e) => e?.data?.statusCode !== 200) !== undefined ||
// response.some((e) => e.isError === true);
// const isLoading = response.some((e) => e.isLoading === true);
// const errorMessage = response.find((e) => e.data?.error !== null)?.data?.error;
// const data = response.map((responseOfQuery) =>
// responseOfQuery?.data?.payload?.result.map((e, index) => ({
// query: queryLength[index]?.query,
// queryData: e,
// legend: queryLength[index]?.legend,
// })),
// );
useEffect(() => {
(async (): Promise<void> => {
try {
const getMaxMinTime = GetMaxMinTime({
graphType: widget?.panelTypes,
maxTime,
minTime,
const response = await GetMetricQueryRange({
selectedTime: widget.timePreferance,
graphType: widget.panelTypes,
query: widget.query,
globalSelectedInterval,
});
const { start, end } = GetStartAndEndTime({
type: widget.timePreferance,
maxTime: getMaxMinTime.maxTime,
minTime: getMaxMinTime.minTime,
});
const isError = response.error;
const response = await Promise.all(
widget.query
.filter((e) => e.query.length !== 0)
.map(async (query) => {
const result = await getQueryResult({
end,
query: encodeURIComponent(query.query),
start,
step: '60',
});
return {
query: query.query,
queryData: result,
legend: query.legend,
};
}),
);
const isError = response.find((e) => e.queryData.statusCode !== 200);
if (isError !== undefined) {
if (isError != null) {
setState((state) => ({
...state,
error: true,
errorMessage: isError.queryData.error || 'Something went wrong',
errorMessage: isError || 'Something went wrong',
loading: false,
}));
} else {
const chartDataSet = getChartData({
queryData: response.map((e) => ({
query: e.query,
legend: e.legend,
queryData: e.queryData.payload?.result || [],
})),
queryData: [
{
queryData: response.payload?.data?.result
? response.payload?.data?.result
: [],
},
],
});
setState((state) => ({
@@ -111,7 +146,7 @@ function GridCardGraph({
}));
}
})();
}, [widget, maxTime, minTime]);
}, [widget, maxTime, minTime, globalSelectedInterval]);
const onToggleModal = useCallback(
(func: React.Dispatch<React.SetStateAction<boolean>>) => {
@@ -121,18 +156,20 @@ function GridCardGraph({
);
const onDeleteHandler = useCallback(() => {
deleteWidget({ widgetId: widget.id });
onToggleModal(setDeletModal);
// eslint-disable-next-line no-param-reassign
isDeleted.current = true;
}, [deleteWidget, widget, onToggleModal, isDeleted]);
const isEmptyWidget = widget?.id === 'empty' || isEmpty(widget);
const widgetId = isEmptyWidget ? layout[0].i : widget?.id;
deleteWidget({ widgetId, setLayout });
onToggleModal(setDeleteModal);
}, [deleteWidget, layout, onToggleModal, setLayout, widget]);
const getModals = (): JSX.Element => {
return (
<>
<Modal
destroyOnClose
onCancel={(): void => onToggleModal(setDeletModal)}
onCancel={(): void => onToggleModal(setDeleteModal)}
visible={deleteModal}
title="Delete"
height="10vh"
@@ -163,7 +200,9 @@ function GridCardGraph({
);
};
if (state.error) {
const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget);
if (state.error && !isEmptyLayout) {
return (
<>
{getModals()}
@@ -172,7 +211,7 @@ function GridCardGraph({
title={widget?.title}
widget={widget}
onView={(): void => onToggleModal(setModal)}
onDelete={(): void => onToggleModal(setDeletModal)}
onDelete={(): void => onToggleModal(setDeleteModal)}
/>
<ErrorContainer>{state.errorMessage}</ErrorContainer>
@@ -180,7 +219,10 @@ function GridCardGraph({
);
}
if (state.loading === true || state.payload === undefined) {
if (
(state.loading === true || state.payload === undefined) &&
!isEmptyLayout
) {
return <Spinner height="20vh" tip="Loading..." />;
}
@@ -199,27 +241,33 @@ function GridCardGraph({
setHovered(false);
}}
>
<WidgetHeader
parentHover={hovered}
title={widget.title}
widget={widget}
onView={(): void => onToggleModal(setModal)}
onDelete={(): void => onToggleModal(setDeletModal)}
/>
{!isEmptyLayout && (
<WidgetHeader
parentHover={hovered}
title={widget?.title}
widget={widget}
onView={(): void => onToggleModal(setModal)}
onDelete={(): void => onToggleModal(setDeleteModal)}
/>
)}
{getModals()}
{!isEmptyLayout && getModals()}
<GridGraphComponent
{...{
GRAPH_TYPES: widget.panelTypes,
data: state.payload,
isStacked: widget.isStacked,
opacity: widget.opacity,
title: ' ', // empty title to accommodate absolutely positioned widget header
name,
yAxisUnit,
}}
/>
{!isEmpty(widget) && !!state.payload && (
<GridGraphComponent
{...{
GRAPH_TYPES: widget.panelTypes,
data: state.payload,
isStacked: widget.isStacked,
opacity: widget.opacity,
title: ' ', // empty title to accommodate absolutely positioned widget header
name,
yAxisUnit,
}}
/>
)}
{isEmptyLayout && <EmptyWidget />}
</span>
);
}
@@ -239,9 +287,12 @@ interface DispatchProps {
interface GridCardGraphProps extends DispatchProps {
widget: Widgets;
isDeleted: React.MutableRefObject<boolean>;
name: string;
yAxisUnit: string | undefined;
// eslint-disable-next-line react/require-default-props
layout?: Layout[];
// eslint-disable-next-line react/require-default-props
setLayout?: React.Dispatch<React.SetStateAction<LayoutProps[]>>;
}
const mapDispatchToProps = (
@@ -250,4 +301,4 @@ const mapDispatchToProps = (
deleteWidget: bindActionCreators(DeleteWidget, dispatch),
});
export default connect(null, mapDispatchToProps)(GridCardGraph);
export default connect(null, mapDispatchToProps)(memo(GridCardGraph));

View File

@@ -0,0 +1,106 @@
import { PlusOutlined, SaveFilled } from '@ant-design/icons';
import useComponentPermission from 'hooks/useComponentPermission';
import React from 'react';
import { Layout } from 'react-grid-layout';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { LayoutProps, State } from '.';
import {
Button,
ButtonContainer,
Card,
CardContainer,
ReactGridLayout,
} from './styles';
function GraphLayout({
layouts,
saveLayoutState,
onLayoutSaveHandler,
addPanelLoading,
onAddPanelHandler,
onLayoutChangeHandler,
widgets,
setLayout,
}: GraphLayoutProps): JSX.Element {
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const [saveLayoutPermission, addPanelPermission] = useComponentPermission(
['save_layout', 'add_panel'],
role,
);
return (
<>
<ButtonContainer>
{saveLayoutPermission && (
<Button
loading={saveLayoutState.loading}
onClick={(): Promise<void> => onLayoutSaveHandler(layouts)}
icon={<SaveFilled />}
danger={saveLayoutState.error}
>
Save Layout
</Button>
)}
{addPanelPermission && (
<Button
loading={addPanelLoading}
disabled={addPanelLoading}
onClick={onAddPanelHandler}
icon={<PlusOutlined />}
>
Add Panel
</Button>
)}
</ButtonContainer>
<ReactGridLayout
cols={12}
rowHeight={100}
autoSize
width={100}
isDraggable={addPanelPermission}
isDroppable={addPanelPermission}
isResizable={addPanelPermission}
useCSSTransforms
allowOverlap={false}
onLayoutChange={onLayoutChangeHandler}
>
{layouts.map(({ Component, ...rest }) => {
const currentWidget = (widgets || [])?.find((e) => e.id === rest.i);
return (
<CardContainer
isDarkMode={isDarkMode}
key={currentWidget?.id || 'empty'} // don't change this key
data-grid={rest}
>
<Card>
<Component setLayout={setLayout} />
</Card>
</CardContainer>
);
})}
</ReactGridLayout>
</>
);
}
interface GraphLayoutProps {
layouts: LayoutProps[];
saveLayoutState: State;
onLayoutSaveHandler: (layout: Layout[]) => Promise<void>;
addPanelLoading: boolean;
onAddPanelHandler: VoidFunction;
onLayoutChangeHandler: (layout: Layout[]) => Promise<void>;
widgets: Widgets[] | undefined;
setLayout: React.Dispatch<React.SetStateAction<LayoutProps[]>>;
}
export default GraphLayout;

View File

@@ -104,7 +104,7 @@ function WidgetHeader({
overlay={menu}
trigger={['click']}
overlayStyle={{ minWidth: 100 }}
placement="bottomCenter"
placement="bottom"
>
<HeaderContainer
onMouseOver={(): void => setLocalHover(true)}

View File

@@ -1,283 +1,303 @@
/* eslint-disable react/no-unstable-nested-components */
import { SaveFilled } from '@ant-design/icons';
import { notification } from 'antd';
import updateDashboardApi from 'api/dashboard/update';
import Spinner from 'components/Spinner';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import useComponentPermission from 'hooks/useComponentPermission';
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { Layout } from 'react-grid-layout';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { connect, useDispatch, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import {
ToggleAddWidget,
ToggleAddWidgetProps,
} from 'store/actions/dashboard/toggleAddWidget';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_DASHBOARD } from 'types/actions/dashboard';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import DashboardReducer from 'types/reducer/dashboards';
import { v4 } from 'uuid';
import AddWidget from './AddWidget';
import Graph from './Graph';
import {
Button,
ButtonContainer,
Card,
CardContainer,
ReactGridLayout,
} from './styles';
import { updateDashboard } from './utils';
import GraphLayoutContainer from './GraphLayout';
import { UpdateDashboard } from './utils';
function GridGraph(): JSX.Element {
const { dashboards, loading } = useSelector<AppState, DashboardReducer>(
export const getPreLayouts = (
widgets: Widgets[] | undefined,
layout: Layout[],
): LayoutProps[] =>
layout.map((e, index) => ({
...e,
Component: ({ setLayout }: ComponentProps): JSX.Element => {
const widget = widgets?.find((widget) => widget.id === e.i);
return (
<Graph
name={e.i + index}
widget={widget as Widgets}
yAxisUnit={widget?.yAxisUnit}
layout={layout}
setLayout={setLayout}
/>
);
},
}));
function GridGraph(props: Props): JSX.Element {
const { toggleAddWidget } = props;
const [addPanelLoading, setAddPanelLoading] = useState(false);
const { t } = useTranslation(['common']);
const { dashboards, isAddWidget } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [saveLayoutPermission] = useComponentPermission(['save_layout'], role);
const [saveLayoutState, setSaveLayoutState] = useState<State>({
loading: false,
error: false,
errorMessage: '',
payload: [],
});
const [selectedDashboard] = dashboards;
const { data } = selectedDashboard;
const { widgets } = data;
const [layouts, setLayout] = useState<LayoutProps[]>([]);
const dispatch = useDispatch<Dispatch<AppActions>>();
const AddWidgetWrapper = useCallback(() => <AddWidget />, []);
const isMounted = useRef(true);
const isDeleted = useRef(false);
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [saveLayout] = useComponentPermission(['save_layout'], role);
const getPreLayouts: () => LayoutProps[] = useCallback(() => {
if (widgets === undefined) {
return [];
}
// when the layout is not present
if (data.layout === undefined) {
return widgets.map((e, index) => {
return {
h: 2,
w: 6,
y: Infinity,
i: (index + 1).toString(),
x: (index % 2) * 6,
Component: (): JSX.Element => (
<Graph
name={`${e.id + index}non-expanded`}
isDeleted={isDeleted}
widget={widgets[index]}
yAxisUnit={e.yAxisUnit}
/>
),
};
});
}
return data.layout
.filter((_, index) => widgets[index])
.map((e, index) => ({
...e,
Component: (): JSX.Element => {
if (widgets[index]) {
return (
<Graph
name={e.i + index}
isDeleted={isDeleted}
widget={widgets[index]}
yAxisUnit={widgets[index].yAxisUnit}
/>
);
}
return <div />;
},
}));
}, [widgets, data.layout]);
const [layouts, setLayout] = useState<LayoutProps[]>(
getPreLayouts(widgets, selectedDashboard.data.layout || []),
);
useEffect(() => {
if (
loading === false &&
(isMounted.current === true || isDeleted.current === true)
) {
const preLayouts = getPreLayouts();
setLayout(() => {
const getX = (): number => {
if (preLayouts && preLayouts?.length > 0) {
const last = preLayouts[(preLayouts?.length || 0) - 1];
(async (): Promise<void> => {
if (!isAddWidget) {
const isEmptyLayoutPresent = layouts.find((e) => e.i === 'empty');
if (isEmptyLayoutPresent) {
// non empty layout
const updatedLayout = layouts.filter((e) => e.i !== 'empty');
// non widget
const updatedWidget = widgets?.filter((e) => e.id !== 'empty');
setLayout(updatedLayout);
return (last.w + last.x) % 12;
}
return 0;
};
const updatedDashboard: Dashboard = {
...selectedDashboard,
data: {
...selectedDashboard.data,
layout: updatedLayout,
widgets: updatedWidget,
},
};
return [
...preLayouts,
{
i: (preLayouts.length + 1).toString(),
x: getX(),
y: Infinity,
w: 6,
h: 2,
Component: AddWidgetWrapper,
maxW: 6,
isDraggable: false,
isResizable: false,
isBounded: true,
},
];
});
}
return (): void => {
isMounted.current = false;
};
}, [widgets, layouts.length, AddWidgetWrapper, loading, getPreLayouts]);
const onDropHandler = useCallback(
async (allLayouts: Layout[], currentLayout: Layout, event: DragEvent) => {
event.preventDefault();
if (event.dataTransfer) {
try {
const graphType = event.dataTransfer.getData('text') as GRAPH_TYPES;
const generateWidgetId = v4();
await updateDashboard({
data,
generateWidgetId,
graphType,
selectedDashboard,
layout: allLayouts
.map((e, index) => ({
...e,
i: index.toString(),
// when a new element drops
w: e.i === '__dropping-elem__' ? 6 : e.w,
h: e.i === '__dropping-elem__' ? 2 : e.h,
}))
// removing add widgets layout config
.filter((e) => e.maxW === undefined),
await updateDashboardApi({
data: updatedDashboard.data,
uuid: updatedDashboard.uuid,
});
} catch (error) {
notification.error({
message:
error instanceof Error ? error.toString() : 'Something went wrong',
dispatch({
type: UPDATE_DASHBOARD,
payload: updatedDashboard,
});
}
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onLayoutSaveHandler = useCallback(
async (layout: Layout[]) => {
try {
setSaveLayoutState((state) => ({
...state,
error: false,
errorMessage: '',
loading: true,
}));
const updatedDashboard: Dashboard = {
...selectedDashboard,
data: {
title: data.title,
description: data.description,
name: data.name,
tags: data.tags,
widgets: data.widgets,
layout,
},
uuid: selectedDashboard.uuid,
};
// Save layout only when users has the has the permission to do so.
if (saveLayoutPermission) {
const response = await updateDashboardApi(updatedDashboard);
if (response.statusCode === 200) {
setSaveLayoutState((state) => ({
...state,
error: false,
errorMessage: '',
loading: false,
}));
dispatch({
type: UPDATE_DASHBOARD,
payload: updatedDashboard,
});
} else {
setSaveLayoutState((state) => ({
...state,
error: true,
errorMessage: response.error || 'Something went wrong',
loading: false,
}));
}
}
} catch (error) {
console.error(error);
}
},
[data, selectedDashboard],
[
data.description,
data.name,
data.tags,
data.title,
data.widgets,
dispatch,
saveLayoutPermission,
selectedDashboard,
],
);
const onLayoutSaveHandler = async (): Promise<void> => {
setSaveLayoutState((state) => ({
...state,
error: false,
errorMessage: '',
loading: true,
}));
const setLayoutFunction = useCallback(
(layout: Layout[]) => {
setLayout(
layout.map((e) => {
const currentWidget =
widgets?.find((widget) => widget.id === e.i) || ({} as Widgets);
const response = await updateDashboardApi({
data: {
title: data.title,
description: data.description,
name: data.name,
tags: data.tags,
widgets: data.widgets,
layout: saveLayoutState.payload.filter((e) => e.maxW === undefined),
},
uuid: selectedDashboard.uuid,
});
if (response.statusCode === 200) {
setSaveLayoutState((state) => ({
...state,
error: false,
errorMessage: '',
loading: false,
}));
} else {
setSaveLayoutState((state) => ({
...state,
error: true,
errorMessage: response.error || 'Something went wrong',
loading: false,
}));
return {
...e,
Component: (): JSX.Element => (
<Graph
name={currentWidget.id}
widget={currentWidget}
yAxisUnit={currentWidget?.yAxisUnit}
layout={layout}
setLayout={setLayout}
/>
),
};
}),
);
},
[widgets],
);
const onEmptyWidgetHandler = useCallback(async () => {
try {
const id = 'empty';
const layout = [
{
i: id,
w: 6,
x: 0,
h: 2,
y: 0,
},
...(data.layout || []),
];
await UpdateDashboard({
data,
generateWidgetId: id,
graphType: 'EMPTY_WIDGET',
selectedDashboard,
layout,
isRedirected: false,
});
setLayoutFunction(layout);
} catch (error) {
notification.error({
message: error instanceof Error ? error.toString() : 'Something went wrong',
});
}
}, [data, selectedDashboard, setLayoutFunction]);
const onLayoutChangeHandler = async (layout: Layout[]): Promise<void> => {
setLayoutFunction(layout);
// await onLayoutSaveHandler(layout);
};
const onLayoutChangeHandler = (layout: Layout[]): void => {
setSaveLayoutState({
loading: false,
error: false,
errorMessage: '',
payload: layout,
});
};
const onAddPanelHandler = useCallback(() => {
try {
setAddPanelLoading(true);
const isEmptyLayoutPresent =
layouts.find((e) => e.i === 'empty') !== undefined;
if (layouts.length === 0) {
return <Spinner height="40vh" size="large" tip="Loading..." />;
}
if (!isEmptyLayoutPresent) {
onEmptyWidgetHandler()
.then(() => {
setAddPanelLoading(false);
toggleAddWidget(true);
})
.catch(() => {
notification.error(t('something_went_wrong'));
});
} else {
toggleAddWidget(true);
setAddPanelLoading(false);
}
} catch (error) {
if (typeof error === 'string') {
notification.error({
message: error || t('something_went_wrong'),
});
}
}
}, [layouts, onEmptyWidgetHandler, t, toggleAddWidget]);
return (
<>
{saveLayout && (
<ButtonContainer>
<Button
loading={saveLayoutState.loading}
onClick={onLayoutSaveHandler}
icon={<SaveFilled />}
danger={saveLayoutState.error}
>
Save Layout
</Button>
</ButtonContainer>
)}
<ReactGridLayout
isResizable
isDraggable
cols={12}
rowHeight={100}
autoSize
width={100}
isDroppable
useCSSTransforms
onDrop={onDropHandler}
onLayoutChange={onLayoutChangeHandler}
>
{layouts.map(({ Component, ...rest }, index) => {
const widget = (widgets || [])[index] || {};
const type = widget?.panelTypes || 'TIME_SERIES';
const isQueryType = type === 'VALUE';
return (
<CardContainer
isQueryType={isQueryType}
isDarkMode={isDarkMode}
key={rest.i}
data-grid={rest}
>
<Card isDarkMode={isDarkMode} isQueryType={isQueryType}>
<Component />
</Card>
</CardContainer>
);
})}
</ReactGridLayout>
</>
<GraphLayoutContainer
{...{
addPanelLoading,
layouts,
onAddPanelHandler,
onLayoutChangeHandler,
onLayoutSaveHandler,
saveLayoutState,
widgets,
setLayout,
}}
/>
);
}
interface LayoutProps extends Layout {
Component: () => JSX.Element;
interface ComponentProps {
setLayout: React.Dispatch<React.SetStateAction<LayoutProps[]>>;
}
interface State {
export interface LayoutProps extends Layout {
Component: (props: ComponentProps) => JSX.Element;
}
export interface State {
loading: boolean;
error: boolean;
payload: Layout[];
errorMessage: string;
}
export default memo(GridGraph);
interface DispatchProps {
toggleAddWidget: (
props: ToggleAddWidgetProps,
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
toggleAddWidget: bindActionCreators(ToggleAddWidget, dispatch),
});
type Props = DispatchProps;
export default connect(null, mapDispatchToProps)(GridGraph);

View File

@@ -1,14 +1,11 @@
import { Button as ButtonComponent, Card as CardComponent } from 'antd';
import { Button as ButtonComponent, Card as CardComponent, Space } from 'antd';
import { StyledCSS } from 'container/GantChart/Trace/styles';
import RGL, { WidthProvider } from 'react-grid-layout';
import styled, { css } from 'styled-components';
const ReactGridLayoutComponent = WidthProvider(RGL);
interface Props {
isQueryType: boolean;
}
export const Card = styled(CardComponent)<Props>`
export const Card = styled(CardComponent)`
&&& {
height: 100%;
}
@@ -54,9 +51,22 @@ export const ReactGridLayout = styled(ReactGridLayoutComponent)`
border: 1px solid #434343;
margin-top: 1rem;
position: relative;
min-height: 40vh;
.react-grid-item.react-grid-placeholder {
background: grey;
opacity: 0.2;
transition-duration: 100ms;
z-index: 2;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
`;
export const ButtonContainer = styled.div`
export const ButtonContainer = styled(Space)`
display: flex;
justify-content: end;
margin-top: 1rem;

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