Compare commits

...

68 Commits

Author SHA1 Message Date
Prashant Shahi
8809105a8d Prashant/deploy changes (#955)
- set information log level in clickhouse logger config
- maximum logs size 150m (3 files each of 50m)

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2022-04-06 00:05:05 +05:30
palash-signoz
064c3e0449 chore: default text is updated (#954)
* text and title is updated
2022-04-05 23:13:12 +05:30
palash-signoz
2a348e916c feat: version page is added (#924)
* feat👔 : getLatestVersion api is added

* chore: VERSION page is added

* feat:  version page is added

* chore: all string is grabbed from locale

* chore: warning is removed

* chore: translation json is added

* chore: feedback about version is added

* chore: made two different functions

* unused import is removed

* feat: version changes are updated

* chore: if current version is present then it is displayed
2022-04-05 18:21:25 +05:30
palash-signoz
5744193f50 Merge pull request #953 from pranshuchittora/pranshuchittora/fix/trace-detail-error-span-color
fix: error color for spans having error on trace detail page
2022-04-05 18:00:08 +05:30
Pranshu Chittora
ccf352f2db fix: error color for spans having error on trace detail page 2022-04-05 17:02:39 +05:30
palash-signoz
6e446dc0ab Merge pull request #949 from pranshuchittora/pranshuchittora/feat/ttl-s3
feat(FE): TTL/s3 integration
2022-04-05 16:28:41 +05:30
Pranshu Chittora
566c2becdf feat: dynamic step size for the data for graphs (#929)
* feat: dynamic step size for the data for graphs

* fix: remove console.log

* chore: add jest globals

* feat: add step size for dashboard

* chore: undo .eslintignore
2022-04-05 16:09:57 +05:30
Pranshu Chittora
3b3fd2b3a9 chore: update type 2022-04-05 16:09:04 +05:30
Pranshu Chittora
eae53d9eff feat: condition changes 2022-04-05 16:05:46 +05:30
Pranshu Chittora
42842b6b17 feat: i18n support for setting route names 2022-04-05 15:51:38 +05:30
Pranshu Chittora
95f8dfb4bc feat: i18n support for settings page 2022-04-05 15:44:01 +05:30
palash-signoz
a8c5934fc5 fix: Fix jest (#945)
* bug: jest is now fixed

* chore: files are included for the eslint

* chore: build is fixed

* test: jest test are fixed
2022-04-05 14:47:37 +05:30
palash-signoz
3f2a4d6eac bug: Trace filter page fixes (#846)
* order is added in the url
* local min max duration is kept in memory to show min and max even after filtering by duration
* checkbox ordering does not change when the user selects or un-selects a checkbox
2022-04-05 13:23:08 +05:30
Pranshu Chittora
170609a81f chore: type changes 2022-04-05 12:26:43 +05:30
Pranshu Chittora
76fccbbba4 fix: styling changes 2022-04-05 12:19:54 +05:30
palash-signoz
147ed9f24b chore: editor config is added (#818) 2022-04-05 11:19:06 +05:30
palash-signoz
a69bc321a9 Merge pull request #935 from pranshuchittora/pranshuchittora/feat/graph-memory-issue
feat: FE memory fixes and UX enhancements
2022-04-05 10:35:08 +05:30
Pranshu Chittora
c9e02a8b25 feat: final touches to the ttl 2022-04-05 00:06:13 +05:30
Pranshu Chittora
24d6a1e7b2 feat: s3 ttl validation 2022-04-04 19:38:23 +05:30
Nishidh Jain
a0efa63185 Fix(FE) : Ask for confirmation before deleting any dashboard from dashboard list (#534)
* A confirmation dialog will pop up before deleting any dashboard

Co-authored-by: Palash gupta <palash@signoz.io>
2022-04-04 17:35:44 +05:30
Ankit Nayan
fd83cea9a0 chore: removing version api from being tracked (#950) 2022-04-04 17:07:21 +05:30
Pranshu Chittora
5be1eb58b2 feat: ttl api integration 2022-04-04 15:26:29 +05:30
Pranshu Chittora
8367c106bc Merge branch 'develop' of github.com:SigNoz/signoz into pranshuchittora/feat/ttl-s3 2022-04-04 15:06:33 +05:30
Pranshu Chittora
8064ae1f37 feat: ttl api integration 2022-04-04 15:06:06 +05:30
palash-signoz
ab4d9af442 Merge pull request #928 from palash-signoz/tag-value-suggestion
feat: Tag value suggestion
2022-04-04 12:52:34 +05:30
Palash gupta
eb0d3374d5 Merge branch 'develop' into tag-value-suggestion 2022-04-04 12:35:50 +05:30
palash-signoz
6c4c814b3f bug: pathname check is added (#948) 2022-04-04 10:25:15 +05:30
Palash gupta
32e8e48928 chore: behaviour for dropdown is updated 2022-04-04 08:24:28 +05:30
Naman Jain
53e7037f48 fix: run go vet to fix some issues with json tag (#936)
Co-authored-by: Naman Jain <jain_n@apple.com>
2022-04-02 16:15:03 +05:30
palash-signoz
a566b5dc97 bug: no service and loading check are added (#934) 2022-04-01 17:59:44 +05:30
Pranshu Chittora
4dc668fd13 fix: remove unused props 2022-04-01 17:43:56 +05:30
palash-signoz
d085506d3e bug: logged in check is added in the useEffect (#921) 2022-04-01 15:47:39 +05:30
palash-signoz
1b28a4e6f5 chore: links are updated for all dashboard and promql (#908) 2022-04-01 15:43:58 +05:30
Pranshu Chittora
20e924b116 feat: S3 TTL support 2022-04-01 15:12:30 +05:30
Ahsan Barkati
1d28ceb3d7 feat(query-service): Add cold storage support in getTTL API (#922)
* Add cold storage support in getTTL API
2022-04-01 11:22:25 +05:30
Prashant Shahi
1002ab553e chore(release): 📌 pin 0.7.4 SigNoz version and deployment changes
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2022-03-29 22:59:32 +05:30
Amol Umbark
3dc94c8da7 (fix): Duplicate alerts in triggered alerts (#932)
* (fix): Duplicate alerts in triggered alerts fixed by changing source api from /alert/groups to /alerts

* (fix): added comments for removed lines of group api call

* (fix): restored all getGroup
2022-03-29 19:59:40 +05:30
Pranshu Chittora
5a5aca2113 chore: remove unused code 2022-03-29 16:06:27 +05:30
Pranshu Chittora
cb22117a0f Merge branch 'develop' of github.com:SigNoz/signoz into pranshuchittora/feat/graph-memory-issue 2022-03-29 16:05:49 +05:30
Pranshu Chittora
739946fa47 fix: over memory allocation on Graph on big time range 2022-03-29 16:05:08 +05:30
Pranshu Chittora
7939902f03 fix: dashboard table element overflow (#930) 2022-03-29 12:24:03 +05:30
palash-signoz
d34e08fa3d chore: build.yml file is updated for more strict frontend checks (#906)
chore: build.yml file is updated for more strict frontend checks
2022-03-29 11:13:26 +05:30
Palash gupta
5556d1d6fc feat: tag value suggestion is updated 2022-03-29 09:59:50 +05:30
Palash gupta
d4d1104a53 WIP: value suggestion is added 2022-03-29 00:02:56 +05:30
Palash gupta
225a345baa chore: getTagValue api is added 2022-03-29 00:02:16 +05:30
Amol Umbark
0efb901863 feat: Amol/webhook (#868)
webhook receiver enabled for alerts

Co-authored-by: Palash gupta <palash@signoz.io>
2022-03-28 21:01:57 +05:30
palash-signoz
e7ba5f9f33 bug 🐛 : on click tag filter is now fixed (#916) 2022-03-25 19:16:07 +05:30
palash-signoz
995232e057 Merge pull request #914 from palash-signoz/tsc-fix
chore: tsc fix are updated over frontend
2022-03-25 15:39:27 +05:30
Palash gupta
cc5d47e3ee chore: updated the type any 2022-03-25 12:39:26 +05:30
Palash gupta
b1de6c1d7d chore: link is reverted 2022-03-25 12:35:38 +05:30
Palash gupta
84bfe11285 chore: tsc error are fixed 2022-03-25 12:33:52 +05:30
Pranshu Chittora
ca78947a55 fix: save unit on dashboard without hitting apply (#912) 2022-03-25 12:29:40 +05:30
palash-signoz
ac49f84982 Merge pull request #3 from pranshuchittora/pranshuchittora/fix/tsc-fixes
fix: tsc fixes
2022-03-25 12:12:08 +05:30
Pranshu Chittora
cc47f02ebf fix: tsc fixes 2022-03-25 12:03:57 +05:30
Palash gupta
ac70240b72 chore: some tsc fix 2022-03-24 15:39:33 +05:30
palash-signoz
78b1a750fa husky: pre-commit hook is added (#904) 2022-03-24 15:06:57 +05:30
palash-signoz
d5a6336239 Merge pull request #903 from pranshuchittora/pranshuchittora/feat/transformed-labels-on-tooltips
feat(FE): unit label on graph tooltip
2022-03-24 14:23:47 +05:30
palash-signoz
01bad0f18a chore: eslint fix (#884)
* chore: eslint is updated

* chore: some eslint fixes are made

* chore: some more eslint fix are updated

* chore: some eslint fix is made

* chore: styled components type is added

* chore: some more eslint fix are made

* chore: some more eslint fix are updated

* chore: some more eslint fix are updated

* fix: eslint fixes

Co-authored-by: Pranshu Chittora <pranshu@signoz.io>
2022-03-24 12:06:57 +05:30
Pranshu Chittora
1b79a9bf35 feat: unit label on graph tooltip 2022-03-24 11:44:38 +05:30
Prashant Shahi
3d8354fb99 chore(release): 📌 pin 0.7.3 SigNoz version
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2022-03-24 00:52:32 +05:30
Prashant Shahi
696241b962 chore(install-script): 🔧 amazon-linux improvements and fixes (#900)
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2022-03-24 00:46:43 +05:30
Pranshu Chittora
8a883f1b5e feat: unit selection for value graph on dashboard (#898) 2022-03-23 22:03:57 +05:30
palash-signoz
7765cee610 feat: onClick feature is updated (#895) 2022-03-23 19:44:26 +05:30
Ankit Nayan
b958bad81f Merge pull request #897 from palash-signoz/896-monaco-editor-change
chore: onChange event is added
2022-03-23 19:43:59 +05:30
Palash gupta
deff5d5e17 chore: onChange event is added 2022-03-23 19:03:05 +05:30
Ankit Nayan
44d3f35a5f Merge branch 'release/v0.7.2' into develop 2022-03-23 12:41:13 +05:30
palash-signoz
897c5d2371 Merge pull request #890 from pranshuchittora/pranshuchittora/fix/832
fix: top endpoints table overflow
2022-03-23 11:48:09 +05:30
Pranshu Chittora
f22d5f0fbd fix: top endpoints table overflow 2022-03-23 11:44:37 +05:30
227 changed files with 4276 additions and 2901 deletions

33
.editorconfig Normal file
View File

@@ -0,0 +1,33 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,py}]
charset = utf-8
# 4 space indentation
[*.py]
indent_style = space
indent_size = 4
# Tab indentation (no size specified)
[Makefile]
indent_style = tab
# Indentation override for all JS under lib directory
[lib/**.js]
indent_style = space
indent_size = 2
# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2

View File

@@ -2,11 +2,12 @@ name: build-pipeline
on:
pull_request:
branches:
- develop
- main
- v*
paths:
- 'pkg/**'
- 'frontend/**'
- "pkg/**"
- "frontend/**"
jobs:
get_filters:
@@ -17,17 +18,17 @@ jobs:
query-service: ${{ steps.filter.outputs.query-service }}
flattener: ${{ steps.filter.outputs.flattener }}
steps:
# For pull requests it's not necessary to checkout the code
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
frontend:
- 'frontend/**'
query-service:
- 'pkg/query-service/**'
flattener:
- 'pkg/processors/flattener/**'
# For pull requests it's not necessary to checkout the code
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
frontend:
- 'frontend/**'
query-service:
- 'pkg/query-service/**'
flattener:
- 'pkg/processors/flattener/**'
build-frontend:
runs-on: ubuntu-latest
@@ -39,12 +40,11 @@ jobs:
uses: actions/checkout@v2
- name: Install dependencies
run: cd frontend && yarn install
- name: Run Prettier
run: cd frontend && npm run prettify
continue-on-error: true
- name: Run ESLint
run: cd frontend && npm run lint
continue-on-error: true
- name: TSC
run: yarn tsc
working-directory: ./frontend
- name: Build frontend docker image
shell: bash
run: |

View File

@@ -115,11 +115,9 @@ down-arm:
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.arm.yaml down -v
clear-standalone-data:
@cd $(STANDALONE_DIRECTORY)
@docker run --rm -v "data:/pwd" busybox \
@docker run --rm -v "$(PWD)/$(STANDALONE_DIRECTORY)/data:/pwd" busybox \
sh -c "cd /pwd && rm -rf alertmanager/* clickhouse/* signoz/*"
clear-swarm-data:
@cd $(SWARM_DIRECTORY)
@docker run --rm -v "data:/pwd" busybox \
@docker run --rm -v "$(PWD)/$(SWARM_DIRECTORY)/data:/pwd" busybox \
sh -c "cd /pwd && rm -rf alertmanager/* clickhouse/* signoz/*"

View File

@@ -1,11 +1,8 @@
<?xml version="1.0"?>
<yandex>
<logger>
<level>trace</level>
<log>/var/log/clickhouse-server/clickhouse-server.log</log>
<errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
<size>1000M</size>
<count>10</count>
<level>information</level>
<console>1</console>
</logger>
<http_port>8123</http_port>
@@ -45,6 +42,34 @@
</client>
</openSSL>
<!-- Example config for tiered storage -->
<!-- <storage_configuration>
<disks>
<default>
<keep_free_space_bytes>10485760</keep_free_space_bytes>
</default>
<s3>
<type>s3</type>
<endpoint>https://BUCKET-NAME.s3.amazonaws.com/data/</endpoint>
<access_key_id>ACCESS-KEY-ID</access_key_id>
<secret_access_key>SECRET-ACCESS-KEY</secret_access_key>
</s3>
</disks>
<policies>
<tiered>
<volumes>
<default>
<disk>default</disk>
</default>
<s3>
<disk>s3</disk>
</s3>
</volumes>
</tiered>
</policies>
</storage_configuration> -->
<!-- Default root page on http[s] server. For example load UI from https://tabix.io/ when opening http://localhost:8123 -->
<!--
<http_server_default_response><![CDATA[<html ng-app="SMI2"><head><base href="http://ui.tabix.io/"></head><body><div ui-view="" class="content-ui"></div><script src="http://loader.tabix.io/master.js"></script></body></html>]]></http_server_default_response>

View File

@@ -3,12 +3,19 @@ version: "3.9"
services:
clickhouse:
image: yandex/clickhouse-server:21.12.3.32
# ports:
# - "9000:9000"
# - "8123:8123"
volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
- ./data/clickhouse/:/var/lib/clickhouse/
deploy:
restart_policy:
condition: on-failure
logging:
options:
max-size: 50m
max-file: "3"
healthcheck:
# "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'"
test: ["CMD", "wget", "--spider", "-q", "localhost:8123/ping"]
@@ -17,19 +24,20 @@ services:
retries: 3
alertmanager:
image: signoz/alertmanager:0.5.0
image: signoz/alertmanager:0.6.0
volumes:
- ./alertmanager.yml:/prometheus/alertmanager.yml
- ./data/alertmanager:/data
command:
- '--config.file=/prometheus/alertmanager.yml'
- '--storage.path=/data'
- --queryService.url=http://query-service:8080
- --storage.path=/data
depends_on:
- query-service
deploy:
restart_policy:
condition: on-failure
query-service:
image: signoz/query-service:0.7.2
image: signoz/query-service:0.7.4
command: ["-config=/root/config/prometheus.yml"]
ports:
- "8080:8080"
@@ -44,14 +52,19 @@ services:
- TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-swarm
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
interval: 30s
timeout: 5s
retries: 3
deploy:
restart_policy:
condition: on-failure
depends_on:
- clickhouse
- clickhouse
frontend:
image: signoz/frontend:0.7.2
image: signoz/frontend:0.7.4
depends_on:
- query-service
ports:

View File

@@ -1,11 +1,8 @@
<?xml version="1.0"?>
<yandex>
<logger>
<level>trace</level>
<log>/var/log/clickhouse-server/clickhouse-server.log</log>
<errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
<size>1000M</size>
<count>10</count>
<level>information</level>
<console>1</console>
</logger>
<http_port>8123</http_port>
@@ -46,30 +43,31 @@
</openSSL>
<!-- Example config for tiered storage -->
<!-- <storage_configuration> -->
<!-- <disks> -->
<!-- <default> -->
<!-- </default> -->
<!-- <s3> -->
<!-- <type>s3</type> -->
<!-- <endpoint>http://172.17.0.1:9100/test/random/</endpoint> -->
<!-- <access_key_id>ash</access_key_id> -->
<!-- <secret_access_key>password</secret_access_key> -->
<!-- </s3> -->
<!-- </disks> -->
<!-- <policies> -->
<!-- <tiered> -->
<!-- <volumes> -->
<!-- <default> -->
<!-- <disk>default</disk> -->
<!-- </default> -->
<!-- <s3> -->
<!-- <disk>s3</disk> -->
<!-- </s3> -->
<!-- </volumes> -->
<!-- </tiered> -->
<!-- </policies> -->
<!-- </storage_configuration> -->
<!-- <storage_configuration>
<disks>
<default>
<keep_free_space_bytes>10485760</keep_free_space_bytes>
</default>
<s3>
<type>s3</type>
<endpoint>https://BUCKET-NAME.s3.amazonaws.com/data/</endpoint>
<access_key_id>ACCESS-KEY-ID</access_key_id>
<secret_access_key>SECRET-ACCESS-KEY</secret_access_key>
</s3>
</disks>
<policies>
<tiered>
<volumes>
<default>
<disk>default</disk>
</default>
<s3>
<disk>s3</disk>
</s3>
</volumes>
</tiered>
</policies>
</storage_configuration> -->
<!-- Default root page on http[s] server. For example load UI from https://tabix.io/ when opening http://localhost:8123 -->

View File

@@ -3,10 +3,17 @@ version: "2.4"
services:
clickhouse:
image: altinity/clickhouse-server:21.12.3.32.altinitydev.arm
# ports:
# - "9000:9000"
# - "8123:8123"
volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
- ./data/clickhouse/:/var/lib/clickhouse/
restart: on-failure
logging:
options:
max-size: 50m
max-file: "3"
healthcheck:
# "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'"
test: ["CMD", "wget", "--spider", "-q", "localhost:8123/ping"]
@@ -15,16 +22,19 @@ services:
retries: 3
alertmanager:
image: signoz/alertmanager:0.5.0
image: signoz/alertmanager:0.6.0
volumes:
- ./alertmanager.yml:/prometheus/alertmanager.yml
- ./data/alertmanager:/data
depends_on:
query-service:
condition: service_healthy
restart: on-failure
command:
- '--config.file=/prometheus/alertmanager.yml'
- '--storage.path=/data'
- --queryService.url=http://query-service:8080
- --storage.path=/data
query-service:
image: signoz/query-service:0.7.2
image: signoz/query-service:0.7.4
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
volumes:
@@ -39,12 +49,17 @@ services:
- DEPLOYMENT_TYPE=docker-standalone-arm
restart: on-failure
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
interval: 30s
timeout: 5s
retries: 3
depends_on:
clickhouse:
condition: service_healthy
frontend:
image: signoz/frontend:0.7.2
image: signoz/frontend:0.7.4
container_name: frontend
depends_on:
- query-service
@@ -66,7 +81,7 @@ services:
# - "14268:14268" # Jaeger receiver
# - "55678:55678" # OpenCensus receiver
# - "55679:55679" # zpages extension
# - "55680:55680" # OTLP gRPC legacy port
# - "55680:55680" # OTLP gRPC legacy receiver
# - "55681:55681" # OTLP HTTP legacy receiver
mem_limit: 2000m
restart: on-failure
@@ -93,7 +108,7 @@ services:
max-file: "3"
command: ["all"]
environment:
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
load-hotrod:
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"

View File

@@ -3,10 +3,17 @@ version: "2.4"
services:
clickhouse:
image: yandex/clickhouse-server:21.12.3.32
# ports:
# - "9000:9000"
# - "8123:8123"
volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
- ./data/clickhouse/:/var/lib/clickhouse/
restart: on-failure
logging:
options:
max-size: 50m
max-file: "3"
healthcheck:
# "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'"
test: ["CMD", "wget", "--spider", "-q", "localhost:8123/ping"]
@@ -15,19 +22,21 @@ services:
retries: 3
alertmanager:
image: signoz/alertmanager:0.5.0
image: signoz/alertmanager:0.6.0
volumes:
- ./alertmanager.yml:/prometheus/alertmanager.yml
- ./data/alertmanager:/data
depends_on:
query-service:
condition: service_healthy
restart: on-failure
command:
- '--config.file=/prometheus/alertmanager.yml'
- '--storage.path=/data'
- --queryService.url=http://query-service:8080
- --storage.path=/data
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:0.7.2
image: signoz/query-service:0.7.4
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
volumes:
@@ -40,14 +49,18 @@ services:
- GODEBUG=netdns=go
- TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-standalone-amd
restart: on-failure
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
interval: 30s
timeout: 5s
retries: 3
depends_on:
clickhouse:
condition: service_healthy
frontend:
image: signoz/frontend:0.7.2
image: signoz/frontend:0.7.4
container_name: frontend
depends_on:
- query-service
@@ -69,7 +82,7 @@ services:
# - "14268:14268" # Jaeger receiver
# - "55678:55678" # OpenCensus receiver
# - "55679:55679" # zpages extension
# - "55680:55680" # OTLP gRPC legacy port
# - "55680:55680" # OTLP gRPC legacy receiver
# - "55681:55681" # OTLP HTTP legacy receiver
mem_limit: 2000m
restart: on-failure

View File

@@ -158,7 +158,9 @@ install_docker() {
echo
# yum install docker
# service docker start
$sudo_cmd amazon-linux-extras install docker
$sudo_cmd yum install -y amazon-linux-extras
$sudo_cmd amazon-linux-extras enable docker
$sudo_cmd yum install -y docker
else
yum_cmd="$sudo_cmd yum --assumeyes --quiet"
@@ -195,16 +197,16 @@ install_docker_compose() {
start_docker() {
echo -e "🐳 Starting Docker ...\n"
if [ $os = "Mac" ]; then
if [[ $os == "Mac" ]]; then
open --background -a Docker && while ! docker system info > /dev/null 2>&1; do sleep 1; done
else
if ! $sudo_cmd systemctl is-active docker.service > /dev/null; then
echo "Starting docker service"
$sudo_cmd systemctl start docker.service
fi
if [ -z $sudo_cmd ]; then
if [[ -z $sudo_cmd ]]; then
docker ps > /dev/null && true
if [ $? -ne 0 ]; then
if [[ $? -ne 0 ]]; then
request_sudo
fi
fi
@@ -230,7 +232,7 @@ wait_for_containers_start() {
}
bye() { # Prints a friendly good bye message and exits the script.
if [ "$?" -ne 0 ]; then
if [[ "$?" -ne 0 ]]; then
set +o errexit
echo "🔴 The containers didn't seem to start correctly. Please run the following command to check containers that may have errored out:"
@@ -266,17 +268,17 @@ bye() { # Prints a friendly good bye message and exits the script.
request_sudo() {
if hash sudo 2>/dev/null; then
sudo_cmd="sudo"
echo -e "\n\n🙇 We will need sudo access to complete the installation."
if ! $sudo_cmd -v && (( $EUID != 0 )); then
echo -e "Please enter your sudo password now:"
if ! $sudo_cmd -v; then
if (( $EUID != 0 )); then
sudo_cmd="sudo"
echo -e "Please enter your sudo password, if prompt."
$sudo_cmd -l | grep -e "NOPASSWD: ALL" > /dev/null
if [[ $? -ne 0 ]] && ! $sudo_cmd -v; then
echo "Need sudo privileges to proceed with the installation."
exit 1;
fi
echo -e "Thanks! 🙏\n"
echo -e "Got it! Thanks!! 🙏\n"
echo -e "Okay! We will bring up the SigNoz cluster from here 🚀\n"
fi
fi
@@ -291,9 +293,6 @@ sudo_cmd=""
# Check sudo permissions
if (( $EUID != 0 )); then
echo "🟡 Running installer with non-sudo permissions."
if ! is_command_present docker; then
$sudo_cmd docker ps
fi
echo " In case of any failure or prompt, please consider running the script with sudo privileges."
echo ""
else
@@ -309,7 +308,7 @@ check_os
# Obtain unique installation id
sysinfo="$(uname -a)"
if [ $? -ne 0 ]; then
if [[ $? -ne 0 ]]; then
uuid="$(uuidgen)"
uuid="${uuid:-$(cat /proc/sys/kernel/random/uuid)}"
sysinfo="${uuid:-$(cat /proc/sys/kernel/random/uuid)}"
@@ -324,7 +323,7 @@ elif hash openssl 2>/dev/null; then
digest_cmd="openssl dgst -sha256"
fi
if [ -z $digest_cmd ]; then
if [[ -z $digest_cmd ]]; then
SIGNOZ_INSTALLATION_ID="$sysinfo"
else
SIGNOZ_INSTALLATION_ID=$(echo "$sysinfo" | $digest_cmd | grep -E -o '[a-zA-Z0-9]{64}')
@@ -407,7 +406,7 @@ send_event() {
;;
esac
if [ "$error" != "" ]; then
if [[ "$error" != "" ]]; then
error='"error": "'"$error"'", '
fi

View File

@@ -1,2 +1,2 @@
node_modules
build
build

View File

@@ -84,6 +84,23 @@ module.exports = {
tsx: 'never',
},
],
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
'jsx-a11y/label-has-associated-control': [
'error',
{
required: {
some: ['nesting', 'id'],
},
},
],
'jsx-a11y/label-has-for': [
'error',
{
required: {
some: ['nesting', 'id'],
},
},
],
// eslint rules need to remove
'no-shadow': 'off',

4
frontend/.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
cd frontend && yarn lint-staged

6
frontend/babel.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
};

View File

@@ -9,12 +9,19 @@ const config: Config.InitialOptions = {
moduleNameMapper: {
'\\.(css|less)$': '<rootDir>/__mocks__/cssMock.ts',
},
notify: true,
notifyMode: 'always',
testMatch: ['<rootDir>/src/**/?(*.)(test).(ts|js)?(x)'],
transform: {
'\\.(js|jsx|ts|tsx)?$': 'babel-jest',
globals: {
extensionsToTreatAsEsm: ['.ts'],
'ts-jest': {
useESM: true,
},
},
testMatch: ['<rootDir>/src/**/?(*.)(test).(ts|js)?(x)'],
preset: 'ts-jest/presets/js-with-ts-esm',
transform: {
'^.+\\.(ts|tsx)?$': 'ts-jest',
'^.+\\.(js|jsx)$': 'babel-jest',
},
transformIgnorePatterns: ['node_modules/(?!(lodash-es)/)'],
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],
moduleDirectories: ['node_modules', 'src'],

View File

@@ -13,7 +13,9 @@
"cypress:run": "cypress run",
"jest": "jest",
"jest:coverage": "jest --coverage",
"jest:watch": "jest --watch"
"jest:watch": "jest --watch",
"postinstall": "yarn husky:configure",
"husky:configure": "cd .. && husky install frontend/.husky"
},
"engines": {
"node": ">=12.13.0"
@@ -21,6 +23,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons": "^4.6.2",
"@grafana/data": "^8.4.3",
"@monaco-editor/react": "^4.3.1",
@@ -54,7 +57,7 @@
"i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3",
"i18next-http-backend": "^1.3.2",
"jest": "26.6.0",
"jest": "^27.5.1",
"less": "^4.1.2",
"less-loader": "^10.2.0",
"lodash-es": "^4.17.21",
@@ -102,7 +105,9 @@
"@babel/preset-env": "^7.12.17",
"@babel/preset-react": "^7.12.13",
"@babel/preset-typescript": "^7.12.17",
"@jest/globals": "^27.5.1",
"@testing-library/cypress": "^8.0.0",
"@testing-library/react-hooks": "^7.0.2",
"@types/color": "^3.0.3",
"@types/compression-webpack-plugin": "^9.0.0",
"@types/copy-webpack-plugin": "^8.0.1",
@@ -145,15 +150,21 @@
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-sonarjs": "^0.12.0",
"husky": "4.3.8",
"husky": "^7.0.4",
"less-plugin-npm-import": "^2.1.0",
"lint-staged": "10.5.3",
"lint-staged": "^12.3.7",
"portfinder-sync": "^0.0.2",
"prettier": "2.2.1",
"react-hot-loader": "^4.13.0",
"ts-jest": "^27.1.4",
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "^3.4.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.5.0"
},
"lint-staged": {
"*.(js|jsx|ts|tsx)": [
"eslint --fix"
]
}
}

View File

@@ -1,3 +1,27 @@
{
"monitor_signup": "Monitor your applications. Find what is causing issues."
"monitor_signup": "Monitor your applications. Find what is causing issues.",
"version": "Version",
"latest_version": "Latest version",
"current_version": "Current version",
"release_notes": "Release Notes",
"read_how_to_upgrade": "Read instructions on how to upgrade",
"latest_version_signoz": "You are running the latest version of SigNoz.",
"stale_version": "You are on an older version and may be loosing on the latest features we have shipped. We recommend to upgrade to the latest version",
"oops_something_went_wrong_version": "Oops.. facing issues with fetching updated version information",
"n_a": "N/A",
"routes": {
"general": "General",
"alert_channels": "Alert Channels"
},
"settings": {
"total_retention_period": "Total Retention Period",
"move_to_s3": "Move to S3\n(should be lower than total retention period)",
"retention_success_message": "Congrats. The retention periods for {{name}} has been updated successfully.",
"retention_error_message": "There was an issue in changing the retention period for {{name}}. Please try again or reach out to support@signoz.io",
"retention_failed_message": "There was an issue in changing the retention period. Please try again or reach out to support@signoz.io",
"retention_comparison_error": "Total retention period for {{name}} cant be lower or equal to the period after which data is moved to s3.",
"retention_null_value_error": "Retention Period for {{name}} is not set yet. Please set by choosing below",
"retention_confirmation": "Are you sure you want to change the retention period?",
"retention_confirmation_description": "This will change the amount of storage needed for saving metrics & traces."
}
}

View File

@@ -85,3 +85,7 @@ export const EditAlertChannelsAlerts = Loadable(
export const AllAlertChannels = Loadable(
() => import(/* webpackChunkName: "All Channels" */ 'pages/AllAlertChannels'),
);
export const StatusPage = Loadable(
() => import(/* webpackChunkName: "All Status" */ 'pages/Status'),
);

View File

@@ -17,6 +17,7 @@ import {
ServicesTablePage,
SettingsPage,
SignupPage,
StatusPage,
TraceDetail,
TraceFilter,
UsageExplorerPage,
@@ -113,6 +114,11 @@ const routes: AppRoutes[] = [
exact: true,
component: AllAlertChannels,
},
{
path: ROUTES.VERSION,
exact: true,
component: StatusPage,
},
];
interface AppRoutes {

View File

@@ -3,13 +3,14 @@ import { ErrorResponse } from 'types/api';
import { ErrorStatusCode } from 'types/common';
export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
if (error.response) {
const { response, request } = error;
if (response) {
// client received an error response (5xx, 4xx)
// making the error status code as standard Error Status Code
const statusCode = error.response.status as ErrorStatusCode;
const statusCode = response.status as ErrorStatusCode;
if (statusCode >= 400 && statusCode < 500) {
const { data } = error.response;
const { data } = response;
if (statusCode === 404) {
return {
@@ -35,7 +36,7 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
message: null,
};
}
if (error.request) {
if (request) {
// client never received a response, or request never left
console.error('client never received a response, or request never left');
@@ -51,7 +52,7 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
return {
statusCode: 500,
payload: null,
error: error.toString(),
error: String(error),
message: null,
};
}

View File

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

View File

@@ -0,0 +1,51 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createWebhook';
const create = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
let httpConfig = {};
if (props.username !== '' && props.password !== '') {
httpConfig = {
basic_auth: {
username: props.username,
password: props.password,
},
};
} else if (props.username === '' && props.password !== '') {
httpConfig = {
authorization: {
type: 'bearer',
credentials: props.password,
},
};
}
const response = await axios.post('/channels', {
name: props.name,
webhook_configs: [
{
send_resolved: true,
url: props.api_url,
http_config: httpConfig,
},
],
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default create;

View File

@@ -0,0 +1,50 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/editWebhook';
const editWebhook = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
let httpConfig = {};
if (props.username !== '' && props.password !== '') {
httpConfig = {
basic_auth: {
username: props.username,
password: props.password,
},
};
} else if (props.username === '' && props.password !== '') {
httpConfig = {
authorization: {
type: 'bearer',
credentials: props.password,
},
};
}
const response = await axios.put(`/channels/${props.id}`, {
name: props.name,
webhook_configs: [
{
send_resolved: true,
url: props.api_url,
http_config: httpConfig,
},
],
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default editWebhook;

View File

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

View File

@@ -9,7 +9,11 @@ const setRetention = async (
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post<PayloadProps>(
`/settings/ttl?duration=${props.duration}&type=${props.type}`,
`/settings/ttl?duration=${props.totalDuration}&type=${props.type}${
props.coldStorage
? `&coldStorage=${props.coldStorage};toColdDuration=${props.toColdDuration}`
: ''
}`,
);
return {

View File

@@ -0,0 +1,28 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/trace/getTagValue';
const getTagValue = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post<PayloadProps>(`/getTagValues`, {
start: props.start.toString(),
end: props.end.toString(),
tagKey: props.tagKey,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getTagValue;

View File

@@ -0,0 +1,25 @@
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import axios, { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/user/getLatestVersion';
const getLatestVersion = async (): Promise<
SuccessResponse<PayloadProps> | ErrorResponse
> => {
try {
const response = await axios.get(
`https://api.github.com/repos/signoz/signoz/releases/latest`,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getLatestVersion;

View File

@@ -1,6 +1,8 @@
import React from 'react';
function Value(props: ValueProps): JSX.Element {
const { fillColor } = props;
return (
<svg
width="78"
@@ -11,7 +13,7 @@ function Value(props: ValueProps): JSX.Element {
>
<path
d="M15.0215 17.875C14.2285 18.8184 13.2783 19.5771 12.1709 20.1514C11.0771 20.7256 9.87402 21.0127 8.56152 21.0127C6.83887 21.0127 5.33496 20.5889 4.0498 19.7412C2.77832 18.8936 1.79395 17.7041 1.09668 16.1729C0.399414 14.6279 0.0507812 12.9258 0.0507812 11.0664C0.0507812 9.07031 0.426758 7.27246 1.17871 5.67285C1.94434 4.07324 3.02441 2.84961 4.41895 2.00195C5.81348 1.1543 7.44043 0.730469 9.2998 0.730469C12.2529 0.730469 14.5771 1.83789 16.2725 4.05273C17.9814 6.25391 18.8359 9.26172 18.8359 13.0762V14.1836C18.8359 19.9941 17.6875 24.2393 15.3906 26.9189C13.0938 29.585 9.62793 30.9521 4.99316 31.0205H4.25488V27.8213H5.05469C8.18555 27.7666 10.5918 26.9531 12.2734 25.3809C13.9551 23.7949 14.8711 21.293 15.0215 17.875ZM9.17676 17.875C10.4482 17.875 11.6172 17.4854 12.6836 16.7061C13.7637 15.9268 14.5498 14.9629 15.042 13.8145V12.2969C15.042 9.80859 14.502 7.78516 13.4219 6.22656C12.3418 4.66797 10.9746 3.88867 9.32031 3.88867C7.65234 3.88867 6.3125 4.53125 5.30078 5.81641C4.28906 7.08789 3.7832 8.76953 3.7832 10.8613C3.7832 12.8984 4.26855 14.5801 5.23926 15.9062C6.22363 17.2188 7.53613 17.875 9.17676 17.875ZM24.5371 29.0107C24.5371 28.3545 24.7285 27.8076 25.1113 27.3701C25.5078 26.9326 26.0957 26.7139 26.875 26.7139C27.6543 26.7139 28.2422 26.9326 28.6387 27.3701C29.0488 27.8076 29.2539 28.3545 29.2539 29.0107C29.2539 29.6396 29.0488 30.166 28.6387 30.5898C28.2422 31.0137 27.6543 31.2256 26.875 31.2256C26.0957 31.2256 25.5078 31.0137 25.1113 30.5898C24.7285 30.166 24.5371 29.6396 24.5371 29.0107ZM51.1562 20.9717H55.2988V24.0684H51.1562V31H47.3418V24.0684H33.7451V21.833L47.1162 1.14062H51.1562V20.9717ZM38.0518 20.9717H47.3418V6.3291L46.8906 7.14941L38.0518 20.9717ZM73.6123 1.12012V4.33984H72.915C69.9619 4.39453 67.6104 5.26953 65.8604 6.96484C64.1104 8.66016 63.0986 11.0459 62.8252 14.1221C64.3975 12.3174 66.5439 11.415 69.2646 11.415C71.8623 11.415 73.9336 12.3311 75.4785 14.1631C77.0371 15.9951 77.8164 18.3604 77.8164 21.2588C77.8164 24.335 76.9756 26.7959 75.2939 28.6416C73.626 30.4873 71.3838 31.4102 68.5674 31.4102C65.71 31.4102 63.3926 30.3164 61.6152 28.1289C59.8379 25.9277 58.9492 23.0977 58.9492 19.6387V18.1826C58.9492 12.6865 60.1182 8.48926 62.4561 5.59082C64.8076 2.67871 68.3008 1.18848 72.9355 1.12012H73.6123ZM68.6289 14.5732C67.3301 14.5732 66.1338 14.9629 65.04 15.7422C63.9463 16.5215 63.1875 17.499 62.7637 18.6748V20.0693C62.7637 22.5303 63.3174 24.5127 64.4248 26.0166C65.5322 27.5205 66.9131 28.2725 68.5674 28.2725C70.2764 28.2725 71.6162 27.6436 72.5869 26.3857C73.5713 25.1279 74.0635 23.4805 74.0635 21.4434C74.0635 19.3926 73.5645 17.7383 72.5664 16.4805C71.582 15.209 70.2695 14.5732 68.6289 14.5732Z"
fill={props.fillColor}
fill={fillColor}
/>
</svg>
);

View File

@@ -1,5 +1,7 @@
import generatePicker from 'antd/es/date-picker/generatePicker';
import { Dayjs } from 'dayjs';
// included in antd
// eslint-disable-next-line import/no-extraneous-dependencies
import dayjsGenerateConfig from 'rc-picker/lib/generate/dayjs';
const DatePicker = generatePicker<Dayjs>(dayjsGenerateConfig);

View File

@@ -9,6 +9,12 @@ function Editor({ value }: EditorProps): JSX.Element {
value={value.current}
options={{ fontSize: 16, automaticLayout: true }}
height="40vh"
onChange={(newValue): void => {
if (value.current && newValue) {
// eslint-disable-next-line no-param-reassign
value.current = newValue;
}
}}
/>
);
}

View File

@@ -0,0 +1,17 @@
import { grey } from '@ant-design/colors';
import { Chart } from 'chart.js';
export const emptyGraph = {
id: 'emptyChart',
afterDraw(chart: Chart): void {
const { height, width, ctx } = chart;
chart.clear();
ctx.save();
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = '1.5rem sans-serif';
ctx.fillStyle = `${grey.primary}`;
ctx.fillText('No data to display', width / 2, height / 2);
ctx.restore();
},
};

View File

@@ -1,5 +1,6 @@
import { Chart, ChartType, Plugin } from 'chart.js';
import { colors } from 'lib/getRandomColor';
import { get } from 'lodash-es';
const getOrCreateLegendList = (
chart: Chart,
@@ -40,9 +41,20 @@ export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => {
}
// Reuse the built-in legendItems generator
const items = chart?.options?.plugins?.legend?.labels?.generateLabels(chart);
const items = get(chart, [
'options',
'plugins',
'legend',
'labels',
'generateLabels',
])
? get(chart, ['options', 'plugins', 'legend', 'labels', 'generateLabels'])(
chart,
)
: null;
items?.forEach((item, index) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
items?.forEach((item: Record<any, any>, index: number) => {
const li = document.createElement('li');
li.style.alignItems = 'center';
li.style.cursor = 'pointer';
@@ -66,8 +78,8 @@ export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => {
// Color box
const boxSpan = document.createElement('span');
boxSpan.style.background = item.strokeStyle || colors[0];
boxSpan.style.borderColor = item?.strokeStyle;
boxSpan.style.background = `${item.strokeStyle}` || `${colors[0]}`;
boxSpan.style.borderColor = `${item?.strokeStyle}`;
boxSpan.style.borderWidth = `${item.lineWidth}px`;
boxSpan.style.display = 'inline-block';
boxSpan.style.minHeight = '20px';

View File

@@ -0,0 +1,19 @@
/* eslint-disable no-restricted-syntax */
import { ChartData } from 'chart.js';
export const hasData = (data: ChartData): boolean => {
const { datasets = [] } = data;
let hasData = false;
try {
for (const dataset of datasets) {
if (dataset.data.length > 0 && dataset.data.some((item) => item !== 0)) {
hasData = true;
break;
}
}
} catch (error) {
console.error(error);
}
return hasData;
};

View File

@@ -27,7 +27,9 @@ import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import { hasData } from './hasData';
import { legend } from './Plugin';
import { emptyGraph } from './Plugin/EmptyGraph';
import { LegendsContainer } from './styles';
import { useXAxisTimeUnit } from './xAxisConfig';
import { getYAxisFormattedValue } from './yAxisConfig';
@@ -79,6 +81,7 @@ function Graph({
return 'rgba(231,233,237,0.8)';
}, [currentTheme]);
// eslint-disable-next-line sonarjs/cognitive-complexity
const buildChart = useCallback(() => {
if (lineChartRef.current !== undefined) {
lineChartRef.current.destroy();
@@ -103,6 +106,21 @@ function Graph({
legend: {
display: false,
},
tooltip: {
callbacks: {
label(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += getYAxisFormattedValue(context.parsed.y, yAxisUnit);
}
return label;
},
},
},
},
layout: {
padding: 0,
@@ -112,6 +130,7 @@ function Graph({
grid: {
display: true,
color: getGridColor(),
drawTicks: true,
},
adapters: {
date: chartjsAdapter,
@@ -140,8 +159,11 @@ function Graph({
},
ticks: {
// Include a dollar sign in the ticks
callback(value, index, ticks) {
return getYAxisFormattedValue(value, yAxisUnit);
callback(value) {
return getYAxisFormattedValue(
parseInt(value.toString(), 10),
yAxisUnit,
);
},
},
},
@@ -161,12 +183,18 @@ function Graph({
}
},
};
const chartHasData = hasData(data);
const chartPlugins = [];
if (chartHasData) {
chartPlugins.push(legend(name, data.datasets.length > 3));
} else {
chartPlugins.push(emptyGraph);
}
lineChartRef.current = new Chart(chartRef.current, {
type,
data,
options,
plugins: [legend(name, data.datasets.length > 3)],
plugins: chartPlugins,
});
}
}, [
@@ -201,18 +229,25 @@ interface GraphProps {
data: Chart['data'];
title?: string;
isStacked?: boolean;
label?: string[];
onClickHandler?: graphOnClickHandler;
onClickHandler?: GraphOnClickHandler;
name: string;
yAxisUnit?: string;
forceReRender?: boolean | null | number;
}
export type graphOnClickHandler = (
export type GraphOnClickHandler = (
event: ChartEvent,
elements: ActiveElement[],
chart: Chart,
data: ChartData,
) => void;
Graph.defaultProps = {
animate: undefined,
title: undefined,
isStacked: undefined,
onClickHandler: undefined,
yAxisUnit: undefined,
forceReRender: undefined,
};
export default Graph;

View File

@@ -68,6 +68,37 @@ const TIME_UNITS_CONFIG: IAxisTimeUintConfig[] = [
},
];
/**
* Finds the relevant time unit based on the input time stamps (in ms)
*/
export const convertTimeRange = (
start: number,
end: number,
): IAxisTimeConfig => {
const MIN_INTERVALS = 6;
const range = end - start;
let relevantTimeUnit = TIME_UNITS_CONFIG[1];
let stepSize = 1;
try {
for (let idx = TIME_UNITS_CONFIG.length - 1; idx >= 0; idx -= 1) {
const timeUnit = TIME_UNITS_CONFIG[idx];
const units = range * timeUnit.multiplier;
const steps = units / MIN_INTERVALS;
if (steps >= 1) {
relevantTimeUnit = timeUnit;
stepSize = steps;
break;
}
}
} catch (error) {
console.error(error);
}
return {
unitName: relevantTimeUnit.unitName,
stepSize: Math.floor(stepSize) || 1,
};
};
/**
* Accepts Chart.js data's data-structure and returns the relevant time unit for the axis based on the range of the data.
*/
@@ -77,10 +108,18 @@ export const useXAxisTimeUnit = (data: Chart['data']): IAxisTimeConfig => {
try {
let minTime = Number.POSITIVE_INFINITY;
let maxTime = Number.NEGATIVE_INFINITY;
data?.labels?.forEach((timeStamp: string | number): void => {
if (typeof timeStamp === 'string') timeStamp = Date.parse(timeStamp);
minTime = Math.min(timeStamp, minTime);
maxTime = Math.max(timeStamp, maxTime);
data?.labels?.forEach((timeStamp: unknown): void => {
const getTimeStamp = (time: string | number): Date | number | string => {
if (typeof timeStamp === 'string') {
return Date.parse(timeStamp);
}
return time;
};
const time = getTimeStamp(timeStamp as string | number);
minTime = Math.min(parseInt(time.toString(), 10), minTime);
maxTime = Math.max(parseInt(time.toString(), 10), maxTime);
});
localTime = {
@@ -113,34 +152,3 @@ export const useXAxisTimeUnit = (data: Chart['data']): IAxisTimeConfig => {
return convertTimeRange(minTime, maxTime);
};
/**
* Finds the relevant time unit based on the input time stamps (in ms)
*/
export const convertTimeRange = (
start: number,
end: number,
): IAxisTimeConfig => {
const MIN_INTERVALS = 6;
const range = end - start;
let relevantTimeUnit = TIME_UNITS_CONFIG[1];
let stepSize = 1;
try {
for (let idx = TIME_UNITS_CONFIG.length - 1; idx >= 0; idx--) {
const timeUnit = TIME_UNITS_CONFIG[idx];
const units = range * timeUnit.multiplier;
const steps = units / MIN_INTERVALS;
if (steps >= 1) {
relevantTimeUnit = timeUnit;
stepSize = steps;
break;
}
}
} catch (error) {
console.error(error);
}
return {
unitName: relevantTimeUnit.unitName,
stepSize: Math.floor(stepSize) || 1,
};
};

View File

@@ -3,7 +3,6 @@ import { formattedValueToString, getValueFormat } from '@grafana/data';
export const getYAxisFormattedValue = (
value: number,
format: string,
decimal?: number,
): string => {
try {
return formattedValueToString(

View File

@@ -1,4 +1,4 @@
import { Form, Input, InputProps } from 'antd';
import { Form, Input, InputProps, InputRef } from 'antd';
import React from 'react';
function InputComponent({
@@ -22,11 +22,12 @@ function InputComponent({
type={type}
onChange={onChangeHandler}
value={value}
ref={ref}
ref={ref as React.Ref<InputRef>}
size={size}
addonBefore={addonBefore}
onBlur={onBlurHandler}
onPressEnter={onPressEnterHandler}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
</Form.Item>
@@ -38,7 +39,7 @@ interface InputComponentProps extends InputProps {
type?: InputProps['type'];
onChangeHandler?: React.ChangeEventHandler<HTMLInputElement>;
placeholder?: InputProps['placeholder'];
ref?: React.LegacyRef<Input>;
ref?: React.LegacyRef<InputRef>;
size?: InputProps['size'];
onBlurHandler?: React.FocusEventHandler<HTMLInputElement>;
onPressEnterHandler?: React.KeyboardEventHandler<HTMLInputElement>;
@@ -47,4 +48,17 @@ interface InputComponentProps extends InputProps {
addonBefore?: React.ReactNode;
}
InputComponent.defaultProps = {
type: undefined,
onChangeHandler: undefined,
placeholder: undefined,
ref: undefined,
size: undefined,
onBlurHandler: undefined,
onPressEnterHandler: undefined,
label: undefined,
labelOnTop: undefined,
addonBefore: undefined,
};
export default InputComponent;

View File

@@ -28,4 +28,9 @@ interface ModalProps {
children: ReactElement;
}
CustomModal.defaultProps = {
closable: undefined,
footer: undefined,
};
export default CustomModal;

View File

@@ -1,3 +1,8 @@
/**
* @jest-environment jsdom
*/
import { expect } from '@jest/globals';
import { render } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';

View File

@@ -3,7 +3,7 @@
exports[`Not Found page test should render Not Found page without errors 1`] = `
<DocumentFragment>
<div
class="sc-gtsrHT VomVY"
class="sc-gsDKAQ cLXpIa"
>
<svg
fill="none"
@@ -272,21 +272,21 @@ exports[`Not Found page test should render Not Found page without errors 1`] = `
</defs>
</svg>
<div
class="sc-hKFxyN dunFuJ"
class="sc-hKwDye foaleg"
>
<p
class="sc-dlnjwi cydxLA"
class="sc-dkPtRN fcyVIq"
>
Ah, seems like we reached a dead end!
</p>
<p
class="sc-dlnjwi cydxLA"
class="sc-dkPtRN fcyVIq"
>
Page Not Found
</p>
</div>
<a
class="sc-bdnxRM bYqcho"
class="sc-bdvvtL dbTZkj"
href="/application"
tabindex="0"
>

View File

@@ -10,8 +10,10 @@ function RouteTab({
onChangeHandler,
...rest
}: RouteTabProps & TabsProps): JSX.Element {
const onChange = (activeRoute: string) => {
onChangeHandler && onChangeHandler();
const onChange = (activeRoute: string): void => {
if (onChangeHandler) {
onChangeHandler();
}
const selectedRoute = routes.find((e) => e.name === activeRoute);
@@ -25,6 +27,7 @@ function RouteTab({
onChange={onChange}
destroyInactiveTabPane
activeKey={activeKey}
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
>
{routes.map(
@@ -48,4 +51,8 @@ interface RouteTabProps {
onChangeHandler?: VoidFunction;
}
RouteTab.defaultProps = {
onChangeHandler: undefined,
};
export default RouteTab;

View File

@@ -17,5 +17,10 @@ interface SpinnerProps {
tip?: SpinProps['tip'];
height?: React.CSSProperties['height'];
}
Spinner.defaultProps = {
size: undefined,
tip: undefined,
height: undefined,
};
export default Spinner;

View File

@@ -6,8 +6,8 @@ import styled, { FlattenSimpleInterpolation } from 'styled-components';
import { IStyledClass } from './types';
const styledClass = (props: IStyledClass): FlattenSimpleInterpolation =>
props.styledclass;
const styledClass = (props: IStyledClass): FlattenSimpleInterpolation | null =>
props.styledclass || null;
type TStyledCol = AntD.ColProps & IStyledClass;
const StyledCol = styled(AntD.Col)<TStyledCol>`

View File

@@ -1,6 +1,7 @@
import { css, FlattenSimpleInterpolation } from 'styled-components';
const cssProprty = (key: string, value): FlattenSimpleInterpolation =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cssProperty = (key: any, value: any): FlattenSimpleInterpolation =>
key &&
value &&
css`
@@ -15,8 +16,8 @@ export const Flex = ({
flexDirection,
flex,
}: IFlexProps): FlattenSimpleInterpolation => css`
${cssProprty('flex-direction', flexDirection)}
${cssProprty('flex', flex)}
${cssProperty('flex-direction', flexDirection)}
${cssProperty('flex', flex)}
`;
interface IDisplayProps {
@@ -25,7 +26,7 @@ interface IDisplayProps {
export const Display = ({
display,
}: IDisplayProps): FlattenSimpleInterpolation => css`
${cssProprty('display', display)}
${cssProperty('display', display)}
`;
interface ISpacingProps {
@@ -36,6 +37,6 @@ export const Spacing = ({
margin,
padding,
}: ISpacingProps): FlattenSimpleInterpolation => css`
${cssProprty('margin', margin)}
${cssProprty('padding', padding)}
${cssProperty('margin', margin)}
${cssProperty('padding', padding)}
`;

View File

@@ -1,11 +1,12 @@
/* eslint-disable react/no-unstable-nested-components */
import { QuestionCircleFilled } from '@ant-design/icons';
import { Tooltip } from 'antd';
import React from 'react';
function TextToolTip({ text, url }: TextToolTipProps) {
function TextToolTip({ text, url }: TextToolTipProps): JSX.Element {
return (
<Tooltip
overlay={() => {
overlay={(): JSX.Element => {
return (
<div>
{`${text} `}

View File

@@ -1,5 +1,5 @@
import { Button, Dropdown, Menu, Typography } from 'antd';
import timeItems, {
import TimeItems, {
timePreferance,
timePreferenceType,
} from 'container/NewWidget/RightContainer/timeItems';
@@ -13,7 +13,7 @@ function TimePreference({
}: TimePreferenceDropDownProps): JSX.Element {
const timeMenuItemOnChangeHandler = useCallback(
(event: TimeMenuItemOnChangeHandlerEvent) => {
const selectedTime = timeItems.find((e) => e.enum === event.key);
const selectedTime = TimeItems.find((e) => e.enum === event.key);
if (selectedTime !== undefined) {
setSelectedTime(selectedTime);
}
@@ -26,7 +26,7 @@ function TimePreference({
<Dropdown
overlay={
<Menu>
{timeItems.map((item) => (
{TimeItems.map((item) => (
<Menu.Item onClick={timeMenuItemOnChangeHandler} key={item.enum}>
<Typography>{item.name}</Typography>
</Menu.Item>

View File

@@ -6,4 +6,4 @@ export const AUTH0_REDIRECT_PATH = '/redirect';
export const DEFAULT_AUTH0_APP_REDIRECTION_PATH = ROUTES.APPLICATION;
export const IS_SIDEBAR_COLLAPSED = 'isSideBarCollapsed'
export const IS_SIDEBAR_COLLAPSED = 'isSideBarCollapsed';

View File

@@ -1,3 +1,3 @@
export enum LOCAL_STORAGE {
export enum LOCALSTORAGE {
METRICS_TIME_IN_DURATION = 'metricsTimeDurations',
}

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line @typescript-eslint/naming-convention
export enum METRICS_PAGE_QUERY_PARAM {
interval = 'interval',
startTime = 'startTime',

View File

@@ -17,6 +17,7 @@ const ROUTES = {
ALL_CHANNELS: '/settings/channels',
CHANNELS_NEW: '/setting/channels/new',
CHANNELS_EDIT: '/setting/channels/edit/:id',
VERSION: '/status',
};
export default ROUTES;

View File

@@ -4,7 +4,7 @@ import { ColumnsType } from 'antd/lib/table';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import React, { useCallback, useState } from 'react';
import { generatePath } from 'react-router';
import { generatePath } from 'react-router-dom';
import { Channels, PayloadProps } from 'types/api/channels/getAll';
import Delete from './Delete';

View File

@@ -30,7 +30,8 @@ function Delete({ notifications, setChannels, id }: DeleteProps): JSX.Element {
} catch (error) {
notifications.error({
message: 'Error',
description: error.toString() || 'Something went wrong',
description:
error instanceof Error ? error.toString() : 'Something went wrong',
});
setLoading(false);
}

View File

@@ -1,21 +1,49 @@
import { notification } from 'antd';
import getLatestVersion from 'api/user/getLatestVersion';
import getVersion from 'api/user/getVersion';
import ROUTES from 'constants/routes';
import TopNav from 'container/Header';
import SideNav from 'container/SideNav';
import useFetch from 'hooks/useFetch';
import history from 'lib/history';
import React, { ReactNode, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import React, { ReactNode, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
UPDATE_CURRENT_ERROR,
UPDATE_CURRENT_VERSION,
UPDATE_LATEST_VERSION,
UPDATE_LATEST_VERSION_ERROR,
} from 'types/actions/app';
import AppReducer from 'types/reducer/app';
import { Content, Layout } from './styles';
const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
function AppLayout(props: AppLayoutProps): JSX.Element {
const { isLoggedIn } = useSelector<AppState, AppReducer>((state) => state.app);
const { pathname } = useLocation();
const { t } = useTranslation();
const [isSignUpPage, setIsSignUpPage] = useState(
ROUTES.SIGN_UP === location.pathname,
const [isSignUpPage, setIsSignUpPage] = useState(ROUTES.SIGN_UP === pathname);
const { payload: versionPayload, loading, error: getVersionError } = useFetch(
getVersion,
);
const {
payload: latestVersionPayload,
loading: latestLoading,
error: latestError,
} = useFetch(getLatestVersion);
const { children } = props;
const dispatch = useDispatch<Dispatch<AppActions>>();
useEffect(() => {
if (!isLoggedIn) {
setIsSignUpPage(true);
@@ -25,6 +53,72 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
}
}, [isLoggedIn, isSignUpPage]);
const latestCurrentCounter = useRef(0);
const latestVersionCounter = useRef(0);
useEffect(() => {
if (isLoggedIn && pathname === ROUTES.SIGN_UP) {
history.push(ROUTES.APPLICATION);
}
if (!latestLoading && latestError && latestCurrentCounter.current === 0) {
latestCurrentCounter.current = 1;
dispatch({
type: UPDATE_LATEST_VERSION_ERROR,
payload: {
isError: true,
},
});
notification.error({
message: t('oops_something_went_wrong_version'),
});
}
if (!loading && getVersionError && latestVersionCounter.current === 0) {
latestVersionCounter.current = 1;
dispatch({
type: UPDATE_CURRENT_ERROR,
payload: {
isError: true,
},
});
notification.error({
message: t('oops_something_went_wrong_version'),
});
}
if (!latestLoading && versionPayload) {
dispatch({
type: UPDATE_CURRENT_VERSION,
payload: {
currentVersion: versionPayload.version,
},
});
}
if (!loading && latestVersionPayload) {
dispatch({
type: UPDATE_LATEST_VERSION,
payload: {
latestVersion: latestVersionPayload.name,
},
});
}
}, [
dispatch,
loading,
latestLoading,
versionPayload,
latestVersionPayload,
isLoggedIn,
pathname,
getVersionError,
latestError,
t,
]);
return (
<Layout>
{!isSignUpPage && <SideNav />}
@@ -36,7 +130,7 @@ const AppLayout: React.FC<AppLayoutProps> = ({ children }) => {
</Layout>
</Layout>
);
};
}
interface AppLayoutProps {
children: ReactNode;

View File

@@ -1,10 +1,22 @@
export interface SlackChannel {
send_resolved: boolean;
api_url: string;
channel: string;
title: string;
text: string;
export interface Channel {
send_resolved?: boolean;
name: string;
}
export type ChannelType = 'slack' | 'email';
export interface SlackChannel extends Channel {
api_url?: string;
channel?: string;
title?: string;
text?: string;
}
export interface WebhookChannel extends Channel {
api_url?: string;
// basic auth
username?: string;
password?: string;
}
export type ChannelType = 'slack' | 'email' | 'webhook';
export const SlackType: ChannelType = 'slack';
export const WebhookType: ChannelType = 'webhook';

View File

@@ -1,20 +1,31 @@
import { Form, notification } from 'antd';
import createSlackApi from 'api/channels/createSlack';
import createWebhookApi from 'api/channels/createWebhook';
import ROUTES from 'constants/routes';
import FormAlertChannels from 'container/FormAlertChannels';
import history from 'lib/history';
import React, { useCallback, useState } from 'react';
import { ChannelType, SlackChannel } from './config';
import {
ChannelType,
SlackChannel,
SlackType,
WebhookChannel,
WebhookType,
} from './config';
function CreateAlertChannels({
preType = 'slack',
}: CreateAlertChannelsProps): JSX.Element {
const [formInstance] = Form.useForm();
const [selectedConfig, setSelectedConfig] = useState<Partial<SlackChannel>>({
text: ` {{ range .Alerts -}}
*Alert:* {{ .Annotations.title }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
const [selectedConfig, setSelectedConfig] = useState<
Partial<SlackChannel & WebhookChannel>
>({
text: `{{ range .Alerts -}}
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
*Summary:* {{ .Annotations.summary }}
*Description:* {{ .Annotations.description }}
*Details:*
@@ -73,17 +84,93 @@ function CreateAlertChannels({
}
setSavingState(false);
} catch (error) {
notifications.error({
message: 'Error',
description:
'An unexpected error occurred while creating this channel, please try again',
});
setSavingState(false);
}
}, [notifications, selectedConfig]);
const onWebhookHandler = useCallback(async () => {
// initial api request without auth params
let request: WebhookChannel = {
api_url: selectedConfig?.api_url || '',
name: selectedConfig?.name || '',
send_resolved: true,
};
setSavingState(true);
try {
if (selectedConfig?.username !== '' || selectedConfig?.password !== '') {
if (selectedConfig?.username !== '') {
// if username is not null then password must be passed
if (selectedConfig?.password !== '') {
request = {
...request,
username: selectedConfig.username,
password: selectedConfig.password,
};
} else {
notifications.error({
message: 'Error',
description: 'A Password must be provided with user name',
});
}
} else if (selectedConfig?.password !== '') {
// only password entered, set bearer token
request = {
...request,
username: '',
password: selectedConfig.password,
};
}
}
const response = await createWebhookApi(request);
if (response.statusCode === 200) {
notifications.success({
message: 'Success',
description: 'Successfully created the channel',
});
setTimeout(() => {
history.replace(ROUTES.SETTINGS);
}, 2000);
} else {
notifications.error({
message: 'Error',
description: response.error || 'Error while creating the channel',
});
}
} catch (error) {
notifications.error({
message: 'Error',
description:
'An unexpected error occurred while creating this channel, please try again',
});
}
setSavingState(false);
}, [notifications, selectedConfig]);
const onSaveHandler = useCallback(
async (value: ChannelType) => {
if (value == 'slack') {
onSlackHandler();
switch (value) {
case SlackType:
onSlackHandler();
break;
case WebhookType:
onWebhookHandler();
break;
default:
notifications.error({
message: 'Error',
description: 'channel type selected is invalid',
});
}
},
[onSlackHandler],
[onSlackHandler, onWebhookHandler, notifications],
);
return (
@@ -108,7 +195,7 @@ function CreateAlertChannels({
}
interface CreateAlertChannelsProps {
preType?: ChannelType;
preType: ChannelType;
}
export default CreateAlertChannels;

View File

@@ -1,28 +1,35 @@
import { Form, notification } from 'antd';
import editSlackApi from 'api/channels/editSlack';
import editWebhookApi from 'api/channels/editWebhook';
import ROUTES from 'constants/routes';
import {
ChannelType,
SlackChannel,
SlackType,
WebhookChannel,
WebhookType,
} from 'container/CreateAlertChannels/config';
import FormAlertChannels from 'container/FormAlertChannels';
import history from 'lib/history';
import { Store } from 'rc-field-form/lib/interface';
import React, { useCallback, useState } from 'react';
import { useParams } from 'react-router';
import { useParams } from 'react-router-dom';
function EditAlertChannels({
initialValue,
}: EditAlertChannelsProps): JSX.Element {
const [formInstance] = Form.useForm();
const [selectedConfig, setSelectedConfig] = useState<Partial<SlackChannel>>({
const [selectedConfig, setSelectedConfig] = useState<
Partial<SlackChannel & WebhookChannel>
>({
...initialValue,
});
const [savingState, setSavingState] = useState<boolean>(false);
const [notifications, NotificationElement] = notification.useNotification();
const { id } = useParams<{ id: string }>();
const [type, setType] = useState<ChannelType>('slack');
const [type, setType] = useState<ChannelType>(
initialValue?.type ? (initialValue.type as ChannelType) : SlackType,
);
const onTypeChangeHandler = useCallback((value: string) => {
setType(value as ChannelType);
@@ -58,13 +65,62 @@ function EditAlertChannels({
setSavingState(false);
}, [selectedConfig, notifications, id]);
const onWebhookEditHandler = useCallback(async () => {
setSavingState(true);
const { name, username, password } = selectedConfig;
const showError = (msg: string): void => {
notifications.error({
message: 'Error',
description: msg,
});
};
if (selectedConfig?.api_url === '') {
showError('Webhook URL is mandatory');
setSavingState(false);
return;
}
if (username && (!password || password === '')) {
showError('Please enter a password');
setSavingState(false);
return;
}
const response = await editWebhookApi({
api_url: selectedConfig?.api_url || '',
name: name || '',
send_resolved: true,
username,
password,
id,
});
if (response.statusCode === 200) {
notifications.success({
message: 'Success',
description: 'Channels Edited Successfully',
});
setTimeout(() => {
history.replace(ROUTES.SETTINGS);
}, 2000);
} else {
showError(response.error || 'error while updating the Channels');
}
setSavingState(false);
}, [selectedConfig, notifications, id]);
const onSaveHandler = useCallback(
(value: ChannelType) => {
if (value === 'slack') {
if (value === SlackType) {
onSlackEditHandler();
} else if (value === WebhookType) {
onWebhookEditHandler();
}
},
[onSlackEditHandler],
[onSlackEditHandler, onWebhookEditHandler],
);
const onTestHandler = useCallback(() => {
@@ -91,7 +147,9 @@ function EditAlertChannels({
}
interface EditAlertChannelsProps {
initialValue: Store;
initialValue: {
[x: string]: unknown;
};
}
export default EditAlertChannels;

View File

@@ -0,0 +1,59 @@
import { Input } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import React from 'react';
import { WebhookChannel } from '../../CreateAlertChannels/config';
function WebhookSettings({ setSelectedConfig }: WebhookProps): JSX.Element {
return (
<>
<FormItem name="api_url" label="Webhook URL">
<Input
onChange={(event): void => {
setSelectedConfig((value) => ({
...value,
api_url: event.target.value,
}));
}}
/>
</FormItem>
<FormItem
name="username"
label="User Name (optional)"
help="Leave empty for bearer auth or when authentication is not necessary."
>
<Input
onChange={(event): void => {
setSelectedConfig((value) => ({
...value,
username: event.target.value,
}));
}}
/>
</FormItem>
<FormItem
name="password"
label="Password (optional)"
help="Specify a password or bearer token"
>
<Input
type="password"
onChange={(event): void => {
setSelectedConfig((value) => ({
...value,
password: event.target.value,
}));
}}
/>
</FormItem>
</>
);
}
interface WebhookProps {
setSelectedConfig: React.Dispatch<
React.SetStateAction<Partial<WebhookChannel>>
>;
}
export default WebhookSettings;

View File

@@ -1,15 +1,18 @@
import { Form, FormInstance, Input, Select, Typography } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import { Store } from 'antd/lib/form/interface';
import ROUTES from 'constants/routes';
import {
ChannelType,
SlackChannel,
SlackType,
WebhookType,
} from 'container/CreateAlertChannels/config';
import history from 'lib/history';
import { Store } from 'rc-field-form/lib/interface';
import React from 'react';
import SlackSettings from './Settings/Slack';
import WebhookSettings from './Settings/Webhook';
import { Button } from './styles';
const { Option } = Select;
@@ -28,6 +31,16 @@ function FormAlertChannels({
initialValue,
nameDisable = false,
}: FormAlertChannelsProps): JSX.Element {
const renderSettings = (): React.ReactElement | null => {
switch (type) {
case SlackType:
return <SlackSettings setSelectedConfig={setSelectedConfig} />;
case WebhookType:
return <WebhookSettings setSelectedConfig={setSelectedConfig} />;
default:
return null;
}
};
return (
<>
{NotificationElement}
@@ -52,14 +65,13 @@ function FormAlertChannels({
<Option value="slack" key="slack">
Slack
</Option>
<Option value="webhook" key="webhook">
Webhook
</Option>
</Select>
</FormItem>
<FormItem>
{type === 'slack' && (
<SlackSettings setSelectedConfig={setSelectedConfig} />
)}
</FormItem>
<FormItem>{renderSettings()}</FormItem>
<FormItem>
<Button
@@ -89,7 +101,6 @@ interface FormAlertChannelsProps {
type: ChannelType;
setSelectedConfig: React.Dispatch<React.SetStateAction<Partial<SlackChannel>>>;
onTypeChangeHandler: (value: ChannelType) => void;
onTestHandler: () => void;
onSaveHandler: (props: ChannelType) => void;
savingState: boolean;
NotificationElement: React.ReactElement<
@@ -101,4 +112,8 @@ interface FormAlertChannelsProps {
nameDisable?: boolean;
}
FormAlertChannels.defaultProps = {
nameDisable: undefined,
};
export default FormAlertChannels;

View File

@@ -1,4 +1,3 @@
import { Tooltip, Typography } from 'antd';
import {
IIntervalUnit,
resolveTimeFromInterval,
@@ -13,21 +12,29 @@ interface SpanLengthProps {
width: string;
leftOffset: string;
bgColor: string;
toolTipText: string;
id: string;
inMsCount: number;
intervalUnit: IIntervalUnit;
}
function SpanLength(props: SpanLengthProps): JSX.Element {
const { width, leftOffset, bgColor, intervalUnit } = props;
const { width, leftOffset, bgColor, intervalUnit, inMsCount } = props;
const { isDarkMode } = useThemeMode();
return (
<SpanWrapper>
<SpanLine leftOffset={leftOffset} isDarkMode={isDarkMode} />
<SpanBorder bgColor={bgColor} leftOffset={leftOffset} width={width} />
<SpanText leftOffset={leftOffset} isDarkMode={isDarkMode}>{`${toFixed(
resolveTimeFromInterval(props.inMsCount, intervalUnit),
<SpanLine
isDarkMode={isDarkMode}
bgColor={bgColor}
leftOffset={leftOffset}
width={width}
/>
<SpanBorder
isDarkMode={isDarkMode}
bgColor={bgColor}
leftOffset={leftOffset}
width={width}
/>
<SpanText isDarkMode={isDarkMode} leftOffset={leftOffset}>{`${toFixed(
resolveTimeFromInterval(inMsCount, intervalUnit),
2,
)} ${intervalUnit.name}`}</SpanText>
</SpanWrapper>

View File

@@ -9,19 +9,19 @@ interface Props {
}
export const SpanLine = styled.div<Props>`
width: ${({ leftOffset }) => `${leftOffset}%`};
width: ${({ leftOffset }): string => `${leftOffset}%`};
height: 0px;
border-bottom: 0.1px solid
${({ isDarkMode }) => (isDarkMode ? '#303030' : '#c0c0c0')};
${({ isDarkMode }): string => (isDarkMode ? '#303030' : '#c0c0c0')};
top: 50%;
position: absolute;
`;
export const SpanBorder = styled.div<Props>`
background: ${({ bgColor }) => bgColor};
background: ${({ bgColor }): string => bgColor};
border-radius: 5px;
height: 0.625rem;
width: ${({ width }) => `${width}%`};
left: ${({ leftOffset }) => `${leftOffset}%`};
width: ${({ width }): string => `${width}%`};
left: ${({ leftOffset }): string => `${leftOffset}%`};
top: 35%;
position: absolute;
`;
@@ -45,13 +45,16 @@ export const SpanWrapper = styled.div`
z-index: 0;
} */
`;
interface SpanTextProps extends Pick<Props, 'leftOffset'> {
isDarkMode: boolean;
}
export const SpanText = styled(Typography)<Pick<Props, 'leftOffset'>>`
export const SpanText = styled(Typography)<SpanTextProps>`
&&& {
left: ${({ leftOffset }) => `${leftOffset}%`};
left: ${({ leftOffset }): string => `${leftOffset}%`};
top: 65%;
position: absolute;
color: ${({ isDarkMode }) => (isDarkMode ? '##ACACAC' : '#666')};
color: ${({ isDarkMode }): string => (isDarkMode ? '##ACACAC' : '#666')};
font-size: 0.75rem;
}
`;

View File

@@ -1,18 +1,11 @@
import React from 'react';
import {
Container,
Service,
Span,
SpanConnector,
SpanName,
SpanWrapper,
} from './styles';
import { Container, Service, Span, SpanWrapper } from './styles';
function SpanNameComponent({
name,
serviceName,
}: SpanNameComponent): JSX.Element {
}: SpanNameComponentProps): JSX.Element {
return (
<Container title={`${name} ${serviceName}`}>
<SpanWrapper>
@@ -23,7 +16,7 @@ function SpanNameComponent({
);
}
interface SpanNameComponent {
interface SpanNameComponentProps {
name: string;
serviceName: string;
}

View File

@@ -5,7 +5,7 @@ import { IIntervalUnit } from 'container/TraceDetail/utils';
import useThemeMode from 'hooks/useThemeMode';
import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants';
import React, { useEffect, useRef, useState } from 'react';
import { pushDStree } from 'store/actions';
import { ITraceTree } from 'types/api/trace/getTraceItem';
import { ITraceMetaData } from '..';
import SpanLength from '../SpanLength';
@@ -38,6 +38,7 @@ function Trace(props: TraceProps): JSX.Element {
activeSpanPath,
isExpandAll,
intervalUnit,
children,
} = props;
const { isDarkMode } = useThemeMode();
@@ -52,7 +53,7 @@ function Trace(props: TraceProps): JSX.Element {
} else if (!isOpen) {
setOpen(activeSpanPath[level] === id);
}
}, [activeSpanPath, isOpen]);
}, [activeSpanPath, isOpen, id, level]);
useEffect(() => {
if (isExpandAll) {
@@ -60,9 +61,9 @@ function Trace(props: TraceProps): JSX.Element {
} else {
setOpen(activeSpanPath[level] === id);
}
}, [isExpandAll]);
}, [isExpandAll, activeSpanPath, id, level]);
const isOnlyChild = props.children.length === 1;
const isOnlyChild = children.length === 1;
const [top, setTop] = useState<number>(0);
const ref = useRef<HTMLUListElement>(null);
@@ -75,25 +76,27 @@ function Trace(props: TraceProps): JSX.Element {
inline: 'nearest',
});
}
}, [activeSelectedId]);
}, [activeSelectedId, id]);
const onMouseEnterHandler = () => {
setActiveHoverId(props.id);
const onMouseEnterHandler = (): void => {
setActiveHoverId(id);
if (ref.current) {
const { top } = getTopLeftFromBody(ref.current);
setTop(top);
}
};
const onMouseLeaveHandler = () => {
const onMouseLeaveHandler = (): void => {
setActiveHoverId('');
};
const onClick = () => {
const onClick = (): void => {
setActiveSelectedId(id);
};
const onClickTreeExpansion = (event) => {
const onClickTreeExpansion: React.MouseEventHandler<HTMLDivElement> = (
event,
): void => {
event.stopPropagation();
setOpen((state) => {
localTreeExpandInteraction.current = !isOpen;
@@ -113,6 +116,7 @@ function Trace(props: TraceProps): JSX.Element {
onMouseLeave={onMouseLeaveHandler}
isOnlyChild={isOnlyChild}
ref={ref}
isDarkMode={isDarkMode}
>
<HoverCard
top={top}
@@ -126,7 +130,11 @@ function Trace(props: TraceProps): JSX.Element {
<StyledRow styledclass={[styles.flexNoWrap]}>
<Col>
{totalSpans !== 1 && (
<CardComponent isDarkMode={isDarkMode} onClick={onClickTreeExpansion}>
<CardComponent
isOnlyChild={isOnlyChild}
isDarkMode={isDarkMode}
onClick={onClickTreeExpansion}
>
{totalSpans}
<CaretContainer>
{isOpen ? <CaretDownFilled /> : <CaretRightFilled />}
@@ -144,7 +152,6 @@ function Trace(props: TraceProps): JSX.Element {
leftOffset={nodeLeftOffset.toString()}
width={width.toString()}
bgColor={serviceColour}
id={id}
inMsCount={inMsCount / 1e6}
intervalUnit={intervalUnit}
/>
@@ -153,11 +160,12 @@ function Trace(props: TraceProps): JSX.Element {
{isOpen && (
<>
{props.children.map((child) => (
{children.map((child) => (
<Trace
key={child.id}
activeHoverId={props.activeHoverId}
setActiveHoverId={props.setActiveHoverId}
activeHoverId={activeHoverId}
setActiveHoverId={setActiveHoverId}
// eslint-disable-next-line react/jsx-props-no-spreading
{...child}
globalSpread={globalSpread}
globalStart={globalStart}
@@ -180,7 +188,7 @@ interface ITraceGlobal {
globalStart: ITraceMetaData['globalStart'];
}
interface TraceProps extends pushDStree, ITraceGlobal {
interface TraceProps extends ITraceTree, ITraceGlobal {
activeHoverId: string;
setActiveHoverId: React.Dispatch<React.SetStateAction<string>>;
setActiveSelectedId: React.Dispatch<React.SetStateAction<string>>;

View File

@@ -1,4 +1,8 @@
import styled, { css } from 'styled-components';
import styled, {
css,
DefaultTheme,
ThemedCssFunction,
} from 'styled-components';
interface Props {
isOnlyChild: boolean;
@@ -13,9 +17,10 @@ export const Wrapper = styled.ul<Props>`
z-index: 1;
ul {
border-left: ${({ isOnlyChild }) => isOnlyChild && 'none'} !important;
border-left: ${({ isOnlyChild }): StyledCSS =>
isOnlyChild && 'none'} !important;
${({ isOnlyChild }) =>
${({ isOnlyChild }): StyledCSS =>
isOnlyChild &&
css`
&:before {
@@ -37,15 +42,27 @@ export const CardContainer = styled.li`
cursor: pointer;
`;
export const CardComponent = styled.div`
border: 1px solid ${({ isDarkMode }) => (isDarkMode ? '#434343' : '#333')};
interface Props {
isDarkMode: boolean;
}
export type StyledCSS =
| ReturnType<ThemedCssFunction<DefaultTheme>>
| string
| false
| undefined;
export const CardComponent = styled.div<Props>`
border: 1px solid
${({ isDarkMode }): StyledCSS => (isDarkMode ? '#434343' : '#333')};
box-sizing: border-box;
border-radius: 2px;
display: flex;
justify-content: center;
align-items: center;
padding: 1px 8px;
background: ${({ isDarkMode }) => (isDarkMode ? '#1d1d1d' : '#ddd')};
background: ${({ isDarkMode }): StyledCSS =>
isDarkMode ? '#1d1d1d' : '#ddd'};
height: 22px;
`;
@@ -61,13 +78,15 @@ interface HoverCardProps {
}
export const HoverCard = styled.div<HoverCardProps>`
display: ${({ isSelected, isHovered }) =>
display: ${({ isSelected, isHovered }): string =>
isSelected || isHovered ? 'block' : 'none'};
width: 200%;
background-color: ${({ isHovered, isDarkMode }) =>
isHovered && (isDarkMode ? '#262626' : '#ddd')};
background-color: ${({ isSelected, isDarkMode }) =>
isSelected && (isDarkMode ? '#4f4f4f' : '#bbb')};
background-color: ${({ isHovered, isDarkMode }): string => {
if (isHovered) {
return isDarkMode ? '#262626' : '#ddd';
}
return isDarkMode ? '#4f4f4f' : '#bbb';
}};
position: absolute;
top: 0;
left: -100%;

View File

@@ -26,13 +26,13 @@ function GanttChart(props: GanttChartProps): JSX.Element {
useEffect(() => {
setActiveSpanPath(getSpanPath(data, spanId));
}, [spanId]);
}, [spanId, data]);
useEffect(() => {
setActiveSpanPath(getSpanPath(data, activeSelectedId));
}, [activeSelectedId]);
}, [activeSelectedId, data]);
const handleCollapse = () => {
const handleCollapse = (): void => {
setIsExpandAll((prev) => !prev);
};
return (
@@ -50,6 +50,7 @@ function GanttChart(props: GanttChartProps): JSX.Element {
activeSpanPath={activeSpanPath}
setActiveHoverId={setActiveHoverId}
key={data.id}
// eslint-disable-next-line react/jsx-props-no-spreading
{...{
...data,
globalSpread,

View File

@@ -1,24 +1,33 @@
import { ITraceTree } from 'types/api/trace/getTraceItem';
export const getMetaDataFromSpanTree = (treeData: ITraceTree) => {
interface GetTraceMetaData {
globalStart: number;
globalEnd: number;
spread: number;
totalSpans: number;
levels: number;
}
export const getMetaDataFromSpanTree = (
treeData: ITraceTree,
): GetTraceMetaData => {
let globalStart = Number.POSITIVE_INFINITY;
let globalEnd = Number.NEGATIVE_INFINITY;
let totalSpans = 0;
let levels = 1;
const traverse = (treeNode: ITraceTree, level = 0) => {
const traverse = (treeNode: ITraceTree, level = 0): void => {
if (!treeNode) {
return;
}
totalSpans++;
totalSpans += 1;
levels = Math.max(levels, level);
const { startTime } = treeNode;
const endTime = startTime + treeNode.value;
globalStart = Math.min(globalStart, startTime);
globalEnd = Math.max(globalEnd, endTime);
for (const childNode of treeNode.children) {
treeNode.children.forEach((childNode) => {
traverse(childNode, level + 1);
}
});
};
traverse(treeData, 1);
@@ -34,7 +43,9 @@ export const getMetaDataFromSpanTree = (treeData: ITraceTree) => {
};
};
export function getTopLeftFromBody(elem: HTMLElement) {
export function getTopLeftFromBody(
elem: HTMLElement,
): { top: number; left: number } {
const box = elem.getBoundingClientRect();
const { body } = document;
@@ -57,18 +68,18 @@ export const getNodeById = (
treeData: ITraceTree,
): ITraceTree | undefined => {
let foundNode: ITraceTree | undefined;
const traverse = (treeNode: ITraceTree, level = 0) => {
const traverse = (treeNode: ITraceTree, level = 0): void => {
if (!treeNode) {
return;
}
if (searchingId == treeNode.id) {
if (searchingId === treeNode.id) {
foundNode = treeNode;
}
for (const childNode of treeNode.children) {
treeNode.children.forEach((childNode) => {
traverse(childNode, level + 1);
}
});
};
traverse(treeData, 1);
@@ -88,7 +99,7 @@ const getSpanWithoutChildren = (
tags: span.tags,
time: span.time,
value: span.value,
error: span.error,
event: span.event,
hasError: span.hasError,
};
};
@@ -101,10 +112,7 @@ export const isSpanPresentInSearchString = (
const stringifyTree = JSON.stringify(parsedTree);
if (stringifyTree.includes(searchedString)) {
return true;
}
return false;
return stringifyTree.includes(searchedString);
};
export const isSpanPresent = (
@@ -117,7 +125,7 @@ export const isSpanPresent = (
treeNode: ITraceTree,
level = 0,
foundNode: ITraceTree[],
) => {
): void => {
if (!treeNode) {
return;
}
@@ -128,9 +136,9 @@ export const isSpanPresent = (
foundNode.push(treeNode);
}
for (const childNode of treeNode.children) {
treeNode.children.forEach((childNode) => {
traverse(childNode, level + 1, foundNode);
}
});
};
traverse(tree, 1, foundNode);
@@ -140,7 +148,7 @@ export const isSpanPresent = (
export const getSpanPath = (tree: ITraceTree, spanId: string): string[] => {
const spanPath: string[] = [];
const traverse = (treeNode: ITraceTree) => {
const traverse = (treeNode: ITraceTree): boolean => {
if (!treeNode) {
return false;
}
@@ -152,9 +160,9 @@ export const getSpanPath = (tree: ITraceTree, spanId: string): string[] => {
}
let foundInChild = false;
for (const childNode of treeNode.children) {
treeNode.children.forEach((childNode) => {
if (traverse(childNode)) foundInChild = true;
}
});
if (!foundInChild) {
spanPath.pop();
}

View File

@@ -1,109 +1,118 @@
import { DownOutlined } from '@ant-design/icons';
import { Button, Menu } from 'antd';
import { MenuInfo } from 'rc-menu/lib/interface';
import React from 'react';
import { Col, Row, Select } from 'antd';
import { find } from 'lodash-es';
import React, { useEffect, useRef, useState } from 'react';
import { SettingPeroid } from '.';
import {
Dropdown,
Input,
RetentionContainer,
TextContainer,
Typography,
RetentionFieldInputContainer,
RetentionFieldLabel,
} from './styles';
import {
convertHoursValueToRelevantUnit,
SettingPeriod,
TimeUnits,
} from './utils';
const { Option } = Select;
function Retention({
retentionValue,
setRentionValue,
selectedRetentionPeroid,
setSelectedRetentionPeroid,
setRetentionValue,
text,
}: RetentionProps): JSX.Element {
const options: Option[] = [
{
key: 'hr',
value: 'Hrs',
},
{
key: 'day',
value: 'Days',
},
{
key: 'month',
value: 'Months',
},
];
const onClickHandler = (
e: MenuInfo,
func: React.Dispatch<React.SetStateAction<SettingPeroid>>,
): void => {
const selected = e.key as SettingPeroid;
func(selected);
};
const menu = (
<Menu onClick={(e): void => onClickHandler(e, setSelectedRetentionPeroid)}>
{options.map((option) => (
<Menu.Item key={option.key}>{option.value}</Menu.Item>
))}
</Menu>
hide,
}: RetentionProps): JSX.Element | null {
const {
value: initialValue,
timeUnitValue: initialTimeUnitValue,
} = convertHoursValueToRelevantUnit(Number(retentionValue));
const [selectedTimeUnit, setSelectTimeUnit] = useState(initialTimeUnitValue);
const [selectedValue, setSelectedValue] = useState<number | null>(
initialValue,
);
const interacted = useRef(false);
useEffect(() => {
if (!interacted.current) setSelectedValue(initialValue);
}, [initialValue]);
const currentSelectedOption = (option: SettingPeroid): string | undefined => {
return options.find((e) => e.key === option)?.value;
useEffect(() => {
if (!interacted.current) setSelectTimeUnit(initialTimeUnitValue);
}, [initialTimeUnitValue]);
const menuItems = TimeUnits.map((option) => (
<Option key={option.value} value={option.value}>
{option.key}
</Option>
));
const currentSelectedOption = (option: SettingPeriod): void => {
const selectedValue = find(TimeUnits, (e) => e.value === option)?.value;
if (selectedValue) setSelectTimeUnit(selectedValue);
};
useEffect(() => {
const inverseMultiplier = find(
TimeUnits,
(timeUnit) => timeUnit.value === selectedTimeUnit,
)?.multiplier;
if (!selectedValue) setRetentionValue(null);
if (selectedValue && inverseMultiplier) {
setRetentionValue(selectedValue * (1 / inverseMultiplier));
}
}, [selectedTimeUnit, selectedValue, setRetentionValue]);
const onChangeHandler = (
e: React.ChangeEvent<HTMLInputElement>,
func: React.Dispatch<React.SetStateAction<string>>,
func: React.Dispatch<React.SetStateAction<number | null>>,
): void => {
interacted.current = true;
const { value } = e.target;
const integerValue = parseInt(value, 10);
if (value.length > 0 && integerValue.toString() === value) {
const parsedValue = Math.abs(integerValue).toString();
const parsedValue = Math.abs(integerValue);
func(parsedValue);
}
if (value.length === 0) {
func('');
func(null);
}
};
if (hide) {
return null;
}
return (
<RetentionContainer>
<TextContainer>
<Typography>{text}</Typography>
</TextContainer>
<Input
value={retentionValue}
onChange={(e): void => onChangeHandler(e, setRentionValue)}
/>
<Dropdown overlay={menu}>
<Button>
{currentSelectedOption(selectedRetentionPeroid)} <DownOutlined />
</Button>
</Dropdown>
<Row justify="space-between">
<Col flex={1} style={{ display: 'flex' }}>
<RetentionFieldLabel>{text}</RetentionFieldLabel>
</Col>
<Col flex="150px">
<RetentionFieldInputContainer>
<Input
value={selectedValue && selectedValue >= 0 ? selectedValue : ''}
onChange={(e): void => onChangeHandler(e, setSelectedValue)}
style={{ width: 75 }}
/>
<Select
value={selectedTimeUnit}
onChange={currentSelectedOption}
style={{ width: 100 }}
>
{menuItems}
</Select>
</RetentionFieldInputContainer>
</Col>
</Row>
</RetentionContainer>
);
}
interface Option {
key: SettingPeroid;
value: string;
}
interface RetentionProps {
retentionValue: string;
retentionValue: number | null;
text: string;
setRentionValue: React.Dispatch<React.SetStateAction<string>>;
selectedRetentionPeroid: SettingPeroid;
setSelectedRetentionPeroid: React.Dispatch<
React.SetStateAction<SettingPeroid>
>;
setRetentionValue: React.Dispatch<React.SetStateAction<number | null>>;
hide: boolean;
}
export default Retention;

View File

@@ -1,189 +1,254 @@
import { Button, Modal, notification, Typography } from 'antd';
import getRetentionperoidApi from 'api/settings/getRetention';
import { Button, Col, Modal, notification, Row, Typography } from 'antd';
import getDisks from 'api/disks/getDisks';
import getRetentionPeriodApi from 'api/settings/getRetention';
import setRetentionApi from 'api/settings/setRetention';
import Spinner from 'components/Spinner';
import TextToolTip from 'components/TextToolTip';
import useFetch from 'hooks/useFetch';
import convertIntoHr from 'lib/convertIntoHr';
import getSettingsPeroid from 'lib/getSettingsPeroid';
import React, { useCallback, useEffect, useState } from 'react';
import { find } from 'lodash-es';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IDiskType } from 'types/api/disks/getDisks';
import { PayloadProps } from 'types/api/settings/getRetention';
import Retention from './Retention';
import {
ButtonContainer,
Container,
ErrorText,
ErrorTextContainer,
ToolTipContainer,
} from './styles';
function GeneralSettings(): JSX.Element {
const [
selectedMetricsPeroid,
setSelectedMetricsPeroid,
] = useState<SettingPeroid>('month');
const { t } = useTranslation();
const [notifications, Element] = notification.useNotification();
const [retentionPeroidMetrics, setRetentionPeroidMetrics] = useState<string>(
'',
);
const [modal, setModal] = useState<boolean>(false);
const [postApiLoading, setPostApiLoading] = useState<boolean>(false);
const [selectedTracePeroid, setSelectedTracePeroid] = useState<SettingPeroid>(
'hr',
);
const [availableDisks, setAvailableDisks] = useState<IDiskType[] | null>(null);
const [retentionPeroidTrace, setRetentionPeroidTrace] = useState<string>('');
const [isDefaultMetrics, setIsDefaultMetrics] = useState<boolean>(false);
const [isDefaultTrace, setIsDefaultTrace] = useState<boolean>(false);
const onClickSaveHandler = useCallback(() => {
onModalToggleHandler();
useEffect(() => {
getDisks().then((response) => setAvailableDisks(response.payload));
}, []);
const { payload, loading, error, errorMessage } = useFetch<
const { payload: currentTTLValues, loading, error, errorMessage } = useFetch<
PayloadProps,
undefined
>(getRetentionperoidApi, undefined);
>(getRetentionPeriodApi, undefined);
const [metricsTotalRetentionPeriod, setMetricsTotalRetentionPeriod] = useState<
number | null
>(null);
const [metricsS3RetentionPeriod, setMetricsS3RetentionPeriod] = useState<
number | null
>(null);
const [tracesTotalRetentionPeriod, setTracesTotalRetentionPeriod] = useState<
number | null
>(null);
const [tracesS3RetentionPeriod, setTracesS3RetentionPeriod] = useState<
number | null
>(null);
useEffect(() => {
if (currentTTLValues) {
setMetricsTotalRetentionPeriod(currentTTLValues.metrics_ttl_duration_hrs);
setMetricsS3RetentionPeriod(
currentTTLValues.metrics_move_ttl_duration_hrs
? currentTTLValues.metrics_move_ttl_duration_hrs
: null,
);
setTracesTotalRetentionPeriod(currentTTLValues.traces_ttl_duration_hrs);
setTracesS3RetentionPeriod(
currentTTLValues.traces_move_ttl_duration_hrs
? currentTTLValues.traces_move_ttl_duration_hrs
: null,
);
}
}, [currentTTLValues]);
const onModalToggleHandler = (): void => {
setModal((modal) => !modal);
};
const checkMetricTraceDefault = (trace: number, metric: number): void => {
if (metric === -1) {
setIsDefaultMetrics(true);
} else {
setIsDefaultMetrics(false);
const onClickSaveHandler = useCallback(() => {
onModalToggleHandler();
}, []);
const s3Enabled = useMemo(
() => !!find(availableDisks, (disks: IDiskType) => disks?.type === 's3'),
[availableDisks],
);
const renderConfig = [
{
name: 'Metrics',
retentionFields: [
{
name: t('settings.total_retention_period'),
value: metricsTotalRetentionPeriod,
setValue: setMetricsTotalRetentionPeriod,
},
{
name: t('settings.move_to_s3'),
value: metricsS3RetentionPeriod,
setValue: setMetricsS3RetentionPeriod,
hide: !s3Enabled,
},
],
},
{
name: 'Traces',
retentionFields: [
{
name: t('settings.total_retention_period'),
value: tracesTotalRetentionPeriod,
setValue: setTracesTotalRetentionPeriod,
},
{
name: t('settings.move_to_s3'),
value: tracesS3RetentionPeriod,
setValue: setTracesS3RetentionPeriod,
hide: !s3Enabled,
},
],
},
].map((category): JSX.Element | null => {
if (
Array.isArray(category.retentionFields) &&
category.retentionFields.length > 0
) {
return (
<Col flex="40%" style={{ minWidth: 475 }} key={category.name}>
<Typography.Title level={3}>{category.name}</Typography.Title>
{category.retentionFields.map((retentionField) => (
<Retention
key={retentionField.name}
text={retentionField.name}
retentionValue={retentionField.value}
setRetentionValue={retentionField.setValue}
hide={!!retentionField.hide}
/>
))}
</Col>
);
}
if (trace === -1) {
setIsDefaultTrace(true);
} else {
setIsDefaultTrace(false);
}
};
useEffect(() => {
if (!loading && payload !== undefined) {
const { metrics_ttl_duration_hrs, traces_ttl_duration_hrs } = payload;
checkMetricTraceDefault(traces_ttl_duration_hrs, metrics_ttl_duration_hrs);
const traceValue = getSettingsPeroid(traces_ttl_duration_hrs);
const metricsValue = getSettingsPeroid(metrics_ttl_duration_hrs);
setRetentionPeroidTrace(traceValue.value.toString());
setSelectedTracePeroid(traceValue.peroid);
setRetentionPeroidMetrics(metricsValue.value.toString());
setSelectedMetricsPeroid(metricsValue.peroid);
}
}, [setSelectedMetricsPeroid, loading, payload]);
return null;
});
const onOkHandler = async (): Promise<void> => {
try {
setPostApiLoading(true);
const retentionTraceValue =
retentionPeroidTrace === '0' && (payload?.traces_ttl_duration_hrs || 0) < 0
? payload?.traces_ttl_duration_hrs || 0
: parseInt(retentionPeroidTrace, 10);
const retentionMetricsValue =
retentionPeroidMetrics === '0' &&
(payload?.metrics_ttl_duration_hrs || 0) < 0
? payload?.metrics_ttl_duration_hrs || 0
: parseInt(retentionPeroidMetrics, 10);
const [tracesResponse, metricsResponse] = await Promise.all([
const [metricsTTLApiResponse, tracesTTLApiResponse] = await Promise.all([
setRetentionApi({
duration: `${convertIntoHr(retentionTraceValue, selectedTracePeroid)}h`,
type: 'traces',
type: 'metrics',
totalDuration: `${metricsTotalRetentionPeriod || -1}h`,
coldStorage: s3Enabled ? 's3' : null,
toColdDuration: `${metricsS3RetentionPeriod || -1}h`,
}),
setRetentionApi({
duration: `${convertIntoHr(
retentionMetricsValue,
selectedMetricsPeroid,
)}h`,
type: 'metrics',
type: 'traces',
totalDuration: `${tracesTotalRetentionPeriod || -1}h`,
coldStorage: s3Enabled ? 's3' : null,
toColdDuration: `${tracesS3RetentionPeriod || -1}h`,
}),
]);
[
{
apiResponse: metricsTTLApiResponse,
name: 'metrics',
},
{
apiResponse: tracesTTLApiResponse,
name: 'traces',
},
].forEach(({ apiResponse, name }) => {
if (apiResponse.statusCode === 200) {
notifications.success({
message: 'Success!',
placement: 'topRight',
if (
tracesResponse.statusCode === 200 &&
metricsResponse.statusCode === 200
) {
notifications.success({
message: 'Success!',
placement: 'topRight',
description: 'Congrats. The retention periods were updated correctly.',
});
checkMetricTraceDefault(retentionTraceValue, retentionMetricsValue);
onModalToggleHandler();
} else {
notifications.error({
message: 'Error',
description:
'There was an issue in changing the retention period. Please try again or reach out to support@signoz.io',
placement: 'topRight',
});
}
description: t('settings.retention_success_message', { name }),
});
} else {
notifications.error({
message: 'Error',
description: t('settings.retention_error_message', { name }),
placement: 'topRight',
});
}
});
onModalToggleHandler();
setPostApiLoading(false);
} catch (error) {
notifications.error({
message: 'Error',
description:
'There was an issue in changing the retention period. Please try again or reach out to support@signoz.io',
description: t('settings.retention_failed_message'),
placement: 'topRight',
});
}
setModal(false);
};
const [isDisabled, errorText] = useMemo(() => {
// Various methods to return dynamic error message text.
const messages = {
compareError: (name: string | number): string =>
t('settings.retention_comparison_error', { name }),
nullValueError: (name: string | number): string =>
t('settings.retention_null_value_error', { name }),
};
// Defaults to button not disabled and empty error message text.
let isDisabled = false;
let errorText = '';
if (s3Enabled) {
if (
(metricsTotalRetentionPeriod || metricsS3RetentionPeriod) &&
Number(metricsTotalRetentionPeriod) <= Number(metricsS3RetentionPeriod)
) {
isDisabled = true;
errorText = messages.compareError('metrics');
} else if (
(tracesTotalRetentionPeriod || tracesS3RetentionPeriod) &&
Number(tracesTotalRetentionPeriod) <= Number(tracesS3RetentionPeriod)
) {
isDisabled = true;
errorText = messages.compareError('traces');
}
}
if (!metricsTotalRetentionPeriod || !tracesTotalRetentionPeriod) {
isDisabled = true;
if (!metricsTotalRetentionPeriod && !tracesTotalRetentionPeriod) {
errorText = messages.nullValueError('metrics and traces');
} else if (!metricsTotalRetentionPeriod) {
errorText = messages.nullValueError('metrics');
} else if (!tracesTotalRetentionPeriod) {
errorText = messages.nullValueError('traces');
}
}
return [isDisabled, errorText];
}, [
metricsS3RetentionPeriod,
metricsTotalRetentionPeriod,
s3Enabled,
t,
tracesS3RetentionPeriod,
tracesTotalRetentionPeriod,
]);
if (error) {
return <Typography>{errorMessage}</Typography>;
}
if (loading || payload === undefined) {
if (loading || currentTTLValues === undefined) {
return <Spinner tip="Loading.." height="70vh" />;
}
const getErrorText = (): string => {
const getValue = (value: string): string =>
`Retention Peroid for ${value} is not set yet. Please set by choosing below`;
if (!isDefaultMetrics && !isDefaultTrace) {
return '';
}
if (isDefaultMetrics && !isDefaultTrace) {
return `${getValue('Metrics')}`;
}
if (!isDefaultMetrics && isDefaultTrace) {
return `${getValue('Trace')}`;
}
return `${getValue('Trace , Metrics')}`;
};
const isDisabledHandler = (): boolean => {
if (retentionPeroidTrace === '' || retentionPeroidMetrics === '') {
return true;
}
return false;
};
const errorText = getErrorText();
return (
<Container>
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
{Element}
{errorText ? (
<ErrorTextContainer>
<ErrorText>{errorText}</ErrorText>
@@ -205,25 +270,10 @@ function GeneralSettings(): JSX.Element {
/>
</ToolTipContainer>
)}
<Retention
text="Retention Period for Metrics"
selectedRetentionPeroid={selectedMetricsPeroid}
setRentionValue={setRetentionPeroidMetrics}
retentionValue={retentionPeroidMetrics}
setSelectedRetentionPeroid={setSelectedMetricsPeroid}
/>
<Retention
text="Retention Period for Traces"
selectedRetentionPeroid={selectedTracePeroid}
setRentionValue={setRetentionPeroidTrace}
retentionValue={retentionPeroidTrace}
setSelectedRetentionPeroid={setSelectedTracePeroid}
/>
<Row justify="space-around">{renderConfig}</Row>
<Modal
title="Are you sure you want to change the retention period?"
title={t('settings.retention_confirmation')}
focusTriggerAfterClose
forceRender
destroyOnClose
@@ -234,24 +284,18 @@ function GeneralSettings(): JSX.Element {
visible={modal}
confirmLoading={postApiLoading}
>
<Typography>
This will change the amount of storage needed for saving metrics & traces.
</Typography>
<Typography>{t('settings.retention_confirmation_description')}</Typography>
</Modal>
<ButtonContainer>
<Button
onClick={onClickSaveHandler}
disabled={isDisabledHandler()}
type="primary"
>
<Button onClick={onClickSaveHandler} disabled={isDisabled} type="primary">
Save
</Button>
</ButtonContainer>
</Container>
</Col>
);
}
export type SettingPeroid = 'hr' | 'day' | 'month';
export type SettingPeriod = 'hr' | 'day' | 'month';
export default GeneralSettings;

View File

@@ -1,16 +1,13 @@
import {
Col,
Dropdown as DropDownComponent,
Input as InputComponent,
Typography as TypographyComponent,
} from 'antd';
import styled from 'styled-components';
export const RetentionContainer = styled.div`
width: 50%;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
export const RetentionContainer = styled(Col)`
margin: 0.75rem 0;
`;
export const Input = styled(InputComponent)`
@@ -37,13 +34,6 @@ export const ButtonContainer = styled.div`
}
`;
export const Container = styled.div`
&&& {
display: flex;
flex-direction: column;
}
`;
export const Dropdown = styled(DropDownComponent)`
&&& {
display: flex;
@@ -90,3 +80,12 @@ export const ErrorText = styled(TypographyComponent)`
font-style: italic;
}
`;
export const RetentionFieldLabel = styled(TypographyComponent)`
vertical-align: middle;
white-space: pre-wrap;
`;
export const RetentionFieldInputContainer = styled.div`
display: inline-flex;
`;

View File

@@ -0,0 +1,42 @@
export type SettingPeriod = 'hr' | 'day' | 'month';
export interface ITimeUnit {
value: SettingPeriod;
key: string;
multiplier: number;
}
export const TimeUnits: ITimeUnit[] = [
{
value: 'hr',
key: 'Hours',
multiplier: 1,
},
{
value: 'day',
key: 'Days',
multiplier: 1 / 24,
},
{
value: 'month',
key: 'Months',
multiplier: 1 / (24 * 30),
},
];
export const convertHoursValueToRelevantUnit = (
value: number,
): { value: number; timeUnitValue: SettingPeriod } => {
if (value)
for (let idx = TimeUnits.length - 1; idx >= 0; idx -= 1) {
const timeUnit = TimeUnits[idx];
const convertedValue = timeUnit.multiplier * value;
if (
convertedValue >= 1 &&
convertedValue === parseInt(`${convertedValue}`, 10)
) {
return { value: convertedValue, timeUnitValue: timeUnit.value };
}
}
return { value, timeUnitValue: TimeUnits[0].value };
};

View File

@@ -1,6 +1,7 @@
import { Typography } from 'antd';
import { ChartData } from 'chart.js';
import Graph, { graphOnClickHandler } from 'components/Graph';
import Graph, { GraphOnClickHandler } from 'components/Graph';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import ValueGraph from 'components/ValueGraph';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import history from 'lib/history';
@@ -57,7 +58,11 @@ function GridGraphComponent({
<Typography>{title}</Typography>
</TitleContainer>
<ValueContainer isDashboardPage={isDashboardPage}>
<ValueGraph value={value.toString()} />
<ValueGraph
value={
yAxisUnit ? getYAxisFormattedValue(value, yAxisUnit) : value.toString()
}
/>
</ValueContainer>
</>
);
@@ -72,9 +77,17 @@ export interface GridGraphComponentProps {
title?: string;
opacity?: string;
isStacked?: boolean;
onClickHandler?: graphOnClickHandler;
onClickHandler?: GraphOnClickHandler;
name: string;
yAxisUnit?: string;
}
GridGraphComponent.defaultProps = {
title: undefined,
opacity: undefined,
isStacked: undefined,
onClickHandler: undefined,
yAxisUnit: undefined,
};
export default GridGraphComponent;

View File

@@ -1,90 +0,0 @@
import Graph, { graphOnClickHandler } from 'components/Graph';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
import GetMaxMinTime from 'lib/getMaxMinTime';
import { colors } from 'lib/getRandomColor';
import getStartAndEndTime from 'lib/getStartAndEndTime';
import getTimeString from 'lib/getTimeString';
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
function EmptyGraph({
selectedTime,
widget,
onClickHandler,
}: EmptyGraphProps): JSX.Element {
const { minTime, maxTime, loading } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const maxMinTime = GetMaxMinTime({
graphType: widget.panelTypes,
maxTime,
minTime,
});
const { end, start } = getStartAndEndTime({
type: selectedTime.enum,
maxTime: maxMinTime.maxTime,
minTime: maxMinTime.minTime,
});
const dateFunction = useCallback(() => {
if (!loading) {
const dates: Date[] = [];
const startString = getTimeString(start);
const endString = getTimeString(end);
const parsedStart = parseInt(startString, 10);
const parsedEnd = parseInt(endString, 10);
let startDate = parsedStart;
const endDate = parsedEnd;
while (endDate >= startDate) {
const newDate = new Date(startDate);
startDate += 20000;
dates.push(newDate);
}
return dates;
}
return [];
}, [start, end, loading]);
const date = dateFunction();
return (
<Graph
{...{
type: 'line',
onClickHandler,
data: {
datasets: [
{
data: new Array(date?.length).fill(0),
borderColor: colors[0],
showLine: true,
borderWidth: 1.5,
spanGaps: true,
pointRadius: 0,
},
],
labels: date,
},
}}
/>
);
}
interface EmptyGraphProps {
selectedTime: timePreferance;
widget: Widgets;
onClickHandler: graphOnClickHandler | undefined;
}
export default EmptyGraph;

View File

@@ -2,7 +2,7 @@ import { Button, Typography } from 'antd';
import getQueryResult from 'api/widgets/getQuery';
import { AxiosError } from 'axios';
import { ChartData } from 'chart.js';
import { graphOnClickHandler } from 'components/Graph';
import { GraphOnClickHandler } from 'components/Graph';
import Spinner from 'components/Spinner';
import TimePreference from 'components/TimePreferenceDropDown';
import GridGraphComponent from 'container/GridGraphComponent';
@@ -23,14 +23,12 @@ import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import EmptyGraph from './EmptyGraph';
import { NotFoundContainer, TimeContainer } from './styles';
function FullView({
widget,
fullViewOptions = true,
onClickHandler,
noDataGraph = false,
name,
yAxisUnit,
}: FullViewProps): JSX.Element {
@@ -65,7 +63,9 @@ function FullView({
minTime,
});
const getMinMax = (time: timePreferenceType) => {
const getMinMax = (
time: timePreferenceType,
): { min: string | number; max: string | number } => {
if (time === 'GLOBAL_TIME') {
const minMax = GetMinMax(globalSelectedTime);
return {
@@ -142,7 +142,7 @@ function FullView({
loading: false,
}));
}
}, [widget, maxTime, minTime, selectedTime.enum]);
}, [widget, maxTime, minTime, selectedTime.enum, globalSelectedTime]);
useEffect(() => {
onFetchDataHandler();
@@ -164,38 +164,6 @@ function FullView({
);
}
if (state.loading === false && state.payload.datasets.length === 0) {
return (
<>
{fullViewOptions && (
<TimeContainer>
<TimePreference
{...{
selectedTime,
setSelectedTime,
}}
/>
<Button onClick={onFetchDataHandler} type="primary">
Refresh
</Button>
</TimeContainer>
)}
{noDataGraph ? (
<EmptyGraph
onClickHandler={onClickHandler}
widget={widget}
selectedTime={selectedTime}
/>
) : (
<NotFoundContainer>
<Typography>No Data</Typography>
</NotFoundContainer>
)}
</>
);
}
return (
<>
{fullViewOptions && (
@@ -240,10 +208,15 @@ interface FullViewState {
interface FullViewProps {
widget: Widgets;
fullViewOptions?: boolean;
onClickHandler?: graphOnClickHandler;
noDataGraph?: boolean;
onClickHandler?: GraphOnClickHandler;
name: string;
yAxisUnit?: string;
}
FullView.defaultProps = {
fullViewOptions: undefined,
onClickHandler: undefined,
yAxisUnit: undefined,
};
export default FullView;

View File

@@ -124,6 +124,13 @@ function GridCardGraph({
[],
);
const onDeleteHandler = useCallback(() => {
deleteWidget({ widgetId: widget.id });
onToggleModal(setDeletModal);
// eslint-disable-next-line no-param-reassign
isDeleted.current = true;
}, [deleteWidget, widget, onToggleModal, isDeleted]);
const getModals = (): JSX.Element => {
return (
<>
@@ -160,12 +167,6 @@ function GridCardGraph({
);
};
const onDeleteHandler = useCallback(() => {
deleteWidget({ widgetId: widget.id });
onToggleModal(setDeletModal);
isDeleted.current = true;
}, [deleteWidget, widget, onToggleModal, isDeleted]);
if (state.error) {
return (
<>

View File

@@ -6,11 +6,9 @@ import { notification } from 'antd';
import updateDashboardApi from 'api/dashboard/update';
import Spinner from 'components/Spinner';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import history from 'lib/history';
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import { Layout } from 'react-grid-layout';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import DashboardReducer from 'types/reducer/dashboards';
import { v4 } from 'uuid';
@@ -156,7 +154,8 @@ function GridGraph(): JSX.Element {
});
} catch (error) {
notification.error({
message: error.toString() || 'Something went wrong',
message:
error instanceof Error ? error.toString() : 'Something went wrong',
});
}
}

View File

@@ -1,21 +1,23 @@
import { Breadcrumb } from 'antd';
import ROUTES from 'constants/routes';
import React from 'react';
import { RouteComponentProps, withRouter } from 'react-router';
import { Link } from 'react-router-dom';
import { Link, RouteComponentProps, withRouter } from 'react-router-dom';
const breadcrumbNameMap = {
[ROUTES.APPLICATION]: 'Application',
[ROUTES.TRACES]: 'Traces',
[ROUTES.TRACE]: 'Traces',
[ROUTES.SERVICE_MAP]: 'Service Map',
[ROUTES.USAGE_EXPLORER]: 'Usage Explorer',
[ROUTES.INSTRUMENTATION]: 'Add instrumentation',
[ROUTES.SETTINGS]: 'Settings',
[ROUTES.DASHBOARD]: 'Dashboard',
[ROUTES.VERSION]: 'Status',
};
function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element {
const pathArray = props.location.pathname.split('/').filter((i) => i);
const { location } = props;
const pathArray = location.pathname.split('/').filter((i) => i);
const extraBreadcrumbItems = pathArray.map((_, index) => {
const url = `/${pathArray.slice(0, index + 1).join('/')}`;

View File

@@ -1,3 +1,4 @@
/* eslint-disable react/jsx-no-bind */
import { Modal } from 'antd';
import DatePicker from 'components/DatePicker';
import dayjs, { Dayjs } from 'dayjs';
@@ -22,10 +23,7 @@ function CustomDateTimeModal({
}
function disabledDate(current: Dayjs): boolean {
if (current > dayjs()) {
return true;
}
return false;
return current > dayjs();
}
return (

View File

@@ -1,25 +1,25 @@
import ROUTES from 'constants/routes';
type fiveMin = '5min';
type fifteenMin = '15min';
type thrityMin = '30min';
type oneMin = '1min';
type sixHour = '6hr';
type oneHour = '1hr';
type oneDay = '1day';
type oneWeek = '1week';
type custom = 'custom';
type FiveMin = '5min';
type FifteenMin = '15min';
type ThirtyMin = '30min';
type OneMin = '1min';
type SixHour = '6hr';
type OneHour = '1hr';
type OneDay = '1day';
type OneWeek = '1week';
type Custom = 'custom';
export type Time =
| fiveMin
| fifteenMin
| thrityMin
| oneMin
| sixHour
| oneHour
| custom
| oneWeek
| oneDay;
| FiveMin
| FifteenMin
| ThirtyMin
| OneMin
| SixHour
| OneHour
| Custom
| OneWeek
| OneDay;
export const Options: Option[] = [
{ value: '5min', label: 'Last 5 min' },

View File

@@ -1,12 +1,12 @@
import { Button, Select as DefaultSelect } from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { LOCAL_STORAGE } from 'constants/localStorage';
import { LOCALSTORAGE } from 'constants/localStorage';
import dayjs, { Dayjs } from 'dayjs';
import getTimeString from 'lib/getTimeString';
import React, { useCallback, useEffect, useState } from 'react';
import { connect, useSelector } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GlobalTimeLoading, UpdateTimeInterval } from 'store/actions';
@@ -26,7 +26,7 @@ function DateTimeSelection({
updateTimeInterval,
globalTimeLoading,
}: Props): JSX.Element {
const [form_dtselector] = Form.useForm();
const [formSelector] = Form.useForm();
const params = new URLSearchParams(location.search);
const searchStartTime = params.get('startTime');
@@ -72,10 +72,27 @@ function DateTimeSelection({
GlobalReducer
>((state) => state.globalTime);
const getInputLabel = (
startTime?: Dayjs,
endTime?: Dayjs,
timeInterval: Time = '15min',
): string | Time => {
if (startTime && endTime && timeInterval === 'custom') {
const format = 'YYYY/MM/DD HH:mm';
const startString = startTime.format(format);
const endString = endTime.format(format);
return `${startString} - ${endString}`;
}
return timeInterval;
};
const getDefaultTime = (pathName: string): Time => {
const defaultSelectedOption = getDefaultOption(pathName);
const routes = getLocalStorageKey(LOCAL_STORAGE.METRICS_TIME_IN_DURATION);
const routes = getLocalStorageKey(LOCALSTORAGE.METRICS_TIME_IN_DURATION);
if (routes !== null) {
const routesObject = JSON.parse(routes || '{}');
@@ -94,7 +111,7 @@ function DateTimeSelection({
);
const updateLocalStorageForRoutes = (value: Time): void => {
const preRoutes = getLocalStorageKey(LOCAL_STORAGE.METRICS_TIME_IN_DURATION);
const preRoutes = getLocalStorageKey(LOCALSTORAGE.METRICS_TIME_IN_DURATION);
if (preRoutes !== null) {
const preRoutesObject = JSON.parse(preRoutes);
@@ -104,46 +121,12 @@ function DateTimeSelection({
preRoute[location.pathname] = value;
setLocalStorageKey(
LOCAL_STORAGE.METRICS_TIME_IN_DURATION,
LOCALSTORAGE.METRICS_TIME_IN_DURATION,
JSON.stringify(preRoute),
);
}
};
const onSelectHandler = (value: Time): void => {
if (value !== 'custom') {
updateTimeInterval(value);
const selectedLabel = getInputLabel(undefined, undefined, value);
setSelectedTimeInterval(selectedLabel as Time);
updateLocalStorageForRoutes(value);
} else {
setRefreshButtonHidden(true);
setCustomDTPickerVisible(true);
}
};
const onRefreshHandler = (): void => {
onSelectHandler(selectedTimeInterval);
onLastRefreshHandler();
};
const getInputLabel = (
startTime?: Dayjs,
endTime?: Dayjs,
timeInterval: Time = '15min',
): string | Time => {
if (startTime && endTime && timeInterval === 'custom') {
const format = 'YYYY/MM/DD HH:mm';
const startString = startTime.format(format);
const endString = endTime.format(format);
return `${startString} - ${endString}`;
}
return timeInterval;
};
const onLastRefreshHandler = useCallback(() => {
const currentTime = dayjs();
@@ -177,6 +160,23 @@ function DateTimeSelection({
return `Last refresh - ${secondsDiff} sec ago`;
}, [maxTime, minTime, selectedTimeInterval]);
const onSelectHandler = (value: Time): void => {
if (value !== 'custom') {
updateTimeInterval(value);
const selectedLabel = getInputLabel(undefined, undefined, value);
setSelectedTimeInterval(selectedLabel as Time);
updateLocalStorageForRoutes(value);
} else {
setRefreshButtonHidden(true);
setCustomDTPickerVisible(true);
}
};
const onRefreshHandler = (): void => {
onSelectHandler(selectedTimeInterval);
onLastRefreshHandler();
};
const onCustomDateHandler = (dateTimeRange: DateTimeRangeType): void => {
if (dateTimeRange !== null) {
const [startTimeMoment, endTimeMoment] = dateTimeRange;
@@ -199,12 +199,12 @@ function DateTimeSelection({
// this is triggred when we change the routes and based on that we are changing the default options
useEffect(() => {
const metricsTimeDuration = getLocalStorageKey(
LOCAL_STORAGE.METRICS_TIME_IN_DURATION,
LOCALSTORAGE.METRICS_TIME_IN_DURATION,
);
if (metricsTimeDuration === null) {
setLocalStorageKey(
LOCAL_STORAGE.METRICS_TIME_IN_DURATION,
LOCALSTORAGE.METRICS_TIME_IN_DURATION,
JSON.stringify({}),
);
}
@@ -252,12 +252,12 @@ function DateTimeSelection({
return (
<Container>
<Form
form={form_dtselector}
form={formSelector}
layout="inline"
initialValues={{ interval: selectedTime }}
>
<DefaultSelect
onSelect={(value): void => onSelectHandler(value as Time)}
onSelect={(value: unknown): void => onSelectHandler(value as Time)}
value={getInputLabel(startTime, endTime, selectedTime)}
data-testid="dropDown"
>

View File

@@ -2,7 +2,7 @@ import { Col } from 'antd';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import React from 'react';
import { matchPath, useLocation } from 'react-router-dom';
import { matchPath } from 'react-router-dom';
import ShowBreadcrumbs from './Breadcrumbs';
import DateTimeSelector from './DateTimeSelection';
@@ -21,7 +21,7 @@ function TopNav(): JSX.Element | null {
}
const checkRouteExists = (currentPath: string): boolean => {
for (let i = 0; i < routesToSkip.length; ++i) {
for (let i = 0; i < routesToSkip.length; i += 1) {
if (
matchPath(currentPath, { path: routesToSkip[i], exact: true, strict: true })
) {

View File

@@ -21,6 +21,8 @@ function DeleteAlert({
payload: undefined,
});
const defaultErrorMessage = 'Something went wrong';
const onDeleteHandler = async (id: number): Promise<void> => {
try {
setDeleteAlertState((state) => ({
@@ -48,11 +50,11 @@ function DeleteAlert({
...state,
loading: false,
error: true,
errorMessage: response.error || 'Something went wrong',
errorMessage: response.error || defaultErrorMessage,
}));
notifications.error({
message: response.error || 'Something went wrong',
message: response.error || defaultErrorMessage,
});
}
} catch (error) {
@@ -60,11 +62,11 @@ function DeleteAlert({
...state,
loading: false,
error: true,
errorMessage: 'Something went wrong',
errorMessage: defaultErrorMessage,
}));
notifications.error({
message: 'Something went wrong',
message: defaultErrorMessage,
});
}
};

View File

@@ -8,7 +8,7 @@ import ROUTES from 'constants/routes';
import useInterval from 'hooks/useInterval';
import history from 'lib/history';
import React, { useCallback, useState } from 'react';
import { generatePath } from 'react-router';
import { generatePath } from 'react-router-dom';
import { Alerts } from 'types/api/alerts/getAll';
import DeleteAlert from './DeleteAlert';

View File

@@ -1,5 +1,6 @@
import { Button } from 'antd';
import React, { useCallback } from 'react';
import { ExclamationCircleOutlined } from '@ant-design/icons';
import { Modal } from 'antd';
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
@@ -7,18 +8,30 @@ import { DeleteDashboard, DeleteDashboardProps } from 'store/actions';
import AppActions from 'types/actions';
import { Data } from '../index';
import { TableLinkText } from './styles';
const { confirm } = Modal;
function DeleteButton({ deleteDashboard, id }: DeleteButtonProps): JSX.Element {
const onClickHandler = useCallback(() => {
deleteDashboard({
uuid: id,
const openConfirmationDialog = (): void => {
confirm({
title: 'Do you really want to delete this dashboard?',
icon: <ExclamationCircleOutlined style={{ color: '#e42b35' }} />,
onOk() {
deleteDashboard({
uuid: id,
});
},
okText: 'Delete',
okButtonProps: { danger: true },
centered: true,
});
}, [id, deleteDashboard]);
};
return (
<Button onClick={onClickHandler} type="link">
<TableLinkText type="danger" onClick={openConfirmationDialog}>
Delete
</Button>
</TableLinkText>
);
}
@@ -40,10 +53,18 @@ const WrapperDeleteButton = connect(null, mapDispatchToProps)(DeleteButton);
// This is to avoid the type collision
function Wrapper(props: Data): JSX.Element {
const { createdBy, description, id, key, lastUpdatedTime, name, tags } = props;
return (
<WrapperDeleteButton
{...{
...props,
createdBy,
description,
id,
key,
lastUpdatedTime,
name,
tags,
}}
/>
);

View File

@@ -1,25 +1,23 @@
import { Button } from 'antd';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import React from 'react';
import { generatePath } from 'react-router-dom';
import { Data } from '..';
import { TableLinkText } from './styles';
function Name(name: Data['name'], data: Data): JSX.Element {
const onClickHandler = () => {
const onClickHandler = (): void => {
const { id: DashboardId } = data;
history.push(
generatePath(ROUTES.DASHBOARD, {
dashboardId: data.id,
dashboardId: DashboardId,
}),
);
};
return (
<Button onClick={onClickHandler} type="link">
{name}
</Button>
);
return <TableLinkText onClick={onClickHandler}>{name}</TableLinkText>;
}
export default Name;

View File

@@ -1,12 +1,13 @@
/* eslint-disable react/destructuring-assignment */
import { Tag } from 'antd';
import React from 'react';
import { Data } from '../index';
function Tags(props: Data['tags']): JSX.Element {
function Tags(data: Data['tags']): JSX.Element {
return (
<>
{props.map((e) => (
{data.map((e) => (
<Tag key={e}>{e}</Tag>
))}
</>

View File

@@ -0,0 +1,8 @@
import { blue } from '@ant-design/colors';
import { Typography } from 'antd';
import styled from 'styled-components';
export const TableLinkText = styled(Typography.Text)`
color: ${blue.primary} !important;
cursor: pointer;
`;

View File

@@ -157,7 +157,7 @@ function ListOfAllDashboard(): JSX.Element {
<TextToolTip
{...{
text: `More details on how to create dashboards`,
url: 'https://signoz.io/docs/userguide/metrics-dashboard',
url: 'https://signoz.io/docs/userguide/dashboards',
}}
/>

View File

@@ -15,7 +15,7 @@ import MetricReducer from 'types/reducer/metrics';
import { Card, Col, GraphContainer, GraphTitle, Row } from '../styles';
import TopEndpointsTable from '../TopEndpointsTable';
import { Button } from './styles';
import { Button, TableContainerCard } from './styles';
function Application({ getWidget }: DashboardProps): JSX.Element {
const { servicename } = useParams<{ servicename?: string }>();
@@ -179,7 +179,6 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
<GraphContainer>
<FullView
name="request_per_sec"
noDataGraph
fullViewOptions={false}
onClickHandler={(event, element, chart, data): void => {
onClickhandler(event, element, chart, data, 'Request');
@@ -187,7 +186,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
widget={getWidget([
{
query: `sum(rate(signoz_latency_count{service_name="${servicename}", span_kind="SPAN_KIND_SERVER"}[2m]))`,
legend: 'Request per second',
legend: 'Requests',
},
])}
yAxisUnit="reqps"
@@ -214,7 +213,6 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
<GraphContainer>
<FullView
name="error_percentage_%"
noDataGraph
fullViewOptions={false}
onClickHandler={(ChartEvent, activeElements, chart, data): void => {
onClickhandler(ChartEvent, activeElements, chart, data, 'Error');
@@ -222,7 +220,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
widget={getWidget([
{
query: `max(sum(rate(signoz_calls_total{service_name="${servicename}", span_kind="SPAN_KIND_SERVER", status_code="STATUS_CODE_ERROR"}[1m]) OR rate(signoz_calls_total{service_name="${servicename}", span_kind="SPAN_KIND_SERVER", http_status_code=~"5.."}[1m]))*100/sum(rate(signoz_calls_total{service_name="${servicename}", span_kind="SPAN_KIND_SERVER"}[1m]))) < 1000 OR vector(0)`,
legend: 'Error Percentage (%)',
legend: 'Error Percentage',
},
])}
yAxisUnit="%"
@@ -232,9 +230,9 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
</Col>
<Col span={12}>
<Card>
<TableContainerCard>
<TopEndpointsTable data={topEndPoints} />
</Card>
</TableContainerCard>
</Col>
</Row>
</>

View File

@@ -17,7 +17,6 @@ function DBCall({ getWidget }: DBCallProps): JSX.Element {
<GraphContainer>
<FullView
name="database_call_rps"
noDataGraph
fullViewOptions={false}
widget={getWidget([
{
@@ -37,7 +36,6 @@ function DBCall({ getWidget }: DBCallProps): JSX.Element {
<GraphContainer>
<FullView
name="database_call_avg_duration"
noDataGraph
fullViewOptions={false}
widget={getWidget([
{

View File

@@ -9,6 +9,8 @@ import { Card, GraphContainer, GraphTitle, Row } from '../styles';
function External({ getWidget }: ExternalProps): JSX.Element {
const { servicename } = useParams<{ servicename?: string }>();
const legend = '{{http_url}}';
return (
<>
<Row gutter={24}>
@@ -19,11 +21,10 @@ function External({ getWidget }: ExternalProps): JSX.Element {
<FullView
name="external_call_error_percentage"
fullViewOptions={false}
noDataGraph
widget={getWidget([
{
query: `max((sum(rate(signoz_external_call_latency_count{service_name="${servicename}", status_code="STATUS_CODE_ERROR"}[1m]) OR rate(signoz_external_call_latency_count{service_name="${servicename}", http_status_code=~"5.."}[1m]) OR vector(0)) by (http_url))*100/sum(rate(signoz_external_call_latency_count{service_name="${servicename}"}[1m])) by (http_url)) < 1000 OR vector(0)`,
legend: '{{http_url}}',
legend,
},
])}
yAxisUnit="%"
@@ -38,7 +39,6 @@ function External({ getWidget }: ExternalProps): JSX.Element {
<GraphContainer>
<FullView
name="external_call_duration"
noDataGraph
fullViewOptions={false}
widget={getWidget([
{
@@ -60,12 +60,11 @@ function External({ getWidget }: ExternalProps): JSX.Element {
<GraphContainer>
<FullView
name="external_call_rps_by_address"
noDataGraph
fullViewOptions={false}
widget={getWidget([
{
query: `sum(rate(signoz_external_call_latency_count{service_name="${servicename}"}[5m])) by (http_url)`,
legend: '{{http_url}}',
legend,
},
])}
yAxisUnit="reqps"
@@ -79,13 +78,12 @@ function External({ getWidget }: ExternalProps): JSX.Element {
<GraphTitle>External Call duration(by Address)</GraphTitle>
<GraphContainer>
<FullView
noDataGraph
name="external_call_duration_by_address"
fullViewOptions={false}
widget={getWidget([
{
query: `(sum(rate(signoz_external_call_latency_sum{service_name="${servicename}"}[5m])) by (http_url))/(sum(rate(signoz_external_call_latency_count{service_name="${servicename}"}[5m])) by (http_url))`,
legend: '{{http_url}}',
legend,
},
])}
yAxisUnit="ms"

View File

@@ -1,6 +1,8 @@
import { Button as ButtonComponent } from 'antd';
import styled from 'styled-components';
import { Card } from '../styles';
export const Button = styled(ButtonComponent)`
&&& {
position: absolute;
@@ -8,3 +10,6 @@ export const Button = styled(ButtonComponent)`
display: none;
}
`;
export const TableContainerCard = styled(Card)`
overflow-x: scroll;
`;

View File

@@ -2,19 +2,20 @@ import { Button, Table, Tooltip } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { METRICS_PAGE_QUERY_PARAM } from 'constants/query';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import React from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { topEndpointListItem } from 'store/actions/MetricsActions';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import history from 'lib/history';
function TopEndpointsTable(props: TopEndpointsTableProps): JSX.Element {
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { data } = props;
const params = useParams<{ servicename: string }>();
const handleOnClick = (operation: string): void => {
@@ -80,7 +81,7 @@ function TopEndpointsTable(props: TopEndpointsTableProps): JSX.Element {
title: 'Number of Calls',
dataIndex: 'numCalls',
key: 'numCalls',
sorter: (a: topEndpointListItem, b: topEndpointListItem): number =>
sorter: (a: TopEndpointListItem, b: TopEndpointListItem): number =>
a.numCalls - b.numCalls,
},
];
@@ -91,7 +92,7 @@ function TopEndpointsTable(props: TopEndpointsTableProps): JSX.Element {
title={(): string => {
return 'Top Endpoints';
}}
dataSource={props.data}
dataSource={data}
columns={columns}
pagination={false}
rowKey="name"
@@ -99,10 +100,18 @@ function TopEndpointsTable(props: TopEndpointsTableProps): JSX.Element {
);
}
type DataProps = topEndpointListItem;
interface TopEndpointListItem {
p50: number;
p95: number;
p99: number;
numCalls: number;
name: string;
}
type DataProps = TopEndpointListItem;
interface TopEndpointsTableProps {
data: topEndpointListItem[];
data: TopEndpointListItem[];
}
export default TopEndpointsTable;

View File

@@ -22,6 +22,7 @@ function SkipOnBoardingModal({ onContinueClick }: Props): JSX.Element {
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
title="youtube_video"
/>
<div>
<Typography>No instrumentation data.</Typography>

View File

@@ -5,8 +5,9 @@ import { SKIP_ONBOARDING } from 'constants/onboarding';
import ROUTES from 'constants/routes';
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { servicesListItem } from 'store/actions/MetricsActions/metricsInterfaces';
import { Link } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { ServicesList } from 'types/api/metrics/getService';
import MetricReducer from 'types/reducer/metrics';
import SkipBoardModal from './SkipOnBoardModal';
@@ -26,10 +27,6 @@ function Metrics(): JSX.Element {
setSkipOnboarding(true);
};
const onClickHandler = (to: string): void => {
window.open(to, '_blank');
};
if (
services.length === 0 &&
loading === false &&
@@ -46,9 +43,9 @@ function Metrics(): JSX.Element {
key: 'serviceName',
// eslint-disable-next-line react/display-name
render: (text: string): JSX.Element => (
<div onClick={(): void => onClickHandler(`${ROUTES.APPLICATION}/${text}`)}>
<Link to={`${ROUTES.APPLICATION}/${text}`}>
<Name>{text}</Name>
</div>
</Link>
),
},
{
@@ -87,6 +84,6 @@ function Metrics(): JSX.Element {
);
}
type DataProps = servicesListItem;
type DataProps = ServicesList;
export default Metrics;

View File

@@ -1,6 +1,4 @@
import TimeSeries, {
TimeSeriesProps as IconProps,
} from 'assets/Dashboard/TimeSeries';
import TimeSeries from 'assets/Dashboard/TimeSeries';
import ValueIcon from 'assets/Dashboard/Value';
const Items: ItemsProps[] = [
@@ -24,4 +22,8 @@ interface ItemsProps {
display: string;
}
interface IconProps {
fillColor: React.CSSProperties['color'];
}
export default Items;

View File

@@ -2,9 +2,9 @@ import { Button, Divider } from 'antd';
import Input from 'components/Input';
import TextToolTip from 'components/TextToolTip';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
import React, { useCallback, useState } from 'react';
import { connect } from 'react-redux';
import { useLocation } from 'react-router';
import React, { useCallback, useMemo, useState } from 'react';
import { connect, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { DeleteQuery } from 'store/actions';
@@ -12,8 +12,11 @@ import {
UpdateQuery,
UpdateQueryProps,
} from 'store/actions/dashboard/updateQuery';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { DeleteQueryProps } from 'types/actions/dashboard';
import { Widgets } from 'types/api/dashboard/getAll';
import DashboardReducer from 'types/reducer/dashboards';
import {
ButtonContainer,
@@ -32,10 +35,27 @@ function Query({
const [promqlQuery, setPromqlQuery] = useState(preQuery);
const [legendFormat, setLegendFormat] = useState(preLegend);
const { search } = useLocation();
const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
const [selectedDashboards] = dashboards;
const { widgets } = selectedDashboards.data;
const query = new URLSearchParams(search);
const widgetId = query.get('widgetId') || '';
const urlQuery = useMemo(() => {
return new URLSearchParams(search);
}, [search]);
const getWidget = useCallback(() => {
const widgetId = urlQuery.get('widgetId');
return widgets?.find((e) => e.id === widgetId);
}, [widgets, urlQuery]);
const selectedWidget = getWidget() as Widgets;
const onChangeHandler = useCallback(
(setFunc: React.Dispatch<React.SetStateAction<string>>, value: string) => {
setFunc(value);
@@ -49,6 +69,7 @@ function Query({
legend: legendFormat,
query: promqlQuery,
widgetId,
yAxisUnit: selectedWidget.yAxisUnit,
});
};
@@ -93,7 +114,7 @@ function Query({
<TextToolTip
{...{
text: `More details on how to plot metrics graphs`,
url: 'https://signoz.io/docs/userguide/prometheus-metrics/',
url: 'https://signoz.io/docs/userguide/send-metrics/#related-videos',
}}
/>
</ButtonContainer>

View File

@@ -2,7 +2,7 @@ import { PlusOutlined } from '@ant-design/icons';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
import React, { useCallback, useMemo } from 'react';
import { connect, useSelector } from 'react-redux';
import { useLocation } from 'react-router';
import { useLocation } from 'react-router-dom';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { CreateQuery, CreateQueryProps } from 'store/actions';

View File

@@ -4,7 +4,7 @@ import { NewWidgetProps } from 'container/NewWidget';
import getChartData from 'lib/getChartData';
import React from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import DashboardReducer from 'types/reducer/dashboards';

View File

@@ -4,14 +4,26 @@ import React from 'react';
import { flattenedCategories } from './dataFormatCategories';
const findCategoryById = (searchValue) =>
find(flattenedCategories, (option) => option.id == searchValue);
const findCategoryByName = (searchValue) =>
find(flattenedCategories, (option) => option.name == searchValue);
const findCategoryById = (
searchValue: string,
): Record<string, string> | undefined =>
find(flattenedCategories, (option) => option.id === searchValue);
const findCategoryByName = (
searchValue: string,
): Record<string, string> | undefined =>
find(flattenedCategories, (option) => option.name === searchValue);
function YAxisUnitSelector({ defaultValue, onSelect }): JSX.Element {
function YAxisUnitSelector({
defaultValue,
onSelect,
fieldLabel,
}: {
defaultValue: string;
onSelect: React.Dispatch<React.SetStateAction<string>>;
fieldLabel: string;
}): JSX.Element {
const onSelectHandler = (selectedValue: string): void => {
onSelect(findCategoryByName(selectedValue)?.id);
onSelect(findCategoryByName(selectedValue)?.id || '');
};
const options = flattenedCategories.map((options) => ({
value: options.name,
@@ -19,16 +31,21 @@ function YAxisUnitSelector({ defaultValue, onSelect }): JSX.Element {
return (
<Col style={{ marginTop: '1rem' }}>
<div style={{ margin: '0.5rem 0' }}>
<Typography.Text>Y Axis Unit</Typography.Text>
<Typography.Text>{fieldLabel}</Typography.Text>
</div>
<AutoComplete
style={{ width: '100%' }}
options={options}
defaultValue={findCategoryById(defaultValue)?.name}
onSelect={onSelectHandler}
filterOption={(inputValue, option): boolean =>
option!.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
filterOption={(inputValue, option): boolean => {
if (option) {
return (
option.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
);
}
return false;
}}
>
<Input size="large" placeholder="Unit" allowClear />
</AutoComplete>

View File

@@ -1,23 +1,11 @@
import {
// Button,
Input,
// Slider,
// Switch,
// Typography,
} from 'antd';
import { Input } from 'antd';
import InputComponent from 'components/Input';
import TimePreference from 'components/TimePreferenceDropDown';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import GraphTypes from 'container/NewDashboard/ComponentsSlider/menuItems';
import React, { useCallback } from 'react';
import { dataTypeCategories } from './dataFormatCategories';
import {
Container,
// NullButtonContainer, TextContainer,
Title,
} from './styles';
// import {ca} from '@grafana/data'
import { Container, Title } from './styles';
import { timePreferance } from './timeItems';
import YAxisUnitSelector from './YAxisUnitSelector';
@@ -25,14 +13,8 @@ const { TextArea } = Input;
function RightContainer({
description,
// opacity,
// selectedNullZeroValue,
setDescription,
// setOpacity,
// setSelectedNullZeroValue,
// setStacked,
setTitle,
// stacked,
title,
selectedGraph,
setSelectedTime,
@@ -47,21 +29,6 @@ function RightContainer({
[],
);
// const nullValueButtons = [
// {
// check: 'zero',
// name: 'Zero',
// },
// {
// check: 'interpolate',
// name: 'Interpolate',
// },
// {
// check: 'blank',
// name: 'Blank',
// },
// ];
const selectedGraphType =
GraphTypes.find((e) => e.name === selectedGraph)?.display || '';
@@ -148,7 +115,11 @@ function RightContainer({
setSelectedTime,
}}
/>
<YAxisUnitSelector defaultValue={yAxisUnit} onSelect={setYAxisUnit} />
<YAxisUnitSelector
defaultValue={yAxisUnit}
onSelect={setYAxisUnit}
fieldLabel={selectedGraphType === 'Value' ? 'Unit' : 'Y Axis Unit'}
/>
</Container>
);
}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/naming-convention */
export const timeItems: timePreferance[] = [
{
name: 'Global Time',

View File

@@ -5,8 +5,7 @@ import history from 'lib/history';
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { connect, useSelector } from 'react-redux';
import { useLocation, useParams } from 'react-router';
import { generatePath } from 'react-router-dom';
import { generatePath, useLocation, useParams } from 'react-router-dom';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { ApplySettingsToPanel, ApplySettingsToPanelProps } from 'store/actions';
@@ -30,7 +29,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import LeftContainer from './LeftContainer';
import RightContainer from './RightContainer';
import timeItems, { timePreferance } from './RightContainer/timeItems';
import TimeItems, { timePreferance } from './RightContainer/timeItems';
import {
ButtonContainer,
Container,
@@ -91,7 +90,7 @@ function NewWidget({
const getSelectedTime = useCallback(
() =>
timeItems.find(
TimeItems.find(
(e) => e.enum === (selectedWidget?.timePreferance || 'GLOBAL_TIME'),
),
[selectedWidget],

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