Compare commits

..

85 Commits

Author SHA1 Message Date
Rajat-Dabade
ced74603c0 chore: updated test script 2023-12-15 13:46:16 +05:30
Rajat-Dabade
f59fb81109 refactor: updated test directory 2023-12-15 13:46:16 +05:30
Rajat-Dabade
507e68a0c1 refactor: reverted back as working directory is specified as frontend 2023-12-15 13:46:16 +05:30
Rajat-Dabade
4ad8a1f3ad refactor: shifted i18n to original location 2023-12-15 13:46:16 +05:30
Rajat-Dabade
19faf6a584 chore: updates 2023-12-15 13:46:16 +05:30
Rajat-Dabade
3978ada811 refactor: for push and pull request 2023-12-15 13:46:16 +05:30
Rajat-Dabade
0a04fc04a5 refactor: generate code coverage report on every push 2023-12-15 13:46:16 +05:30
Rajat-Dabade
7c9e333b84 refactor: added package-manager 2023-12-15 13:46:16 +05:30
Rajat-Dabade
dd78afb20f refactor: updated the working directory 2023-12-15 13:46:16 +05:30
Rajat-Dabade
237d765376 refactor: updated github flow 2023-12-15 13:46:16 +05:30
Rajat-Dabade
85e865fb1b refactor: updated token 2023-12-15 13:46:16 +05:30
Rajat-Dabade
975e5daf03 refactor: updated test case 2023-12-15 13:46:16 +05:30
Rajat-Dabade
8a532cca17 refactor: updated jest running command 2023-12-15 13:46:16 +05:30
Rajat-Dabade
b9c908719f refactor: updated the command for jest 2023-12-15 13:46:16 +05:30
Rajat-Dabade
63c7b5e9e1 chore: minor changes 2023-12-15 13:46:16 +05:30
Rajat-Dabade
32eeb3d106 refactor: done some changes 2023-12-15 13:46:16 +05:30
Rajat-Dabade
1a4ec2bf00 feat: jest code coverage report 2023-12-15 13:46:16 +05:30
Yunus M
1d014ab4f7 Rearrange variables (#4187)
* feat: variable re-arrange

* feat: update variable update from dashboard description

* feat: update variable update from dashboard description

* feat: update custom variable dropdown values on change

* feat: handle dependent value updates to dashboard description

* feat: handle dependent 0th order variable update

* feat: update variable item test

* feat: transform variables data to support rearraging

* feat: update modal import

* feat: remove console logs

* feat: ts-ignore

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

* feat: handle no data

* feat: show bigger point if only data point exists

* feat: show bigger point if only data point exists

* feat: widget ui fixes

* feat: no data - full view fix

* fix: show closed point on hover

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

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

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

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

* fix: transformIgnorePatterns axios

* fix: remove axios types

* feat: handle error type from dashboardAPI response

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

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

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

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

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

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

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

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

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

* chore: minor cleanup

* chore: some more cleanup

* chore: some more cleanup

* chore: get jest passing

* chore: normalize ts in pipelines previews input and output

* chore: some cleanup

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

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

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

* chore: add happy case test for severity parser

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

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

* chore: add if condition to generated severity_parser operators

* chore: add postablePipeline validation for severity parser

* chore: minor cleanup in tests

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

* fix: hide tooltip if no tooltip values present

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

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

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

* feat: remove console logs

* fix: remove console.log

* fix: update tests

* fix: update tests

---------

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

* chore: add test for timestamp parsing processor

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

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

* chore: add helper for generating regex for strptime layouts

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

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

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

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

* chore: postable pipeline validation for time parser

* chore: some cleanup

* chore: some more cleanup

* chore: add validation for strptime layouts in postable pipelines

---------

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

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

---------

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

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

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

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

* fix: temp workaround for supporting pipeline filters using names with dots converted to underscore
2023-11-26 12:57:23 +05:30
207 changed files with 5756 additions and 1938 deletions

4
.github/CODEOWNERS vendored
View File

@@ -8,8 +8,4 @@
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv
/deploy/ @prashant-shahi
/sample-apps/ @prashant-shahi
**/query-service/ @srikanthccv
Makefile @srikanthccv
go.* @srikanthccv
.git* @srikanthccv
.github @prashant-shahi

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

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

View File

@@ -0,0 +1,32 @@
name: Code Coverage
on:
push:
branches:
- develop
- main
- release/v*
pull_request:
branches:
- develop
- main
- release/v*
jobs:
coverage:
runs-on: ubuntu-latest
permissions:
checks: write
pull-requests: write
contents: write
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- uses: jwalton/gh-find-current-pr@v1
id: findPr
- uses: ArtiomTr/jest-coverage-report-action@v2
with:
package-manager: yarn
working-directory: frontend
test-script: yarn jest:coverage
github-token: ${{ secrets.GITHUB_TOKEN }}
output: comment
prnumber: ${{ steps.findPr.outputs.number }}

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
version: "3.9"
x-clickhouse-defaults: &clickhouse-defaults
image: clickhouse/clickhouse-server:23.7.3-alpine
image: clickhouse/clickhouse-server:23.11.1-alpine
tty: true
deploy:
restart_policy:
@@ -146,7 +146,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.34.3
image: signoz/query-service:0.35.1
command:
[
"-config=/root/config/prometheus.yml",
@@ -186,7 +186,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:0.34.3
image: signoz/frontend:0.35.1
deploy:
restart_policy:
condition: on-failure
@@ -199,7 +199,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/signoz-otel-collector:0.88.1
image: signoz/signoz-otel-collector:0.88.3
command:
[
"--config=/etc/otel-collector-config.yaml",
@@ -237,7 +237,7 @@ services:
- query-service
otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.88.1
image: signoz/signoz-schema-migrator:0.88.3
deploy:
restart_policy:
condition: on-failure
@@ -250,7 +250,7 @@ services:
# - clickhouse-3
otel-collector-metrics:
image: signoz/signoz-otel-collector:0.88.1
image: signoz/signoz-otel-collector:0.88.3
command:
[
"--config=/etc/otel-collector-metrics-config.yaml",

View File

@@ -66,7 +66,7 @@ services:
- --storage.path=/data
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.1}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.3}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@@ -81,7 +81,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector:
container_name: signoz-otel-collector
image: signoz/signoz-otel-collector:0.88.1
image: signoz/signoz-otel-collector:0.88.3
command:
[
"--config=/etc/otel-collector-config.yaml",
@@ -118,7 +118,7 @@ services:
otel-collector-metrics:
container_name: signoz-otel-collector-metrics
image: signoz/signoz-otel-collector:0.88.1
image: signoz/signoz-otel-collector:0.88.3
command:
[
"--config=/etc/otel-collector-metrics-config.yaml",

View File

@@ -3,7 +3,7 @@ version: "2.4"
x-clickhouse-defaults: &clickhouse-defaults
restart: on-failure
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
image: clickhouse/clickhouse-server:23.7.3-alpine
image: clickhouse/clickhouse-server:23.11.1-alpine
tty: true
depends_on:
- zookeeper-1
@@ -164,7 +164,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.34.3}
image: signoz/query-service:${DOCKER_TAG:-0.35.1}
container_name: signoz-query-service
command:
[
@@ -203,7 +203,7 @@ services:
<<: *db-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.34.3}
image: signoz/frontend:${DOCKER_TAG:-0.35.1}
container_name: signoz-frontend
restart: on-failure
depends_on:
@@ -215,7 +215,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.1}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.3}
container_name: otel-migrator
command:
- "--dsn=tcp://clickhouse:9000"
@@ -229,7 +229,7 @@ services:
otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.3}
container_name: signoz-otel-collector
command:
[
@@ -269,7 +269,7 @@ services:
condition: service_healthy
otel-collector-metrics:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.1}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.3}
container_name: signoz-otel-collector-metrics
command:
[

View File

@@ -1,5 +1,5 @@
# use a minimal alpine image
FROM alpine:3.18.3
FROM alpine:3.18.5
# Add Maintainer Info
LABEL maintainer="signoz"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,9 @@
import cacheBursting from 'i18n-translations-hash.json';
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
import { initReactI18next } from 'react-i18next';
import cacheBursting from '../../i18n-translations-hash.json';
i18n
// load translation using http -> see /public/locales
.use(Backend)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,6 +23,7 @@ import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants';
import GraphManager from './GraphManager';
@@ -52,7 +53,7 @@ function FullView({
const [chartOptions, setChartOptions] = useState<uPlot.Options>();
const { selectedDashboard } = useDashboard();
const { selectedDashboard, isDashboardLocked } = useDashboard();
const getSelectedTime = useCallback(
() =>
@@ -92,6 +93,21 @@ function FullView({
const isDarkMode = useIsDarkMode();
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
useEffect((): void => {
const { startTime, endTime } = getTimeRange(response);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [maxTime, minTime, globalSelectedInterval, response]);
useEffect(() => {
if (!response.isFetching && fullViewRef.current) {
const width = fullViewRef.current?.clientWidth
@@ -114,6 +130,8 @@ function FullView({
graphsVisibilityStates,
setGraphsVisibilityStates,
thresholds: widget.thresholds,
minTimeScale,
maxTimeScale,
});
setChartOptions(newChartOptions);
@@ -155,7 +173,12 @@ function FullView({
)}
</div>
<div className="graph-container" ref={fullViewRef}>
<div
className={
isDashboardLocked ? 'graph-container disabled' : 'graph-container'
}
ref={fullViewRef}
>
{chartOptions && (
<GraphContainer
style={{ height: '90%' }}
@@ -178,7 +201,7 @@ function FullView({
)}
</div>
{canModifyChart && chartOptions && (
{canModifyChart && chartOptions && !isDashboardLocked && (
<GraphManager
data={chartData}
name={name}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import './GridCardLayout.styles.scss';
import { PlusOutlined, SaveFilled } from '@ant-design/icons';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { themeColors } from 'constants/theme';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -155,6 +156,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
onLayoutChange={setLayouts}
draggableHandle=".drag-handle"
layout={layouts}
style={{ backgroundColor: isDarkMode ? '' : themeColors.snowWhite }}
>
{layouts.map((layout) => {
const { i: id } = layout;
@@ -165,7 +167,7 @@ function GraphLayout({ onAddPanelHandler }: GraphLayoutProps): JSX.Element {
className={isDashboardLocked ? '' : 'enable-resize'}
isDarkMode={isDarkMode}
key={id}
data-grid={layout}
data-grid={JSON.stringify(currentWidget)}
>
<Card
className="grid-item"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { Data } from '..';
import { Data } from '../DashboardsList';
import { TableLinkText } from './styles';
interface DeleteButtonProps {

View File

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

View File

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

View File

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

View File

@@ -8,9 +8,10 @@ export const getWidgetQueryBuilder = ({
title = '',
panelTypes,
yAxisUnit = '',
id,
}: GetWidgetQueryBuilderProps): Widgets => ({
description: '',
id: v4(),
id: id || v4(),
isStacked: false,
nullZeroValues: '',
opacity: '0',

View File

@@ -16,7 +16,7 @@ import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { GraphTitle, MENU_ITEMS } from '../constant';
import { GraphTitle, MENU_ITEMS, SERVICE_CHART_ID } from '../constant';
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
import { Card, GraphContainer, Row } from '../styles';
import { Button } from './styles';
@@ -66,6 +66,7 @@ function DBCall(): JSX.Element {
title: GraphTitle.DATABASE_CALLS_RPS,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'reqps',
id: SERVICE_CHART_ID.dbCallsRPS,
}),
[servicename, tagFilterItems],
);
@@ -85,6 +86,7 @@ function DBCall(): JSX.Element {
title: GraphTitle.DATABASE_CALLS_AVG_DURATION,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ms',
id: SERVICE_CHART_ID.dbCallsAvgDuration,
}),
[servicename, tagFilterItems],
);
@@ -107,7 +109,7 @@ function DBCall(): JSX.Element {
<Card data-testid="database_call_rps">
<GraphContainer>
<Graph
filterNaN
fillSpans={false}
name="database_call_rps"
widget={databaseCallsRPSWidget}
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
@@ -141,7 +143,7 @@ function DBCall(): JSX.Element {
<Card data-testid="database_call_avg_duration">
<GraphContainer>
<Graph
filterNaN
fillSpans
name="database_call_avg_duration"
widget={databaseCallsAverageDurationWidget}
headerMenuList={MENU_ITEMS}

View File

@@ -17,7 +17,7 @@ import { useParams } from 'react-router-dom';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { GraphTitle, legend, MENU_ITEMS } from '../constant';
import { GraphTitle, legend, MENU_ITEMS, SERVICE_CHART_ID } from '../constant';
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
import { Card, GraphContainer, Row } from '../styles';
import { Button } from './styles';
@@ -57,6 +57,7 @@ function External(): JSX.Element {
title: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: '%',
id: SERVICE_CHART_ID.externalCallErrorPercentage,
}),
[servicename, tagFilterItems],
);
@@ -82,6 +83,7 @@ function External(): JSX.Element {
title: GraphTitle.EXTERNAL_CALL_DURATION,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ms',
id: SERVICE_CHART_ID.externalCallDuration,
}),
[servicename, tagFilterItems],
);
@@ -103,6 +105,7 @@ function External(): JSX.Element {
title: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'reqps',
id: SERVICE_CHART_ID.externalCallRPSByAddress,
}),
[servicename, tagFilterItems],
);
@@ -124,6 +127,7 @@ function External(): JSX.Element {
title: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ms',
id: SERVICE_CHART_ID.externalCallDurationByAddress,
}),
[servicename, tagFilterItems],
);
@@ -148,7 +152,7 @@ function External(): JSX.Element {
<Card data-testid="external_call_error_percentage">
<GraphContainer>
<Graph
filterNaN
fillSpans={false}
headerMenuList={MENU_ITEMS}
name="external_call_error_percentage"
widget={externalCallErrorWidget}
@@ -184,7 +188,7 @@ function External(): JSX.Element {
<Card data-testid="external_call_duration">
<GraphContainer>
<Graph
filterNaN
fillSpans
name="external_call_duration"
headerMenuList={MENU_ITEMS}
widget={externalCallDurationWidget}
@@ -221,7 +225,7 @@ function External(): JSX.Element {
<Card data-testid="external_call_rps_by_address">
<GraphContainer>
<Graph
filterNaN
fillSpans
name="external_call_rps_by_address"
widget={externalCallRPSWidget}
headerMenuList={MENU_ITEMS}
@@ -260,7 +264,7 @@ function External(): JSX.Element {
name="external_call_duration_by_address"
widget={externalCallDurationAddressWidget}
headerMenuList={MENU_ITEMS}
filterNaN
fillSpans
onClickHandler={(xValue, yValue, mouseX, mouseY): void => {
onGraphClickHandler(setSelectedTimeStamp)(
xValue,

View File

@@ -26,7 +26,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { v4 as uuid } from 'uuid';
import { GraphTitle } from '../constant';
import { GraphTitle, SERVICE_CHART_ID } from '../constant';
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
import {
errorPercentage,
@@ -131,6 +131,7 @@ function Application(): JSX.Element {
title: GraphTitle.RATE_PER_OPS,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ops',
id: SERVICE_CHART_ID.rps,
}),
[servicename, tagFilterItems, topLevelOperationsRoute],
);
@@ -152,6 +153,7 @@ function Application(): JSX.Element {
title: GraphTitle.ERROR_PERCENTAGE,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: '%',
id: SERVICE_CHART_ID.errorPercentage,
}),
[servicename, tagFilterItems, topLevelOperationsRoute],
);

View File

@@ -8,7 +8,10 @@ import {
import { PANEL_TYPES } from 'constants/queryBuilder';
import Graph from 'container/GridCardLayout/GridCard';
import DisplayThreshold from 'container/GridCardLayout/WidgetHeader/DisplayThreshold';
import { GraphTitle } from 'container/MetricsApplication/constant';
import {
GraphTitle,
SERVICE_CHART_ID,
} from 'container/MetricsApplication/constant';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { apDexMetricsQueryBuilderQueries } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
import { ReactNode, useMemo } from 'react';
@@ -59,6 +62,7 @@ function ApDexMetrics({
</Space>
),
panelTypes: PANEL_TYPES.TIME_SERIES,
id: SERVICE_CHART_ID.apdex,
}),
[
delta,
@@ -84,7 +88,7 @@ function ApDexMetrics({
return (
<Graph
name="apdex"
filterNaN
fillSpans={false}
widget={apDexMetricsWidget}
onDragSelect={onDragSelect}
onClickHandler={handleGraphClick('ApDex')}

View File

@@ -1,7 +1,10 @@
import { FeatureKeys } from 'constants/features';
import { PANEL_TYPES } from 'constants/queryBuilder';
import Graph from 'container/GridCardLayout/GridCard';
import { GraphTitle } from 'container/MetricsApplication/constant';
import {
GraphTitle,
SERVICE_CHART_ID,
} from 'container/MetricsApplication/constant';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { latency } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
@@ -59,6 +62,7 @@ function ServiceOverview({
title: GraphTitle.LATENCY,
panelTypes: PANEL_TYPES.TIME_SERIES,
yAxisUnit: 'ns',
id: SERVICE_CHART_ID.latency,
}),
[servicename, isSpanMetricEnable, topLevelOperationsRoute, tagFilterItems],
);
@@ -88,7 +92,7 @@ function ServiceOverview({
widget={latencyWidget}
onClickHandler={handleGraphClick('Service')}
isQueryEnabled={isQueryEnabled}
filterNaN
fillSpans={false}
/>
</GraphContainer>
</Card>

View File

@@ -27,7 +27,7 @@ function TopLevelOperation({
) : (
<GraphContainer>
<Graph
filterNaN
fillSpans={false}
name={name}
widget={widget}
onClickHandler={handleGraphClick(opName)}

View File

@@ -0,0 +1,19 @@
import { TopOperationList } from '../TopOperationsTable';
interface TopOperation {
numCalls: number;
errorCount: number;
}
export const getTopOperationList = ({
errorCount,
numCalls,
}: TopOperation): TopOperationList =>
({
p50: 0,
errorCount,
name: 'test',
numCalls,
p95: 0,
p99: 0,
} as TopOperationList);

View File

@@ -79,3 +79,17 @@ export const topOperationMetricsDownloadOptions: DownloadOptions = {
isDownloadEnabled: true,
fileName: 'top-operation',
} as const;
export const SERVICE_CHART_ID = {
latency: 'SERVICE_OVERVIEW_LATENCY',
error: 'SERVICE_OVERVIEW_ERROR',
rps: 'SERVICE_OVERVIEW_RPS',
apdex: 'SERVICE_OVERVIEW_APDEX',
errorPercentage: 'SERVICE_OVERVIEW_ERROR_PERCENTAGE',
dbCallsRPS: 'SERVICE_DATABASE_CALLS_RPS',
dbCallsAvgDuration: 'SERVICE_DATABASE_CALLS_AVG_DURATION',
externalCallDurationByAddress: 'SERVICE_EXTERNAL_CALLS_DURATION_BY_ADDRESS',
externalCallErrorPercentage: 'SERVICE_EXTERNAL_CALLS_ERROR_PERCENTAGE',
externalCallDuration: 'SERVICE_EXTERNAL_CALLS_DURATION',
externalCallRPSByAddress: 'SERVICE_EXTERNAL_CALLS_RPS_BY_ADDRESS',
};

View File

@@ -9,6 +9,7 @@ export interface GetWidgetQueryBuilderProps {
title?: ReactNode;
panelTypes: Widgets['panelTypes'];
yAxisUnit?: Widgets['yAxisUnit'];
id?: Widgets['id'];
}
export interface NavigateToTraceProps {

View File

@@ -0,0 +1,70 @@
import { getTopOperationList } from './__mocks__/getTopOperation';
import { TopOperationList } from './TopOperationsTable';
import {
convertedTracesToDownloadData,
getErrorRate,
getNearestHighestBucketValue,
} from './utils';
describe('Error Rate', () => {
test('should return correct error rate', () => {
const list: TopOperationList = getTopOperationList({
errorCount: 10,
numCalls: 100,
});
expect(getErrorRate(list)).toBe(10);
});
test('should handle no errors gracefully', () => {
const list = getTopOperationList({ errorCount: 0, numCalls: 100 });
expect(getErrorRate(list)).toBe(0);
});
test('should handle zero calls', () => {
const list = getTopOperationList({ errorCount: 0, numCalls: 0 });
expect(getErrorRate(list)).toBe(0);
});
});
describe('getNearestHighestBucketValue', () => {
test('should return nearest higher bucket value', () => {
expect(getNearestHighestBucketValue(50, [10, 20, 30, 40, 60, 70])).toBe('60');
});
test('should return +Inf for value higher than any bucket', () => {
expect(getNearestHighestBucketValue(80, [10, 20, 30, 40, 60, 70])).toBe(
'+Inf',
);
});
test('should return the first bucket for value lower than all buckets', () => {
expect(getNearestHighestBucketValue(5, [10, 20, 30, 40, 60, 70])).toBe('10');
});
});
describe('convertedTracesToDownloadData', () => {
test('should convert trace data correctly', () => {
const data = [
{
name: 'op1',
p50: 50000000,
p95: 95000000,
p99: 99000000,
numCalls: 100,
errorCount: 10,
},
];
expect(convertedTracesToDownloadData(data)).toEqual([
{
Name: 'op1',
'P50 (in ms)': '50.00',
'P95 (in ms)': '95.00',
'P99 (in ms)': '99.00',
'Number of calls': '100',
'Error Rate (%)': '10.00',
},
]);
});
});

View File

@@ -5,8 +5,12 @@ import history from 'lib/history';
import { TopOperationList } from './TopOperationsTable';
import { NavigateToTraceProps } from './types';
export const getErrorRate = (list: TopOperationList): number =>
(list.errorCount / list.numCalls) * 100;
export const getErrorRate = (list: TopOperationList): number => {
if (list.errorCount === 0 && list.numCalls === 0) {
return 0;
}
return (list.errorCount / list.numCalls) * 100;
};
export const navigateToTrace = ({
servicename,

View File

@@ -29,7 +29,7 @@ function SettingsDrawer({ drawerTitle }: { drawerTitle: string }): JSX.Element {
<DrawerContainer
title={drawerTitle}
placement="right"
width="50%"
width="60%"
onClose={onClose}
open={visible}
>

View File

@@ -50,7 +50,7 @@ function DashboardDescription(): JSX.Element {
return (
<Card>
<Row gutter={16}>
<Col flex={1} span={12}>
<Col flex={1} span={9}>
<Typography.Title
level={4}
style={{ padding: 0, margin: 0 }}
@@ -80,12 +80,12 @@ function DashboardDescription(): JSX.Element {
</div>
)}
</Col>
<Col span={8}>
<Col span={12}>
<Row justify="end">
<DashboardVariableSelection />
</Row>
</Col>
<Col span={4} style={{ textAlign: 'right' }}>
<Col span={3} style={{ textAlign: 'right' }}>
{selectedData && (
<ShareModal
isJSONModalVisible={openDashboardJSON}

View File

@@ -0,0 +1,5 @@
.delete-variable-name {
font-weight: 700;
color: rgb(207, 19, 34);
font-style: italic;
}

View File

@@ -0,0 +1,8 @@
.query-container {
display: flex;
flex-flow: row wrap;
min-width: 0;
gap: 1rem;
margin-bottom: 1rem;
flex-direction: column;
}

View File

@@ -1,21 +1,16 @@
/* eslint-disable sonarjs/cognitive-complexity */
import './VariableItem.styles.scss';
import { orange } from '@ant-design/colors';
import {
Button,
Col,
Divider,
Input,
Select,
Switch,
Tag,
Typography,
} from 'antd';
import query from 'api/dashboard/variables/query';
import { Button, Divider, Input, Select, Switch, Tag, Typography } from 'antd';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import Editor from 'components/Editor';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
import { map } from 'lodash-es';
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import {
IDashboardVariable,
TSortVariableValuesType,
@@ -23,10 +18,10 @@ import {
VariableQueryTypeArr,
VariableSortTypeArr,
} from 'types/api/dashboard/getAll';
import { v4 } from 'uuid';
import { v4 as generateUUID } from 'uuid';
import { variablePropsToPayloadVariables } from '../../../utils';
import { TVariableViewMode } from '../types';
import { TVariableMode } from '../types';
import { LabelContainer, VariableItemRow } from './styles';
const { Option } = Select;
@@ -35,9 +30,9 @@ interface VariableItemProps {
variableData: IDashboardVariable;
existingVariables: Record<string, IDashboardVariable>;
onCancel: () => void;
onSave: (name: string, arg0: IDashboardVariable, arg1: string) => void;
onSave: (mode: TVariableMode, variableData: IDashboardVariable) => void;
validateName: (arg0: string) => boolean;
variableViewMode: TVariableViewMode;
mode: TVariableMode;
}
function VariableItem({
variableData,
@@ -45,7 +40,7 @@ function VariableItem({
onCancel,
onSave,
validateName,
variableViewMode,
mode,
}: VariableItemProps): JSX.Element {
const [variableName, setVariableName] = useState<string>(
variableData.name || '',
@@ -79,8 +74,6 @@ function VariableItem({
);
const [previewValues, setPreviewValues] = useState<string[]>([]);
// Internal states
const [previewLoading, setPreviewLoading] = useState<boolean>(false);
// Error messages
const [errorName, setErrorName] = useState<boolean>(false);
const [errorPreview, setErrorPreview] = useState<string | null>(null);
@@ -104,7 +97,7 @@ function VariableItem({
]);
const handleSave = (): void => {
const newVariableData: IDashboardVariable = {
const variable: IDashboardVariable = {
name: variableName,
description: variableDescription,
type: queryType,
@@ -118,245 +111,277 @@ function VariableItem({
selectedValue: (variableData.selectedValue ||
variableTextboxValue) as never,
}),
modificationUUID: v4(),
modificationUUID: generateUUID(),
id: variableData.id || generateUUID(),
order: variableData.order,
};
onSave(
variableName,
newVariableData,
(variableViewMode === 'EDIT' && variableName !== variableData.name
? variableData.name
: '') as string,
);
onCancel();
onSave(mode, variable);
};
// Fetches the preview values for the SQL variable query
const handleQueryResult = async (): Promise<void> => {
setPreviewLoading(true);
setErrorPreview(null);
try {
const variableQueryResponse = await query({
query: variableQueryValue,
variables: variablePropsToPayloadVariables(existingVariables),
});
setPreviewLoading(false);
if (variableQueryResponse.error) {
let message = variableQueryResponse.error;
if (variableQueryResponse.error.includes('Syntax error:')) {
message =
'Please make sure query is valid and dependent variables are selected';
}
setErrorPreview(message);
return;
}
if (variableQueryResponse.payload?.variableValues)
setPreviewValues(
sortValues(
variableQueryResponse.payload?.variableValues || [],
variableSortType,
) as never,
);
} catch (e) {
console.error(e);
}
const handleQueryResult = (response: any): void => {
if (response?.payload?.variableValues)
setPreviewValues(
sortValues(
response.payload?.variableValues || [],
variableSortType,
) as never,
);
};
const { isFetching: previewLoading, refetch: runQuery } = useQuery(
[REACT_QUERY_KEY.DASHBOARD_BY_ID, variableData.name, variableName],
{
enabled: false,
queryFn: () =>
dashboardVariablesQuery({
query: variableQueryValue || '',
variables: variablePropsToPayloadVariables(existingVariables),
}),
refetchOnWindowFocus: false,
onSuccess: (response) => {
setErrorPreview(null);
handleQueryResult(response);
},
onError: (error: {
details: {
error: string;
};
}) => {
const { details } = error;
if (details.error) {
let message = details.error;
if (details.error.includes('Syntax error:')) {
message =
'Please make sure query is valid and dependent variables are selected';
}
setErrorPreview(message);
}
},
},
);
const handleTestRunQuery = useCallback(() => {
runQuery();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Col>
{/* <Typography.Title level={3}>Add Variable</Typography.Title> */}
<VariableItemRow>
<LabelContainer>
<Typography>Name</Typography>
</LabelContainer>
<div>
<Input
placeholder="Unique name of the variable"
style={{ width: 400 }}
value={variableName}
onChange={(e): void => {
setVariableName(e.target.value);
setErrorName(
!validateName(e.target.value) && e.target.value !== variableData.name,
);
}}
/>
<div className="variable-item-container">
<div className="variable-item-content">
<VariableItemRow>
<LabelContainer>
<Typography>Name</Typography>
</LabelContainer>
<div>
<Typography.Text type="warning">
{errorName ? 'Variable name already exists' : ''}
</Typography.Text>
</div>
</div>
</VariableItemRow>
<VariableItemRow>
<LabelContainer>
<Typography>Description</Typography>
</LabelContainer>
<Input.TextArea
value={variableDescription}
placeholder="Write description of the variable"
style={{ width: 400 }}
onChange={(e): void => setVariableDescription(e.target.value)}
/>
</VariableItemRow>
<VariableItemRow>
<LabelContainer>
<Typography>Type</Typography>
</LabelContainer>
<Select
defaultActiveFirstOption
style={{ width: 400 }}
onChange={(e: TVariableQueryType): void => {
setQueryType(e);
}}
value={queryType}
>
<Option value={VariableQueryTypeArr[0]}>Query</Option>
<Option value={VariableQueryTypeArr[1]}>Textbox</Option>
<Option value={VariableQueryTypeArr[2]}>Custom</Option>
</Select>
</VariableItemRow>
<Typography.Title
level={5}
style={{ marginTop: '1rem', marginBottom: '1rem' }}
>
Options
</Typography.Title>
{queryType === 'QUERY' && (
<VariableItemRow>
<LabelContainer>
<Typography>Query</Typography>
</LabelContainer>
<div style={{ flex: 1, position: 'relative' }}>
<Editor
language="sql"
value={variableQueryValue}
onChange={(e): void => setVariableQueryValue(e)}
height="300px"
/>
<Button
type="primary"
onClick={handleQueryResult}
style={{
position: 'absolute',
bottom: 0,
}}
loading={previewLoading}
>
Test Run Query
</Button>
</div>
</VariableItemRow>
)}
{queryType === 'CUSTOM' && (
<VariableItemRow>
<LabelContainer>
<Typography>Values separated by comma</Typography>
</LabelContainer>
<Input.TextArea
value={variableCustomValue}
placeholder="1, 10, mykey, mykey:myvalue"
style={{ width: 400 }}
onChange={(e): void => {
setVariableCustomValue(e.target.value);
setPreviewValues(
sortValues(
commaValuesParser(e.target.value),
variableSortType,
) as never,
);
}}
/>
</VariableItemRow>
)}
{queryType === 'TEXTBOX' && (
<VariableItemRow>
<LabelContainer>
<Typography>Default Value</Typography>
</LabelContainer>
<Input
value={variableTextboxValue}
onChange={(e): void => {
setVariableTextboxValue(e.target.value);
}}
placeholder="Default value if any"
style={{ width: 400 }}
/>
</VariableItemRow>
)}
{(queryType === 'QUERY' || queryType === 'CUSTOM') && (
<>
<VariableItemRow>
<LabelContainer>
<Typography>Preview of Values</Typography>
</LabelContainer>
<div style={{ flex: 1 }}>
{errorPreview ? (
<Typography style={{ color: orange[5] }}>{errorPreview}</Typography>
) : (
map(previewValues, (value, idx) => (
<Tag key={`${value}${idx}`}>{value.toString()}</Tag>
))
)}
</div>
</VariableItemRow>
<VariableItemRow>
<LabelContainer>
<Typography>Sort</Typography>
</LabelContainer>
<Select
defaultActiveFirstOption
<Input
placeholder="Unique name of the variable"
style={{ width: 400 }}
defaultValue={VariableSortTypeArr[0]}
value={variableSortType}
onChange={(value: TSortVariableValuesType): void =>
setVariableSortType(value)
}
>
<Option value={VariableSortTypeArr[0]}>Disabled</Option>
<Option value={VariableSortTypeArr[1]}>Ascending</Option>
<Option value={VariableSortTypeArr[2]}>Descending</Option>
</Select>
</VariableItemRow>
value={variableName}
onChange={(e): void => {
setVariableName(e.target.value);
setErrorName(
!validateName(e.target.value) && e.target.value !== variableData.name,
);
}}
/>
<div>
<Typography.Text type="warning">
{errorName ? 'Variable name already exists' : ''}
</Typography.Text>
</div>
</div>
</VariableItemRow>
<VariableItemRow>
<LabelContainer>
<Typography>Description</Typography>
</LabelContainer>
<Input.TextArea
value={variableDescription}
placeholder="Write description of the variable"
style={{ width: 400 }}
onChange={(e): void => setVariableDescription(e.target.value)}
/>
</VariableItemRow>
<VariableItemRow>
<LabelContainer>
<Typography>Type</Typography>
</LabelContainer>
<Select
defaultActiveFirstOption
style={{ width: 400 }}
onChange={(e: TVariableQueryType): void => {
setQueryType(e);
}}
value={queryType}
>
<Option value={VariableQueryTypeArr[0]}>Query</Option>
<Option value={VariableQueryTypeArr[1]}>Textbox</Option>
<Option value={VariableQueryTypeArr[2]}>Custom</Option>
</Select>
</VariableItemRow>
<Typography.Title
level={5}
style={{ marginTop: '1rem', marginBottom: '1rem' }}
>
Options
</Typography.Title>
{queryType === 'QUERY' && (
<div className="query-container">
<LabelContainer>
<Typography>Query</Typography>
</LabelContainer>
<div style={{ flex: 1, position: 'relative' }}>
<Editor
language="sql"
value={variableQueryValue}
onChange={(e): void => setVariableQueryValue(e)}
height="240px"
options={{
fontSize: 13,
wordWrap: 'on',
lineNumbers: 'off',
glyphMargin: false,
folding: false,
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
minimap: {
enabled: false,
},
}}
/>
<Button
type="primary"
size="small"
onClick={handleTestRunQuery}
style={{
position: 'absolute',
bottom: 0,
}}
loading={previewLoading}
>
Test Run Query
</Button>
</div>
</div>
)}
{queryType === 'CUSTOM' && (
<VariableItemRow>
<LabelContainer>
<Typography>Enable multiple values to be checked</Typography>
<Typography>Values separated by comma</Typography>
</LabelContainer>
<Switch
checked={variableMultiSelect}
<Input.TextArea
value={variableCustomValue}
placeholder="1, 10, mykey, mykey:myvalue"
style={{ width: 400 }}
onChange={(e): void => {
setVariableMultiSelect(e);
if (!e) {
setVariableShowALLOption(false);
}
setVariableCustomValue(e.target.value);
setPreviewValues(
sortValues(
commaValuesParser(e.target.value),
variableSortType,
) as never,
);
}}
/>
</VariableItemRow>
{variableMultiSelect && (
)}
{queryType === 'TEXTBOX' && (
<VariableItemRow>
<LabelContainer>
<Typography>Default Value</Typography>
</LabelContainer>
<Input
value={variableTextboxValue}
onChange={(e): void => {
setVariableTextboxValue(e.target.value);
}}
placeholder="Default value if any"
style={{ width: 400 }}
/>
</VariableItemRow>
)}
{(queryType === 'QUERY' || queryType === 'CUSTOM') && (
<>
<VariableItemRow>
<LabelContainer>
<Typography>Include an option for ALL values</Typography>
<Typography>Preview of Values</Typography>
</LabelContainer>
<div style={{ flex: 1 }}>
{errorPreview ? (
<Typography style={{ color: orange[5] }}>{errorPreview}</Typography>
) : (
map(previewValues, (value, idx) => (
<Tag key={`${value}${idx}`}>{value.toString()}</Tag>
))
)}
</div>
</VariableItemRow>
<VariableItemRow>
<LabelContainer>
<Typography>Sort</Typography>
</LabelContainer>
<Select
defaultActiveFirstOption
style={{ width: 400 }}
defaultValue={VariableSortTypeArr[0]}
value={variableSortType}
onChange={(value: TSortVariableValuesType): void =>
setVariableSortType(value)
}
>
<Option value={VariableSortTypeArr[0]}>Disabled</Option>
<Option value={VariableSortTypeArr[1]}>Ascending</Option>
<Option value={VariableSortTypeArr[2]}>Descending</Option>
</Select>
</VariableItemRow>
<VariableItemRow>
<LabelContainer>
<Typography>Enable multiple values to be checked</Typography>
</LabelContainer>
<Switch
checked={variableShowALLOption}
onChange={(e): void => setVariableShowALLOption(e)}
checked={variableMultiSelect}
onChange={(e): void => {
setVariableMultiSelect(e);
if (!e) {
setVariableShowALLOption(false);
}
}}
/>
</VariableItemRow>
)}
</>
)}
<Divider />
<VariableItemRow>
<Button type="primary" onClick={handleSave} disabled={errorName}>
Save
</Button>
<Button type="dashed" onClick={onCancel}>
Cancel
</Button>
</VariableItemRow>
</Col>
{variableMultiSelect && (
<VariableItemRow>
<LabelContainer>
<Typography>Include an option for ALL values</Typography>
</LabelContainer>
<Switch
checked={variableShowALLOption}
onChange={(e): void => setVariableShowALLOption(e)}
/>
</VariableItemRow>
)}
</>
)}
</div>
<div className="variable-item-footer">
<Divider />
<VariableItemRow>
<Button type="primary" onClick={handleSave} disabled={errorName}>
Save
</Button>
<Button type="default" onClick={onCancel}>
Cancel
</Button>
</VariableItemRow>
</div>
</div>
);
}

View File

@@ -1,19 +1,78 @@
import '../DashboardSettings.styles.scss';
import { blue, red } from '@ant-design/colors';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Modal, Row, Space, Tag } from 'antd';
import { ResizeTable } from 'components/ResizeTable';
import { MenuOutlined, PlusOutlined } from '@ant-design/icons';
import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
import {
DndContext,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable';
// eslint-disable-next-line import/no-extraneous-dependencies
import { CSS } from '@dnd-kit/utilities';
import { Button, Modal, Row, Space, Table, Typography } from 'antd';
import { RowProps } from 'antd/lib';
import { convertVariablesToDbFormat } from 'container/NewDashboard/DashboardVariablesSelection/util';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications';
import { PencilIcon, TrashIcon } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import { TVariableViewMode } from './types';
import { TVariableMode } from './types';
import VariableItem from './VariableItem/VariableItem';
function TableRow({ children, ...props }: RowProps): JSX.Element {
const {
attributes,
listeners,
setNodeRef,
setActivatorNodeRef,
transform,
transition,
isDragging,
} = useSortable({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
id: props['data-row-key'],
});
const style: React.CSSProperties = {
...props.style,
transform: CSS.Transform.toString(transform && { ...transform, scaleY: 1 }),
transition,
...(isDragging ? { position: 'relative', zIndex: 9999 } : {}),
};
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<tr {...props} ref={setNodeRef} style={style} {...attributes}>
{React.Children.map(children, (child) => {
if ((child as React.ReactElement).key === 'sort') {
return React.cloneElement(child as React.ReactElement, {
children: (
<MenuOutlined
ref={setActivatorNodeRef}
style={{ touchAction: 'none', cursor: 'move' }}
// eslint-disable-next-line react/jsx-props-no-spreading
{...listeners}
/>
),
});
}
return child;
})}
</tr>
);
}
function VariablesSetting(): JSX.Element {
const variableToDelete = useRef<string | null>(null);
const variableToDelete = useRef<IDashboardVariable | null>(null);
const [deleteVariableModal, setDeleteVariableModal] = useState(false);
const { t } = useTranslation(['dashboard']);
@@ -24,16 +83,15 @@ function VariablesSetting(): JSX.Element {
const { variables = {} } = selectedDashboard?.data || {};
const variablesTableData = Object.keys(variables).map((variableName) => ({
key: variableName,
name: variableName,
...variables[variableName],
}));
const [variablesTableData, setVariablesTableData] = useState<any>([]);
const [variblesOrderArr, setVariablesOrderArr] = useState<number[]>([]);
const [existingVariableNamesMap, setExistingVariableNamesMap] = useState<
Record<string, string>
>({});
const [
variableViewMode,
setVariableViewMode,
] = useState<null | TVariableViewMode>(null);
const [variableViewMode, setVariableViewMode] = useState<null | TVariableMode>(
null,
);
const [
variableEditData,
@@ -46,7 +104,7 @@ function VariablesSetting(): JSX.Element {
};
const onVariableViewModeEnter = (
viewType: TVariableViewMode,
viewType: TVariableMode,
varData: IDashboardVariable,
): void => {
setVariableEditData(varData);
@@ -55,6 +113,41 @@ function VariablesSetting(): JSX.Element {
const updateMutation = useUpdateDashboard();
useEffect(() => {
const tableRowData = [];
const variableOrderArr = [];
const variableNamesMap = {};
// eslint-disable-next-line no-restricted-syntax
for (const [key, value] of Object.entries(variables)) {
const { order, id, name } = value;
tableRowData.push({
key,
name: key,
...variables[key],
id,
});
if (name) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
variableNamesMap[name] = name;
}
if (order) {
variableOrderArr.push(order);
}
}
tableRowData.sort((a, b) => a.order - b.order);
variableOrderArr.sort((a, b) => a - b);
setVariablesTableData(tableRowData);
setVariablesOrderArr(variableOrderArr);
setExistingVariableNamesMap(variableNamesMap);
}, [variables]);
const updateVariables = (
updatedVariablesData: Dashboard['data']['variables'],
): void => {
@@ -88,34 +181,58 @@ function VariablesSetting(): JSX.Element {
);
};
const getVariableOrder = (): number => {
if (variblesOrderArr && variblesOrderArr.length > 0) {
return variblesOrderArr[variblesOrderArr.length - 1] + 1;
}
return 0;
};
const onVariableSaveHandler = (
name: string,
mode: TVariableMode,
variableData: IDashboardVariable,
oldName: string,
): void => {
if (!variableData.name) {
return;
const updatedVariableData = {
...variableData,
order: variableData?.order >= 0 ? variableData.order : getVariableOrder(),
};
const newVariablesArr = variablesTableData.map(
(variable: IDashboardVariable) => {
if (variable.id === updatedVariableData.id) {
return updatedVariableData;
}
return variable;
},
);
if (mode === 'ADD') {
newVariablesArr.push(updatedVariableData);
}
const newVariables = { ...variables };
newVariables[name] = variableData;
const variables = convertVariablesToDbFormat(newVariablesArr);
if (oldName) {
delete newVariables[oldName];
}
updateVariables(newVariables);
setVariablesTableData(newVariablesArr);
updateVariables(variables);
onDoneVariableViewMode();
};
const onVariableDeleteHandler = (variableName: string): void => {
variableToDelete.current = variableName;
const onVariableDeleteHandler = (variable: IDashboardVariable): void => {
variableToDelete.current = variable;
setDeleteVariableModal(true);
};
const handleDeleteConfirm = (): void => {
const newVariables = { ...variables };
if (variableToDelete?.current) delete newVariables[variableToDelete?.current];
updateVariables(newVariables);
const newVariablesArr = variablesTableData.filter(
(variable: IDashboardVariable) =>
variable.id !== variableToDelete?.current?.id,
);
const updatedVariables = convertVariablesToDbFormat(newVariablesArr);
updateVariables(updatedVariables);
variableToDelete.current = null;
setDeleteVariableModal(false);
};
@@ -124,48 +241,100 @@ function VariablesSetting(): JSX.Element {
setDeleteVariableModal(false);
};
const validateVariableName = (name: string): boolean => !variables[name];
const validateVariableName = (name: string): boolean =>
!existingVariableNamesMap[name];
const columns = [
{
key: 'sort',
width: '10%',
},
{
title: 'Variable',
dataIndex: 'name',
width: 100,
width: '40%',
key: 'name',
},
{
title: 'Definition',
title: 'Description',
dataIndex: 'description',
width: 100,
width: '35%',
key: 'description',
},
{
title: 'Actions',
width: 50,
width: '15%',
key: 'action',
render: (_: IDashboardVariable): JSX.Element => (
render: (variable: IDashboardVariable): JSX.Element => (
<Space>
<Button
type="text"
style={{ padding: 0, cursor: 'pointer', color: blue[5] }}
onClick={(): void => onVariableViewModeEnter('EDIT', _)}
style={{ padding: 8, cursor: 'pointer', color: blue[5] }}
onClick={(): void => onVariableViewModeEnter('EDIT', variable)}
>
Edit
<PencilIcon size={14} />
</Button>
<Button
type="text"
style={{ padding: 0, color: red[6], cursor: 'pointer' }}
style={{ padding: 8, color: red[6], cursor: 'pointer' }}
onClick={(): void => {
if (_.name) onVariableDeleteHandler(_.name);
if (variable) {
onVariableDeleteHandler(variable);
}
}}
>
Delete
<TrashIcon size={14} />
</Button>
</Space>
),
},
];
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
// https://docs.dndkit.com/api-documentation/sensors/pointer#activation-constraints
distance: 1,
},
}),
);
const onDragEnd = ({ active, over }: DragEndEvent): void => {
if (active.id !== over?.id) {
const activeIndex = variablesTableData.findIndex(
(i: { key: UniqueIdentifier }) => i.key === active.id,
);
const overIndex = variablesTableData.findIndex(
(i: { key: UniqueIdentifier | undefined }) => i.key === over?.id,
);
const updatedVariables: IDashboardVariable[] = arrayMove(
variablesTableData,
activeIndex,
overIndex,
);
const reArrangedVariables = {};
for (let index = 0; index < updatedVariables.length; index += 1) {
const variableName = updatedVariables[index].name;
if (variableName) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
reArrangedVariables[variableName] = {
...updatedVariables[index],
order: index,
};
}
}
updateVariables(reArrangedVariables);
setVariablesTableData(updatedVariables);
}
};
return (
<>
{variableViewMode ? (
@@ -175,11 +344,17 @@ function VariablesSetting(): JSX.Element {
onSave={onVariableSaveHandler}
onCancel={onDoneVariableViewMode}
validateName={validateVariableName}
variableViewMode={variableViewMode}
mode={variableViewMode}
/>
) : (
<>
<Row style={{ flexDirection: 'row-reverse', padding: '0.5rem 0' }}>
<Row
style={{
flexDirection: 'row',
justifyContent: 'flex-end',
padding: '0.5rem 0',
}}
>
<Button
data-testid="add-new-variable"
type="primary"
@@ -187,10 +362,32 @@ function VariablesSetting(): JSX.Element {
onVariableViewModeEnter('ADD', {} as IDashboardVariable)
}
>
<PlusOutlined /> New Variables
<PlusOutlined /> Add Variable
</Button>
</Row>
<ResizeTable columns={columns} dataSource={variablesTableData} />
<DndContext
sensors={sensors}
modifiers={[restrictToVerticalAxis]}
onDragEnd={onDragEnd}
>
<SortableContext
// rowKey array
items={variablesTableData.map((variable: { key: any }) => variable.key)}
>
<Table
components={{
body: {
row: TableRow,
},
}}
rowKey="key"
columns={columns}
pagination={false}
dataSource={variablesTableData}
/>
</SortableContext>
</DndContext>
</>
)}
<Modal
@@ -200,8 +397,13 @@ function VariablesSetting(): JSX.Element {
onOk={handleDeleteConfirm}
onCancel={handleDeleteCancel}
>
Are you sure you want to delete variable{' '}
<Tag>{variableToDelete.current}</Tag>?
<Typography.Text>
Are you sure you want to delete variable{' '}
<span className="delete-variable-name">
{variableToDelete?.current?.name}
</span>
?
</Typography.Text>
</Modal>
</>
);

View File

@@ -1 +1,7 @@
export type TVariableViewMode = 'EDIT' | 'ADD';
export type TVariableMode = 'VIEW' | 'EDIT' | 'ADD';
export const VariableModes = {
VIEW: 'VIEW',
EDIT: 'EDIT',
ADD: 'ADD',
};

View File

@@ -13,7 +13,7 @@ function DashboardSettingsContent(): JSX.Element {
{ label: 'Variables', key: 'variables', children: <VariablesSetting /> },
];
return <Tabs items={items} />;
return <Tabs items={items} animated />;
}
export default DashboardSettingsContent;

View File

@@ -0,0 +1,8 @@
.variable-name {
font-size: 0.8rem;
min-width: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: gray;
}

View File

@@ -0,0 +1,150 @@
import { Row } from 'antd';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { convertVariablesToDbFormat } from './util';
import VariableItem from './VariableItem';
function DashboardVariableSelection(): JSX.Element | null {
const { selectedDashboard, setSelectedDashboard } = useDashboard();
const { data } = selectedDashboard || {};
const { variables } = data || {};
const [update, setUpdate] = useState<boolean>(false);
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
const [variablesTableData, setVariablesTableData] = useState<any>([]);
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
useEffect(() => {
if (variables) {
const tableRowData = [];
// eslint-disable-next-line no-restricted-syntax
for (const [key, value] of Object.entries(variables)) {
const { id } = value;
tableRowData.push({
key,
name: key,
...variables[key],
id,
});
}
tableRowData.sort((a, b) => a.order - b.order);
setVariablesTableData(tableRowData);
}
}, [variables]);
const onVarChanged = (name: string): void => {
setLastUpdatedVar(name);
setUpdate(!update);
};
const updateMutation = useUpdateDashboard();
const { notifications } = useNotifications();
const updateVariables = (
name: string,
updatedVariablesData: Dashboard['data']['variables'],
): void => {
if (!selectedDashboard) {
return;
}
updateMutation.mutateAsync(
{
...selectedDashboard,
data: {
...selectedDashboard.data,
variables: updatedVariablesData,
},
},
{
onSuccess: (updatedDashboard) => {
if (updatedDashboard.payload) {
setSelectedDashboard(updatedDashboard.payload);
}
},
onError: () => {
notifications.error({
message: `Error updating ${name} variable`,
});
},
},
);
};
const onValueUpdate = (
name: string,
id: string,
value: IDashboardVariable['selectedValue'],
allSelected: boolean,
): void => {
if (id) {
const newVariablesArr = variablesTableData.map(
(variable: IDashboardVariable) => {
const variableCopy = { ...variable };
if (variableCopy.id === id) {
variableCopy.selectedValue = value;
variableCopy.allSelected = allSelected;
}
return variableCopy;
},
);
const variables = convertVariablesToDbFormat(newVariablesArr);
if (role !== 'VIEWER' && selectedDashboard) {
updateVariables(name, variables);
}
onVarChanged(name);
setUpdate(!update);
}
};
if (!variables) {
return null;
}
const orderBasedSortedVariables = variablesTableData.sort(
(a: { order: number }, b: { order: number }) => a.order - b.order,
);
return (
<Row>
{orderBasedSortedVariables &&
Array.isArray(orderBasedSortedVariables) &&
orderBasedSortedVariables.length > 0 &&
orderBasedSortedVariables.map((variable) => (
<VariableItem
key={`${variable.name}${variable.id}}${variable.order}`}
existingVariables={variables}
lastUpdatedVar={lastUpdatedVar}
variableData={{
name: variable.name,
...variable,
change: update,
}}
onValueUpdate={onValueUpdate}
/>
))}
</Row>
);
}
export default memo(DashboardVariableSelection);

View File

@@ -1,12 +1,20 @@
import '@testing-library/jest-dom/extend-expect';
import { fireEvent, render, screen } from '@testing-library/react';
import {
act,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import React, { useEffect } from 'react';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import VariableItem from './VariableItem';
const mockVariableData: IDashboardVariable = {
id: 'test_variable',
description: 'Test Variable',
type: 'TEXTBOX',
textboxValue: 'defaultValue',
@@ -25,7 +33,6 @@ const mockCustomVariableData: IDashboardVariable = {
};
const mockOnValueUpdate = jest.fn();
const mockOnAllSelectedUpdate = jest.fn();
describe('VariableItem', () => {
let useEffectSpy: jest.SpyInstance;
@@ -41,13 +48,14 @@ describe('VariableItem', () => {
test('renders component with default props', () => {
render(
<VariableItem
variableData={mockVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
onAllSelectedUpdate={mockOnAllSelectedUpdate}
lastUpdatedVar=""
/>,
<MockQueryClientProvider>
<VariableItem
variableData={mockVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
lastUpdatedVar=""
/>
</MockQueryClientProvider>,
);
expect(screen.getByText('$testVariable')).toBeInTheDocument();
@@ -55,45 +63,56 @@ describe('VariableItem', () => {
test('renders Input when the variable type is TEXTBOX', () => {
render(
<VariableItem
variableData={mockVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
onAllSelectedUpdate={mockOnAllSelectedUpdate}
lastUpdatedVar=""
/>,
<MockQueryClientProvider>
<VariableItem
variableData={mockVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
lastUpdatedVar=""
/>
</MockQueryClientProvider>,
);
expect(screen.getByPlaceholderText('Enter value')).toBeInTheDocument();
});
test('calls onChange event handler when Input value changes', () => {
test('calls onChange event handler when Input value changes', async () => {
render(
<VariableItem
variableData={mockVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
onAllSelectedUpdate={mockOnAllSelectedUpdate}
lastUpdatedVar=""
/>,
<MockQueryClientProvider>
<VariableItem
variableData={mockVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
lastUpdatedVar=""
/>
</MockQueryClientProvider>,
);
const inputElement = screen.getByPlaceholderText('Enter value');
fireEvent.change(inputElement, { target: { value: 'newValue' } });
expect(mockOnValueUpdate).toHaveBeenCalledTimes(1);
expect(mockOnValueUpdate).toHaveBeenCalledWith('testVariable', 'newValue');
expect(mockOnAllSelectedUpdate).toHaveBeenCalledTimes(1);
expect(mockOnAllSelectedUpdate).toHaveBeenCalledWith('testVariable', false);
act(() => {
const inputElement = screen.getByPlaceholderText('Enter value');
fireEvent.change(inputElement, { target: { value: 'newValue' } });
});
await waitFor(() => {
// expect(mockOnValueUpdate).toHaveBeenCalledTimes(1);
expect(mockOnValueUpdate).toHaveBeenCalledWith(
'testVariable',
'test_variable',
'newValue',
false,
);
});
});
test('renders a Select element when variable type is CUSTOM', () => {
render(
<VariableItem
variableData={mockCustomVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
onAllSelectedUpdate={mockOnAllSelectedUpdate}
lastUpdatedVar=""
/>,
<MockQueryClientProvider>
<VariableItem
variableData={mockCustomVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
lastUpdatedVar=""
/>
</MockQueryClientProvider>,
);
expect(screen.getByText('$customVariable')).toBeInTheDocument();
@@ -107,13 +126,14 @@ describe('VariableItem', () => {
};
render(
<VariableItem
variableData={customVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
onAllSelectedUpdate={mockOnAllSelectedUpdate}
lastUpdatedVar=""
/>,
<MockQueryClientProvider>
<VariableItem
variableData={customVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
lastUpdatedVar=""
/>
</MockQueryClientProvider>,
);
expect(screen.getByTitle('ALL')).toBeInTheDocument();
@@ -121,48 +141,16 @@ describe('VariableItem', () => {
test('calls useEffect when the component mounts', () => {
render(
<VariableItem
variableData={mockCustomVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
onAllSelectedUpdate={mockOnAllSelectedUpdate}
lastUpdatedVar=""
/>,
<MockQueryClientProvider>
<VariableItem
variableData={mockCustomVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
lastUpdatedVar=""
/>
</MockQueryClientProvider>,
);
expect(useEffect).toHaveBeenCalled();
});
test('calls useEffect only once when the component mounts', () => {
// Render the component
const { rerender } = render(
<VariableItem
variableData={mockCustomVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
onAllSelectedUpdate={mockOnAllSelectedUpdate}
lastUpdatedVar=""
/>,
);
// Create an updated version of the mock data
const updatedMockCustomVariableData = {
...mockCustomVariableData,
selectedValue: 'option1',
};
// Re-render the component with the updated data
rerender(
<VariableItem
variableData={updatedMockCustomVariableData}
existingVariables={{}}
onValueUpdate={mockOnValueUpdate}
onAllSelectedUpdate={mockOnAllSelectedUpdate}
lastUpdatedVar=""
/>,
);
// Check if the useEffect is called with the correct arguments
expect(useEffectSpy).toHaveBeenCalledTimes(4);
});
});

View File

@@ -1,27 +1,37 @@
import './DashboardVariableSelection.styles.scss';
import { orange } from '@ant-design/colors';
import { WarningOutlined } from '@ant-design/icons';
import { Input, Popover, Select, Typography } from 'antd';
import query from 'api/dashboard/variables/query';
import { Input, Popover, Select, Tooltip, Typography } from 'antd';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useDebounce from 'hooks/useDebounce';
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
import map from 'lodash-es/map';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { VariableResponseProps } from 'types/api/dashboard/variables/query';
import { variablePropsToPayloadVariables } from '../utils';
import { SelectItemStyle, VariableContainer, VariableName } from './styles';
import { SelectItemStyle, VariableContainer, VariableValue } from './styles';
import { areArraysEqual } from './util';
const ALL_SELECT_VALUE = '__ALL__';
const variableRegexPattern = /\{\{\s*?\.([^\s}]+)\s*?\}\}/g;
interface VariableItemProps {
variableData: IDashboardVariable;
existingVariables: Record<string, IDashboardVariable>;
onValueUpdate: (
name: string,
id: string,
arg1: IDashboardVariable['selectedValue'],
allSelected: boolean,
) => void;
onAllSelectedUpdate: (name: string, arg1: boolean) => void;
lastUpdatedVar: string;
}
@@ -38,48 +48,75 @@ function VariableItem({
variableData,
existingVariables,
onValueUpdate,
onAllSelectedUpdate,
lastUpdatedVar,
}: VariableItemProps): JSX.Element {
const { isDashboardLocked } = useDashboard();
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
[],
);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [variableValue, setVaribleValue] = useState(
variableData?.selectedValue?.toString() || '',
);
const debouncedVariableValue = useDebounce(variableValue, 500);
const [errorMessage, setErrorMessage] = useState<null | string>(null);
/* eslint-disable sonarjs/cognitive-complexity */
const getOptions = useCallback(async (): Promise<void> => {
if (variableData.type === 'QUERY') {
useEffect(() => {
const { selectedValue } = variableData;
if (selectedValue) {
setVaribleValue(selectedValue?.toString());
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [variableData]);
const getDependentVariables = (queryValue: string): string[] => {
const matches = queryValue.match(variableRegexPattern);
// Extract variable names from the matches array without {{ . }}
return matches
? matches.map((match) => match.replace(variableRegexPattern, '$1'))
: [];
};
const getQueryKey = (variableData: IDashboardVariable): string[] => {
let dependentVariablesStr = '';
const dependentVariables = getDependentVariables(
variableData.queryValue || '',
);
const variableName = variableData.name || '';
dependentVariables?.forEach((element) => {
dependentVariablesStr += `${element}${existingVariables[element]?.selectedValue}`;
});
const variableKey = dependentVariablesStr.replace(/\s/g, '');
return [REACT_QUERY_KEY.DASHBOARD_BY_ID, variableName, variableKey];
};
// eslint-disable-next-line sonarjs/cognitive-complexity
const getOptions = (variablesRes: VariableResponseProps | null): void => {
if (variablesRes && variableData.type === 'QUERY') {
try {
setErrorMessage(null);
setIsLoading(true);
const response = await query({
query: variableData.queryValue || '',
variables: variablePropsToPayloadVariables(existingVariables),
});
setIsLoading(false);
if (response.error) {
let message = response.error;
if (response.error.includes('Syntax error:')) {
message =
'Please make sure query is valid and dependent variables are selected';
}
setErrorMessage(message);
return;
}
if (response.payload?.variableValues) {
if (
variablesRes?.variableValues &&
Array.isArray(variablesRes?.variableValues)
) {
const newOptionsData = sortValues(
response.payload?.variableValues,
variablesRes?.variableValues,
variableData.sort,
);
// Since there is a chance of a variable being dependent on other
// variables, we need to check if the optionsData has changed
// If it has changed, we need to update the dependent variable
// So we compare the new optionsData with the old optionsData
const oldOptionsData = sortValues(optionsData, variableData.sort) as never;
if (!areArraysEqual(newOptionsData, oldOptionsData)) {
/* eslint-disable no-useless-escape */
const re = new RegExp(`\\{\\{\\s*?\\.${lastUpdatedVar}\\s*?\\}\\}`); // regex for `{{.var}}`
@@ -103,11 +140,12 @@ function VariableItem({
} else {
[value] = newOptionsData;
}
if (variableData.name) {
onValueUpdate(variableData.name, value);
onAllSelectedUpdate(variableData.name, allSelected);
if (variableData && variableData?.name && variableData?.id) {
onValueUpdate(variableData.name, variableData.id, value, allSelected);
}
}
setOptionsData(newOptionsData);
}
}
@@ -115,26 +153,43 @@ function VariableItem({
console.error(e);
}
} else if (variableData.type === 'CUSTOM') {
setOptionsData(
sortValues(
commaValuesParser(variableData.customValue || ''),
variableData.sort,
) as never,
);
}
}, [
variableData,
existingVariables,
onValueUpdate,
onAllSelectedUpdate,
optionsData,
lastUpdatedVar,
]);
const optionsData = sortValues(
commaValuesParser(variableData.customValue || ''),
variableData.sort,
) as never;
useEffect(() => {
getOptions();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [variableData, existingVariables]);
setOptionsData(optionsData);
}
};
const { isLoading } = useQuery(getQueryKey(variableData), {
enabled: variableData && variableData.type === 'QUERY',
queryFn: () =>
dashboardVariablesQuery({
query: variableData.queryValue || '',
variables: variablePropsToPayloadVariables(existingVariables),
}),
refetchOnWindowFocus: false,
onSuccess: (response) => {
getOptions(response.payload);
},
onError: (error: {
details: {
error: string;
};
}) => {
const { details } = error;
if (details.error) {
let message = details.error;
if (details.error.includes('Syntax error:')) {
message =
'Please make sure query is valid and dependent variables are selected';
}
setErrorMessage(message);
}
},
});
const handleChange = (value: string | string[]): void => {
if (variableData.name)
@@ -143,11 +198,9 @@ function VariableItem({
(Array.isArray(value) && value.includes(ALL_SELECT_VALUE)) ||
(Array.isArray(value) && value.length === 0)
) {
onValueUpdate(variableData.name, optionsData);
onAllSelectedUpdate(variableData.name, true);
onValueUpdate(variableData.name, variableData.id, optionsData, true);
} else {
onValueUpdate(variableData.name, value);
onAllSelectedUpdate(variableData.name, false);
onValueUpdate(variableData.name, variableData.id, value, false);
}
};
@@ -165,62 +218,94 @@ function VariableItem({
? 'multiple'
: undefined;
const enableSelectAll = variableData.multiSelect && variableData.showALLOption;
useEffect(() => {
if (debouncedVariableValue !== variableData?.selectedValue?.toString()) {
handleChange(debouncedVariableValue);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedVariableValue]);
useEffect(() => {
// Fetch options for CUSTOM Type
if (variableData.type === 'CUSTOM') {
getOptions(null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [variableData.type, variableData.customValue]);
return (
<VariableContainer>
<VariableName>${variableData.name}</VariableName>
{variableData.type === 'TEXTBOX' ? (
<Input
placeholder="Enter value"
bordered={false}
value={variableData.selectedValue?.toString()}
onChange={(e): void => {
handleChange(e.target.value || '');
}}
style={{
width:
50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50),
}}
/>
) : (
!errorMessage && (
<Select
value={selectValue}
onChange={handleChange}
bordered={false}
placeholder="Select value"
mode={mode}
dropdownMatchSelectWidth={false}
style={SelectItemStyle}
loading={isLoading}
showArrow
showSearch
data-testid="variable-select"
>
{enableSelectAll && (
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
ALL
</Select.Option>
)}
{map(optionsData, (option) => (
<Select.Option
data-testid={`option-${option}`}
key={option.toString()}
value={option}
<Tooltip
placement="top"
title={isDashboardLocked ? 'Dashboard is locked' : ''}
>
<VariableContainer>
<Typography.Text className="variable-name" ellipsis>
${variableData.name}
</Typography.Text>
<VariableValue>
{variableData.type === 'TEXTBOX' ? (
<Input
placeholder="Enter value"
disabled={isDashboardLocked}
bordered={false}
value={variableValue}
onChange={(e): void => {
setVaribleValue(e.target.value || '');
}}
style={{
width:
50 + ((variableData.selectedValue?.toString()?.length || 0) * 7 || 50),
}}
/>
) : (
!errorMessage &&
optionsData && (
<Select
value={selectValue}
onChange={handleChange}
bordered={false}
placeholder="Select value"
mode={mode}
dropdownMatchSelectWidth={false}
style={SelectItemStyle}
loading={isLoading}
showArrow
showSearch
data-testid="variable-select"
disabled={isDashboardLocked}
>
{option.toString()}
</Select.Option>
))}
</Select>
)
)}
{errorMessage && (
<span style={{ margin: '0 0.5rem' }}>
<Popover placement="top" content={<Typography>{errorMessage}</Typography>}>
<WarningOutlined style={{ color: orange[5] }} />
</Popover>
</span>
)}
</VariableContainer>
{enableSelectAll && (
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
ALL
</Select.Option>
)}
{map(optionsData, (option) => (
<Select.Option
data-testid={`option-${option}`}
key={option.toString()}
value={option}
>
{option.toString()}
</Select.Option>
))}
</Select>
)
)}
{variableData.type !== 'TEXTBOX' && errorMessage && (
<span style={{ margin: '0 0.5rem' }}>
<Popover
placement="top"
content={<Typography>{errorMessage}</Typography>}
>
<WarningOutlined style={{ color: orange[5] }} />
</Popover>
</span>
)}
</VariableValue>
</VariableContainer>
</Tooltip>
);
}

View File

@@ -1,117 +1,3 @@
import { Row } from 'antd';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications';
import { map, sortBy } from 'lodash-es';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { memo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import DashboardVariableSelection from './DashboardVariableSelection';
import VariableItem from './VariableItem';
function DashboardVariableSelection(): JSX.Element | null {
const { selectedDashboard, setSelectedDashboard } = useDashboard();
const { data } = selectedDashboard || {};
const { variables } = data || {};
const [update, setUpdate] = useState<boolean>(false);
const [lastUpdatedVar, setLastUpdatedVar] = useState<string>('');
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const onVarChanged = (name: string): void => {
setLastUpdatedVar(name);
setUpdate(!update);
};
const updateMutation = useUpdateDashboard();
const { notifications } = useNotifications();
const updateVariables = (
updatedVariablesData: Dashboard['data']['variables'],
): void => {
if (!selectedDashboard) {
return;
}
updateMutation.mutateAsync(
{
...selectedDashboard,
data: {
...selectedDashboard.data,
variables: updatedVariablesData,
},
},
{
onSuccess: (updatedDashboard) => {
if (updatedDashboard.payload) {
setSelectedDashboard(updatedDashboard.payload);
notifications.success({
message: 'Variable updated successfully',
});
}
},
onError: () => {
notifications.error({
message: 'Error while updating variable',
});
},
},
);
};
const onValueUpdate = (
name: string,
value: IDashboardVariable['selectedValue'],
): void => {
const updatedVariablesData = { ...variables };
updatedVariablesData[name].selectedValue = value;
if (role !== 'VIEWER' && selectedDashboard) {
updateVariables(updatedVariablesData);
}
onVarChanged(name);
};
const onAllSelectedUpdate = (
name: string,
value: IDashboardVariable['allSelected'],
): void => {
const updatedVariablesData = { ...variables };
updatedVariablesData[name].allSelected = value;
if (role !== 'VIEWER') {
updateVariables(updatedVariablesData);
}
onVarChanged(name);
};
if (!variables) {
return null;
}
return (
<Row>
{map(sortBy(Object.keys(variables)), (variableName) => (
<VariableItem
key={`${variableName}${variables[variableName].modificationUUID}`}
existingVariables={variables}
variableData={{
name: variableName,
...variables[variableName],
change: update,
}}
onValueUpdate={onValueUpdate}
onAllSelectedUpdate={onAllSelectedUpdate}
lastUpdatedVar={lastUpdatedVar}
/>
))}
</Row>
);
}
export default memo(DashboardVariableSelection);
export default DashboardVariableSelection;

View File

@@ -3,19 +3,40 @@ import { Typography } from 'antd';
import styled from 'styled-components';
export const VariableContainer = styled.div`
max-width: 100%;
border: 1px solid ${grey[1]}66;
border-radius: 2px;
padding: 0;
padding-left: 0.5rem;
margin-right: 8px;
display: flex;
align-items: center;
margin-bottom: 0.3rem;
gap: 4px;
padding: 4px;
`;
export const VariableName = styled(Typography)`
font-size: 0.8rem;
font-style: italic;
color: ${grey[0]};
min-width: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
`;
export const VariableValue = styled(Typography)`
font-size: 0.8rem;
color: ${grey[0]};
flex: 1;
display: flex;
justify-content: flex-end;
align-items: center;
max-width: 300px;
`;
export const SelectItemStyle = {

View File

@@ -1,3 +1,5 @@
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
export function areArraysEqual(
a: (string | number | boolean)[],
b: (string | number | boolean)[],
@@ -14,3 +16,16 @@ export function areArraysEqual(
return true;
}
export const convertVariablesToDbFormat = (
variblesArr: IDashboardVariable[],
): Dashboard['data']['variables'] =>
variblesArr.reduce((result, obj: IDashboardVariable) => {
const { id } = obj;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// eslint-disable-next-line no-param-reassign
result[id] = obj;
return result;
}, {});

View File

@@ -6,8 +6,10 @@ export function variablePropsToPayloadVariables(
): PayloadVariables {
const payloadVariables: PayloadVariables = {};
Object.entries(variables).forEach(([key, value]) => {
payloadVariables[key] = value?.selectedValue;
Object.entries(variables).forEach(([, value]) => {
if (value?.name) {
payloadVariables[value.name] = value?.selectedValue;
}
});
return payloadVariables;

View File

@@ -6,13 +6,16 @@ import { useResizeObserver } from 'hooks/useDimensions';
import useUrlQuery from 'hooks/useUrlQuery';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useCallback, useMemo, useRef } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { UseQueryResult } from 'react-query';
import { useDispatch } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { 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 { getTimeRange } from 'utils/getTimeRange';
function WidgetGraph({
getWidgetQueryRange,
@@ -23,6 +26,21 @@ function WidgetGraph({
}: WidgetGraphProps): JSX.Element {
const { stagedQuery } = useQueryBuilder();
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
useEffect((): void => {
const { startTime, endTime } = getTimeRange(getWidgetQueryRange);
setMinTimeScale(startTime);
setMaxTimeScale(endTime);
}, [getWidgetQueryRange, maxTime, minTime, globalSelectedInterval]);
const graphRef = useRef<HTMLDivElement>(null);
const containerDimensions = useResizeObserver(graphRef);
@@ -63,6 +81,8 @@ function WidgetGraph({
onDragSelect,
thresholds,
fillSpans,
minTimeScale,
maxTimeScale,
}),
[
widgetId,
@@ -73,6 +93,8 @@ function WidgetGraph({
onDragSelect,
thresholds,
fillSpans,
minTimeScale,
maxTimeScale,
],
);

View File

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

View File

@@ -6,6 +6,8 @@ import {
CategoryNames,
DataFormats,
DataRateFormats,
HelperCategory,
HelperFormat,
MiscellaneousFormats,
ThroughputFormats,
TimeFormats,
@@ -76,6 +78,7 @@ export const alertsCategory = [
name: CategoryNames.Miscellaneous,
formats: [
{ name: 'Percent (0.0-1.0)', id: MiscellaneousFormats.PercentUnit },
{ name: 'Percent (0 - 100)', id: MiscellaneousFormats.Percent },
],
},
{
@@ -119,3 +122,10 @@ export const getCategoryByOptionId = (id: string): Category | undefined =>
export const isCategoryName = (name: string): name is CategoryNames =>
alertsCategory.some((category) => category.name === name);
const allFormats: HelperFormat[] = alertsCategory.flatMap(
(category: HelperCategory) => category.formats,
);
export const getFormatNameByOptionId = (id: string): string | undefined =>
allFormats.find((format) => format.id === id)?.name;

View File

@@ -107,48 +107,8 @@ function RightContainer({
}
/>
{/* <TextContainer>
<Typography>Stacked Graphs :</Typography>
<Switch
checked={stacked}
onChange={(): void => {
setStacked((value) => !value);
}}
/>
</TextContainer> */}
{/* <Title light={'true'}>Fill Opacity: </Title> */}
{/* <Slider
value={parseInt(opacity, 10)}
marks={{
0: '0',
33: '33',
66: '66',
100: '100',
}}
onChange={(number): void => onChangeHandler(setOpacity, number.toString())}
step={1}
/> */}
{/* <Title light={'true'}>Null/Zero values: </Title>
<NullButtonContainer>
{nullValueButtons.map((button) => (
<Button
type={button.check === selectedNullZeroValue ? 'primary' : 'default'}
key={button.name}
onClick={(): void =>
onChangeHandler(setSelectedNullZeroValue, button.check)
}
>
{button.name}
</Button>
))}
</NullButtonContainer> */}
<Space style={{ marginTop: 10 }} direction="vertical">
<Typography>Fill span gaps</Typography>
<Typography>Fill gaps</Typography>
<Switch
checked={isFillSpans}

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