Compare commits
68 Commits
v0.7.2
...
v0.7.5-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8809105a8d | ||
|
|
064c3e0449 | ||
|
|
2a348e916c | ||
|
|
5744193f50 | ||
|
|
ccf352f2db | ||
|
|
6e446dc0ab | ||
|
|
566c2becdf | ||
|
|
3b3fd2b3a9 | ||
|
|
eae53d9eff | ||
|
|
42842b6b17 | ||
|
|
95f8dfb4bc | ||
|
|
a8c5934fc5 | ||
|
|
3f2a4d6eac | ||
|
|
170609a81f | ||
|
|
76fccbbba4 | ||
|
|
147ed9f24b | ||
|
|
a69bc321a9 | ||
|
|
c9e02a8b25 | ||
|
|
24d6a1e7b2 | ||
|
|
a0efa63185 | ||
|
|
fd83cea9a0 | ||
|
|
5be1eb58b2 | ||
|
|
8367c106bc | ||
|
|
8064ae1f37 | ||
|
|
ab4d9af442 | ||
|
|
eb0d3374d5 | ||
|
|
6c4c814b3f | ||
|
|
32e8e48928 | ||
|
|
53e7037f48 | ||
|
|
a566b5dc97 | ||
|
|
4dc668fd13 | ||
|
|
d085506d3e | ||
|
|
1b28a4e6f5 | ||
|
|
20e924b116 | ||
|
|
1d28ceb3d7 | ||
|
|
1002ab553e | ||
|
|
3dc94c8da7 | ||
|
|
5a5aca2113 | ||
|
|
cb22117a0f | ||
|
|
739946fa47 | ||
|
|
7939902f03 | ||
|
|
d34e08fa3d | ||
|
|
5556d1d6fc | ||
|
|
d4d1104a53 | ||
|
|
225a345baa | ||
|
|
0efb901863 | ||
|
|
e7ba5f9f33 | ||
|
|
995232e057 | ||
|
|
cc5d47e3ee | ||
|
|
b1de6c1d7d | ||
|
|
84bfe11285 | ||
|
|
ca78947a55 | ||
|
|
ac49f84982 | ||
|
|
cc47f02ebf | ||
|
|
ac70240b72 | ||
|
|
78b1a750fa | ||
|
|
d5a6336239 | ||
|
|
01bad0f18a | ||
|
|
1b79a9bf35 | ||
|
|
3d8354fb99 | ||
|
|
696241b962 | ||
|
|
8a883f1b5e | ||
|
|
7765cee610 | ||
|
|
b958bad81f | ||
|
|
deff5d5e17 | ||
|
|
44d3f35a5f | ||
|
|
897c5d2371 | ||
|
|
f22d5f0fbd |
33
.editorconfig
Normal file
33
.editorconfig
Normal 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
|
||||
34
.github/workflows/build.yaml
vendored
34
.github/workflows/build.yaml
vendored
@@ -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: |
|
||||
|
||||
6
Makefile
6
Makefile
@@ -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/*"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
node_modules
|
||||
build
|
||||
build
|
||||
|
||||
@@ -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
4
frontend/.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
cd frontend && yarn lint-staged
|
||||
6
frontend/babel.config.js
Normal file
6
frontend/babel.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
['@babel/preset-env', { targets: { node: 'current' } }],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
};
|
||||
@@ -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'],
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}} can’t 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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
29
frontend/src/api/alerts/getTriggered.ts
Normal file
29
frontend/src/api/alerts/getTriggered.ts
Normal 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;
|
||||
51
frontend/src/api/channels/createWebhook.ts
Normal file
51
frontend/src/api/channels/createWebhook.ts
Normal 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;
|
||||
50
frontend/src/api/channels/editWebhook.ts
Normal file
50
frontend/src/api/channels/editWebhook.ts
Normal 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;
|
||||
24
frontend/src/api/disks/getDisks.ts
Normal file
24
frontend/src/api/disks/getDisks.ts
Normal 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;
|
||||
@@ -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 {
|
||||
|
||||
28
frontend/src/api/trace/getTagValue.ts
Normal file
28
frontend/src/api/trace/getTagValue.ts
Normal 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;
|
||||
25
frontend/src/api/user/getLatestVersion.ts
Normal file
25
frontend/src/api/user/getLatestVersion.ts
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
17
frontend/src/components/Graph/Plugin/EmptyGraph.ts
Normal file
17
frontend/src/components/Graph/Plugin/EmptyGraph.ts
Normal 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();
|
||||
},
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
19
frontend/src/components/Graph/hasData.ts
Normal file
19
frontend/src/components/Graph/hasData.ts
Normal 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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import { formattedValueToString, getValueFormat } from '@grafana/data';
|
||||
export const getYAxisFormattedValue = (
|
||||
value: number,
|
||||
format: string,
|
||||
decimal?: number,
|
||||
): string => {
|
||||
try {
|
||||
return formattedValueToString(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -28,4 +28,9 @@ interface ModalProps {
|
||||
children: ReactElement;
|
||||
}
|
||||
|
||||
CustomModal.defaultProps = {
|
||||
closable: undefined,
|
||||
footer: undefined,
|
||||
};
|
||||
|
||||
export default CustomModal;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -17,5 +17,10 @@ interface SpinnerProps {
|
||||
tip?: SpinProps['tip'];
|
||||
height?: React.CSSProperties['height'];
|
||||
}
|
||||
Spinner.defaultProps = {
|
||||
size: undefined,
|
||||
tip: undefined,
|
||||
height: undefined,
|
||||
};
|
||||
|
||||
export default Spinner;
|
||||
|
||||
@@ -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>`
|
||||
|
||||
@@ -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)}
|
||||
`;
|
||||
|
||||
@@ -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} `}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export enum LOCAL_STORAGE {
|
||||
export enum LOCALSTORAGE {
|
||||
METRICS_TIME_IN_DURATION = 'metricsTimeDurations',
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export enum METRICS_PAGE_QUERY_PARAM {
|
||||
interval = 'interval',
|
||||
startTime = 'startTime',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
42
frontend/src/container/GeneralSettings/utils.ts
Normal file
42
frontend/src/container/GeneralSettings/utils.ts
Normal 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 };
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('/')}`;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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 })
|
||||
) {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
@@ -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',
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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([
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
export const timeItems: timePreferance[] = [
|
||||
{
|
||||
name: 'Global Time',
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user