Merge pull request #1336 from SigNoz/release/v0.9.0

Release/v0.9.0
This commit is contained in:
Ankit Nayan 2022-06-29 15:26:36 +05:30 committed by GitHub
commit 90566360ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
149 changed files with 9630 additions and 2194 deletions

3
.github/CODEOWNERS vendored
View File

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

View File

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

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

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -2,12 +2,14 @@ version: "3.9"
services:
clickhouse:
image: yandex/clickhouse-server:21.12.3.32
image: clickhouse/clickhouse-server:22.4.5-alpine
# ports:
# - "9000:9000"
# - "8123:8123"
volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
- ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
# - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
- ./data/clickhouse/:/var/lib/clickhouse/
deploy:
restart_policy:
@ -37,7 +39,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.8.2
image: signoz/query-service:0.9.0
command: ["-config=/root/config/prometheus.yml"]
# ports:
# - "6060:6060" # pprof port
@ -65,7 +67,7 @@ services:
- clickhouse
frontend:
image: signoz/frontend:0.8.2
image: signoz/frontend:0.9.0
deploy:
restart_policy:
condition: on-failure
@ -78,7 +80,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/otelcontribcol:0.45.1-0.3
image: signoz/otelcontribcol:0.45.1-1.0
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
@ -104,7 +106,7 @@ services:
- clickhouse
otel-collector-metrics:
image: signoz/otelcontribcol:0.45.1-0.3
image: signoz/otelcontribcol:0.45.1-1.0
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -12,7 +12,7 @@ receivers:
grpc:
thrift_http:
hostmetrics:
collection_interval: 30s
collection_interval: 60s
scrapers:
cpu:
load:
@ -22,7 +22,8 @@ receivers:
network:
processors:
batch:
send_batch_size: 1000
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
signozspanmetrics/prometheus:
metrics_exporter: prometheus

View File

@ -9,12 +9,13 @@ receivers:
config:
scrape_configs:
- job_name: "otel-collector"
scrape_interval: 30s
scrape_interval: 60s
static_configs:
- targets: ["otel-collector:8889"]
processors:
batch:
send_batch_size: 1000
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
# memory_limiter:
# # 80% of maximum memory up to 2G

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -2,12 +2,14 @@ version: "2.4"
services:
clickhouse:
image: yandex/clickhouse-server:21.12.3.32
image: clickhouse/clickhouse-server:22.4.5-alpine
# ports:
# - "9000:9000"
# - "8123:8123"
volumes:
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
- ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
# - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
- ./data/clickhouse/:/var/lib/clickhouse/
restart: on-failure
logging:
@ -36,7 +38,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:0.8.2
image: signoz/query-service:0.9.0
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
# ports:
@ -63,7 +65,7 @@ services:
condition: service_healthy
frontend:
image: signoz/frontend:0.8.2
image: signoz/frontend:0.9.0
container_name: frontend
restart: on-failure
depends_on:
@ -75,7 +77,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector:
image: signoz/otelcontribcol:0.45.1-0.3
image: signoz/otelcontribcol:0.45.1-1.0
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
@ -96,7 +98,7 @@ services:
condition: service_healthy
otel-collector-metrics:
image: signoz/otelcontribcol:0.45.1-0.3
image: signoz/otelcontribcol:0.45.1-1.0
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
volumes:
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml

View File

@ -12,7 +12,7 @@ receivers:
grpc:
thrift_http:
hostmetrics:
collection_interval: 30s
collection_interval: 60s
scrapers:
cpu:
load:
@ -22,7 +22,8 @@ receivers:
network:
processors:
batch:
send_batch_size: 1000
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
signozspanmetrics/prometheus:
metrics_exporter: prometheus

View File

@ -9,12 +9,13 @@ receivers:
config:
scrape_configs:
- job_name: "otel-collector"
scrape_interval: 30s
scrape_interval: 60s
static_configs:
- targets: ["otel-collector:8889"]
processors:
batch:
send_batch_size: 1000
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
# memory_limiter:
# # 80% of maximum memory up to 2G

View File

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

View File

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

View File

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

View File

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

View File

@ -159,6 +159,7 @@
"husky": "^7.0.4",
"is-ci": "^3.0.1",
"jest-playwright-preset": "^1.7.0",
"jest-styled-components": "^7.0.8",
"less-plugin-npm-import": "^2.1.0",
"lint-staged": "^12.3.7",
"portfinder-sync": "^0.0.2",

View File

@ -11,7 +11,7 @@ const config: PlaywrightTestConfig = {
testDir: './tests',
use: {
trace: 'retain-on-failure',
baseURL: process.env.FRONTEND_API_ENDPOINT,
baseURL: process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3301',
},
updateSnapshots: 'all',
fullyParallel: false,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -182,11 +182,10 @@ function Graph({
};
const chartHasData = hasData(data);
const chartPlugins = [];
if (chartHasData) {
if (!chartHasData) chartPlugins.push(emptyGraph);
chartPlugins.push(legend(name, data.datasets.length > 3));
} else {
chartPlugins.push(emptyGraph);
}
lineChartRef.current = new Chart(chartRef.current, {
type,
data,

View File

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

View File

@ -7,13 +7,17 @@ export const getYAxisFormattedValue = (
let decimalPrecision: number | undefined;
const parsedValue = getValueFormat(format)(
parseFloat(value),
undefined,
undefined,
12,
12,
undefined,
);
try {
const decimalSplitted = parsedValue.text.split('.');
if (decimalSplitted.length === 1) {
if (
decimalSplitted.length === 1 ||
parseFloat(parsedValue.text) === parseInt(parsedValue.text, 10)
) {
decimalPrecision = 0;
} else {
const decimalDigits = decimalSplitted[1].split('');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,12 @@
import { Typography } from 'antd';
import getQueryResult from 'api/widgets/getQuery';
import { AxiosError } from 'axios';
import { ChartData } from 'chart.js';
import Spinner from 'components/Spinner';
import GridGraphComponent from 'container/GridGraphComponent';
import getChartData from 'lib/getChartData';
import GetMaxMinTime from 'lib/getMaxMinTime';
import GetStartAndEndTime from 'lib/getStartAndEndTime';
import isEmpty from 'lodash-es/isEmpty';
import React, { memo, useCallback, useState } from 'react';
import React, { memo, useCallback, useEffect, useState } from 'react';
import { Layout } from 'react-grid-layout';
import { useQueries } from 'react-query';
import { connect, useSelector } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
@ -16,15 +14,17 @@ import {
DeleteWidget,
DeleteWidgetProps,
} from 'store/actions/dashboard/deleteWidget';
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { GlobalTime } from 'types/actions/globalTime';
import { Widgets } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { LayoutProps } from '..';
import EmptyWidget from '../EmptyWidget';
import WidgetHeader from '../WidgetHeader';
import FullView from './FullView';
import FullView from './FullView/index.metricsBuilder';
import { ErrorContainer, FullViewContainer, Modal } from './styles';
function GridCardGraph({
@ -35,60 +35,118 @@ function GridCardGraph({
layout = [],
setLayout,
}: GridCardGraphProps): JSX.Element {
const [state, setState] = useState<GridCardGraphState>({
loading: true,
errorMessage: '',
error: false,
payload: undefined,
});
const [hovered, setHovered] = useState(false);
const [modal, setModal] = useState(false);
const [deleteModal, setDeleteModal] = useState(false);
const { minTime, maxTime } = useSelector<AppState, GlobalTime>(
(state) => state.globalTime,
);
const [deleteModal, setDeleteModal] = useState(false);
const { selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const getMaxMinTime = GetMaxMinTime({
graphType: widget?.panelTypes,
maxTime,
minTime,
// const getMaxMinTime = GetMaxMinTime({
// graphType: widget?.panelTypes,
// maxTime,
// minTime,
// });
// const { start, end } = GetStartAndEndTime({
// type: widget?.timePreferance,
// maxTime: getMaxMinTime.maxTime,
// minTime: getMaxMinTime.minTime,
// });
// const queryLength = widget?.query?.filter((e) => e.query.length !== 0) || [];
// const response = useQueries(
// queryLength?.map((query) => {
// return {
// // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
// queryFn: () => {
// return getQueryResult({
// end,
// query: query?.query,
// start,
// step: '60',
// });
// },
// queryHash: `${query?.query}-${query?.legend}-${start}-${end}`,
// retryOnMount: false,
// };
// }),
// );
// const isError =
// response.find((e) => e?.data?.statusCode !== 200) !== undefined ||
// response.some((e) => e.isError === true);
// const isLoading = response.some((e) => e.isLoading === true);
// const errorMessage = response.find((e) => e.data?.error !== null)?.data?.error;
// const data = response.map((responseOfQuery) =>
// responseOfQuery?.data?.payload?.result.map((e, index) => ({
// query: queryLength[index]?.query,
// queryData: e,
// legend: queryLength[index]?.legend,
// })),
// );
useEffect(() => {
(async (): Promise<void> => {
try {
const response = await GetMetricQueryRange({
selectedTime: widget.timePreferance,
graphType: widget.panelTypes,
query: widget.query,
globalSelectedInterval,
});
const { start, end } = GetStartAndEndTime({
type: widget?.timePreferance,
maxTime: getMaxMinTime.maxTime,
minTime: getMaxMinTime.minTime,
});
const isError = response.error;
const queryLength = widget?.query?.filter((e) => e.query.length !== 0) || [];
const response = useQueries(
queryLength?.map((query) => {
return {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
queryFn: () => {
return getQueryResult({
end,
query: query?.query,
start,
step: '60',
});
if (isError != null) {
setState((state) => ({
...state,
error: true,
errorMessage: isError || 'Something went wrong',
loading: false,
}));
} else {
const chartDataSet = getChartData({
queryData: [
{
queryData: response.payload?.data?.result
? response.payload?.data?.result
: [],
},
queryHash: `${query?.query}-${query?.legend}-${start}-${end}`,
retryOnMount: false,
};
}),
);
],
});
const isError =
response.find((e) => e?.data?.statusCode !== 200) !== undefined ||
response.some((e) => e.isError === true);
const isLoading = response.some((e) => e.isLoading === true);
const errorMessage = response.find((e) => e.data?.error !== null)?.data?.error;
const data = response.map((responseOfQuery) =>
responseOfQuery?.data?.payload?.result.map((e, index) => ({
query: queryLength[index]?.query,
queryData: e,
legend: queryLength[index]?.legend,
})),
);
setState((state) => ({
...state,
loading: false,
payload: chartDataSet,
}));
}
} catch (error) {
setState((state) => ({
...state,
error: true,
errorMessage: (error as AxiosError).toString(),
loading: false,
}));
}
})();
}, [widget, maxTime, minTime, globalSelectedInterval]);
const onToggleModal = useCallback(
(func: React.Dispatch<React.SetStateAction<boolean>>) => {
@ -144,14 +202,7 @@ function GridCardGraph({
const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget);
if (isLoading) {
return <Spinner height="20vh" tip="Loading..." />;
}
if (
(isError || data === undefined || data[0] === undefined) &&
!isEmptyLayout
) {
if (state.error && !isEmptyLayout) {
return (
<>
{getModals()}
@ -163,18 +214,17 @@ function GridCardGraph({
onDelete={(): void => onToggleModal(setDeleteModal)}
/>
<ErrorContainer>{errorMessage}</ErrorContainer>
<ErrorContainer>{state.errorMessage}</ErrorContainer>
</>
);
}
const chartData = getChartData({
queryData: data.map((e) => ({
query: e?.map((e) => e.query).join(' ') || '',
queryData: e?.map((e) => e.queryData) || [],
legend: e?.map((e) => e.legend).join('') || '',
})),
});
if (
(state.loading === true || state.payload === undefined) &&
!isEmptyLayout
) {
return <Spinner height="20vh" tip="Loading..." />;
}
return (
<span
@ -203,11 +253,11 @@ function GridCardGraph({
{!isEmptyLayout && getModals()}
{!isEmpty(widget) && (
{!isEmpty(widget) && !!state.payload && (
<GridGraphComponent
{...{
GRAPH_TYPES: widget.panelTypes,
data: chartData,
data: state.payload,
isStacked: widget.isStacked,
opacity: widget.opacity,
title: ' ', // empty title to accommodate absolutely positioned widget header
@ -222,6 +272,13 @@ function GridCardGraph({
);
}
interface GridCardGraphState {
loading: boolean;
error: boolean;
errorMessage: string;
payload: ChartData | undefined;
}
interface DispatchProps {
deleteWidget: ({
widgetId,

View File

@ -113,10 +113,8 @@ function GridGraph(props: Props): JSX.Element {
errorMessage: '',
loading: true,
}));
// Save layout only when users has the has the permission to do so.
if (saveLayoutPermission) {
const response = await updateDashboardApi({
const updatedDashboard: Dashboard = {
...selectedDashboard,
data: {
title: data.title,
description: data.description,
@ -126,7 +124,10 @@ function GridGraph(props: Props): JSX.Element {
layout,
},
uuid: selectedDashboard.uuid,
});
};
// Save layout only when users has the has the permission to do so.
if (saveLayoutPermission) {
const response = await updateDashboardApi(updatedDashboard);
if (response.statusCode === 200) {
setSaveLayoutState((state) => ({
...state,
@ -134,6 +135,10 @@ function GridGraph(props: Props): JSX.Element {
errorMessage: '',
loading: false,
}));
dispatch({
type: UPDATE_DASHBOARD,
payload: updatedDashboard,
});
} else {
setSaveLayoutState((state) => ({
...state,
@ -153,8 +158,9 @@ function GridGraph(props: Props): JSX.Element {
data.tags,
data.title,
data.widgets,
dispatch,
saveLayoutPermission,
selectedDashboard.uuid,
selectedDashboard,
],
);
@ -218,7 +224,7 @@ function GridGraph(props: Props): JSX.Element {
const onLayoutChangeHandler = async (layout: Layout[]): Promise<void> => {
setLayoutFunction(layout);
await onLayoutSaveHandler(layout);
// await onLayoutSaveHandler(layout);
};
const onAddPanelHandler = useCallback(() => {

View File

@ -1,9 +1,16 @@
import { notification } from 'antd';
import updateDashboardApi from 'api/dashboard/update';
import {
ClickHouseQueryTemplate,
PromQLQueryTemplate,
QueryBuilderQueryTemplate,
} from 'constants/dashboard';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import GetQueryName from 'lib/query/GetQueryName';
import { Layout } from 'react-grid-layout';
import store from 'store';
import { Dashboard } from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard';
export const UpdateDashboard = async ({
data,
@ -29,14 +36,32 @@ export const UpdateDashboard = async ({
nullZeroValues: '',
opacity: '',
panelTypes: graphType,
query: [
query: {
queryType: EQueryType.QUERY_BUILDER,
promQL: [
{
query: '',
legend: '',
name: GetQueryName([]) || '',
...PromQLQueryTemplate,
},
],
clickHouse: [
{
name: GetQueryName([]) || '',
...ClickHouseQueryTemplate,
},
],
metricsBuilder: {
formulas: [],
queryBuilder: [
{
name: GetQueryName([]) || '',
...QueryBuilderQueryTemplate,
},
],
},
},
queryData: {
data: [],
data: { queryData: [] },
error: false,
errorMessage: '',
loading: false,

View File

@ -11,7 +11,6 @@ import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
import { generatePath } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { Alerts } from 'types/api/alerts/getAll';
@ -51,11 +50,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const [notifications, Element] = notification.useNotification();
const onEditHandler = (id: string): void => {
history.push(
generatePath(ROUTES.EDIT_ALERTS, {
ruleId: id,
}),
);
history.push(`${ROUTES.EDIT_ALERTS}?ruleId=${id}`);
};
const columns: ColumnsType<Alerts> = [

View File

@ -73,10 +73,7 @@ function ImportJSON({
...e,
queryData: {
...e.queryData,
data: e.queryData.data.map((queryData) => ({
...queryData,
queryData: [],
})),
data: e.queryData.data,
error: false,
errorMessage: '',
loading: false,

View File

@ -0,0 +1,58 @@
import { Dashboard } from 'types/api/dashboard/getAll';
import { v4 as uuid } from 'uuid';
import { TOperator } from '../types';
import { executeSearchQueries } from '../utils';
describe('executeSearchQueries', () => {
const firstDashboard: Dashboard = {
id: 11111,
uuid: uuid(),
created_at: '',
updated_at: '',
data: {
title: 'first dashboard',
},
};
const secondDashboard: Dashboard = {
id: 22222,
uuid: uuid(),
created_at: '',
updated_at: '',
data: {
title: 'second dashboard',
},
};
const thirdDashboard: Dashboard = {
id: 333333,
uuid: uuid(),
created_at: '',
updated_at: '',
data: {
title: 'third dashboard (with special characters +?\\)',
},
};
const dashboards = [firstDashboard, secondDashboard, thirdDashboard];
it('should filter dashboards based on title', () => {
const query = {
category: 'title',
id: 'someid',
operator: '=' as TOperator,
value: 'first dashboard',
};
expect(executeSearchQueries([query], dashboards)).toEqual([firstDashboard]);
});
it('should filter dashboards with special characters', () => {
const query = {
category: 'title',
id: 'someid',
operator: '=' as TOperator,
value: 'third dashboard (with special characters +?\\)',
};
expect(executeSearchQueries([query], dashboards)).toEqual([thirdDashboard]);
});
});

View File

@ -42,6 +42,8 @@ export const executeSearchQueries = (
if (!searchData.length || !queries.length) {
return searchData;
}
const escapeRegExp = (regExp: string): string =>
regExp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
queries.forEach((query: IQueryStructure) => {
const { operator } = query;
@ -61,7 +63,7 @@ export const executeSearchQueries = (
for (const searchSpaceItem of searchSpace) {
if (searchSpaceItem)
for (const queryValue of value) {
if (searchSpaceItem.match(queryValue)) {
if (searchSpaceItem.match(escapeRegExp(queryValue))) {
return resolveOperator(true, operator);
}
}

View File

@ -21,7 +21,10 @@ export const GetTagKeys = async (): Promise<IOption[]> => {
// resolve(TagKeysCache);
// });
// }
const { payload } = await getResourceAttributesTagKeys();
const { payload } = await getResourceAttributesTagKeys({
metricName: 'signoz_calls_total',
match: 'resource_',
});
if (!payload || !payload?.data) {
return [];
}
@ -32,12 +35,15 @@ export const GetTagKeys = async (): Promise<IOption[]> => {
};
export const GetTagValues = async (tagKey: string): Promise<IOption[]> => {
const { payload } = await getResourceAttributesTagValues(tagKey);
const { payload } = await getResourceAttributesTagValues({
tagKey,
metricName: 'signoz_calls_total',
});
if (!payload || !payload?.data) {
return [];
}
return payload.data.filter(Boolean).map((tagValue: string) => ({
return payload.data.map((tagValue: string) => ({
label: tagValue,
value: tagValue,
}));

View File

@ -4,7 +4,7 @@ import React from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { PromQLWidgets } from 'types/api/dashboard/getAll';
import MetricReducer from 'types/reducer/metrics';
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
@ -58,7 +58,7 @@ function DBCall({ getWidget }: DBCallProps): JSX.Element {
}
interface DBCallProps {
getWidget: (query: Widgets['query']) => Widgets;
getWidget: (query: PromQLWidgets['query']) => PromQLWidgets;
}
export default DBCall;

View File

@ -4,7 +4,7 @@ import React from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { PromQLWidgets } from 'types/api/dashboard/getAll';
import MetricReducer from 'types/reducer/metrics';
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
@ -29,7 +29,7 @@ function External({ getWidget }: ExternalProps): JSX.Element {
widget={getWidget([
{
query: `max((sum(rate(signoz_external_call_latency_count{service_name="${servicename}", status_code="STATUS_CODE_ERROR"${resourceAttributePromQLQuery}}[1m]) OR rate(signoz_external_call_latency_count{service_name="${servicename}", http_status_code=~"5.."${resourceAttributePromQLQuery}}[1m]) OR vector(0)) by (http_url))*100/sum(rate(signoz_external_call_latency_count{service_name="${servicename}"${resourceAttributePromQLQuery}}[1m])) by (http_url)) < 1000 OR vector(0)`,
legend,
legend: 'External Call Error Percentage',
},
])}
yAxisUnit="%"
@ -102,7 +102,7 @@ function External({ getWidget }: ExternalProps): JSX.Element {
}
interface ExternalProps {
getWidget: (query: Widgets['query']) => Widgets;
getWidget: (query: PromQLWidgets['query']) => PromQLWidgets;
}
export default External;

View File

@ -11,7 +11,7 @@ import React, { useRef } from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { PromQLWidgets } from 'types/api/dashboard/getAll';
import MetricReducer from 'types/reducer/metrics';
import { Card, Col, GraphContainer, GraphTitle, Row } from '../styles';
@ -248,7 +248,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
}
interface DashboardProps {
getWidget: (query: Widgets['query']) => Widgets;
getWidget: (query: PromQLWidgets['query']) => PromQLWidgets;
}
export default Application;

View File

@ -3,14 +3,14 @@ import ROUTES from 'constants/routes';
import React from 'react';
import { generatePath, useParams } from 'react-router-dom';
import { useLocation } from 'react-use';
import { Widgets } from 'types/api/dashboard/getAll';
import { PromQLWidgets } from 'types/api/dashboard/getAll';
import ResourceAttributesFilter from './ResourceAttributesFilter';
import DBCall from './Tabs/DBCall';
import External from './Tabs/External';
import Overview from './Tabs/Overview';
const getWidget = (query: Widgets['query']): Widgets => {
const getWidget = (query: PromQLWidgets['query']): PromQLWidgets => {
return {
description: '',
id: '',
@ -20,7 +20,7 @@ const getWidget = (query: Widgets['query']): Widgets => {
panelTypes: 'TIME_SERIES',
query,
queryData: {
data: [],
data: { queryData: [] },
error: false,
errorMessage: '',
loading: false,

View File

@ -1,151 +0,0 @@
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, 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';
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,
Container,
InputContainer,
QueryWrapper,
} from './styles';
function Query({
currentIndex,
preLegend,
preQuery,
updateQuery,
deleteQuery,
}: QueryProps): JSX.Element {
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);
},
[],
);
const onBlurHandler = (): void => {
updateQuery({
currentIndex,
legend: legendFormat,
query: promqlQuery,
widgetId,
yAxisUnit: selectedWidget.yAxisUnit,
});
};
const onDeleteQueryHandler = (): void => {
deleteQuery({
widgetId,
currentIndex,
});
};
return (
<>
<Container>
<QueryWrapper>
<InputContainer>
<Input
onChangeHandler={(event): void =>
onChangeHandler(setPromqlQuery, event.target.value)
}
size="middle"
value={promqlQuery}
addonBefore="PromQL Query"
onBlur={(): void => onBlurHandler()}
/>
</InputContainer>
<InputContainer>
<Input
onChangeHandler={(event): void =>
onChangeHandler(setLegendFormat, event.target.value)
}
size="middle"
value={legendFormat}
addonBefore="Legend Format"
onBlur={(): void => onBlurHandler()}
/>
</InputContainer>
</QueryWrapper>
<ButtonContainer>
<Button onClick={onDeleteQueryHandler}>Delete</Button>
<TextToolTip
{...{
text: `More details on how to plot metrics graphs`,
url: 'https://signoz.io/docs/userguide/send-metrics/#related-videos',
}}
/>
</ButtonContainer>
</Container>
<Divider />
</>
);
}
interface DispatchProps {
updateQuery: (
props: UpdateQueryProps,
) => (dispatch: Dispatch<AppActions>) => void;
deleteQuery: (
props: DeleteQueryProps,
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
updateQuery: bindActionCreators(UpdateQuery, dispatch),
deleteQuery: bindActionCreators(DeleteQuery, dispatch),
});
interface QueryProps extends DispatchProps {
selectedTime: timePreferance;
currentIndex: number;
preQuery: string;
preLegend: string;
}
export default connect(null, mapDispatchToProps)(Query);

View File

@ -0,0 +1,20 @@
import { EAggregateOperator } from 'types/common/dashboard';
export const AggregateFunctions = Object.keys(EAggregateOperator)
.filter((key) => Number.isNaN(parseInt(key, 10)))
.map((key) => {
return {
label: key,
value: EAggregateOperator[key as keyof typeof EAggregateOperator],
};
});
export const TagKeyOperator = [
{ label: 'In', value: 'IN' },
{ label: 'Not In', value: 'NIN' },
{ label: 'Like', value: 'LIKE' },
{ label: 'Not Like', value: 'NLIKE' },
// { label: 'Equal', value: 'EQ' },
// { label: 'Not Equal', value: 'NEQ' },
// { label: 'REGEX', value: 'REGEX' },
];

View File

@ -0,0 +1,54 @@
import {
DeleteOutlined,
DownOutlined,
EyeFilled,
EyeInvisibleFilled,
RightOutlined,
} from '@ant-design/icons';
import { Button, Row } from 'antd';
import React, { useState } from 'react';
import { QueryWrapper } from '../styles';
interface IQueryHeaderProps {
disabled: boolean;
onDisable: VoidFunction;
name: string;
onDelete: VoidFunction;
children: React.ReactNode;
}
function QueryHeader({
disabled,
onDisable,
name,
onDelete,
children,
}: IQueryHeaderProps): JSX.Element {
const [collapse, setCollapse] = useState(false);
return (
<QueryWrapper>
<Row style={{ justifyContent: 'space-between' }}>
<Row>
<Button
type="ghost"
icon={disabled ? <EyeInvisibleFilled /> : <EyeFilled />}
onClick={onDisable}
>
{name}
</Button>
<Button
type="ghost"
icon={collapse ? <RightOutlined /> : <DownOutlined />}
onClick={(): void => setCollapse(!collapse)}
/>
</Row>
<Button type="ghost" danger icon={<DeleteOutlined />} onClick={onDelete} />
</Row>
{!collapse && children}
</QueryWrapper>
);
}
export default QueryHeader;

View File

@ -0,0 +1,77 @@
import { PlusOutlined } from '@ant-design/icons';
import { ClickHouseQueryTemplate } from 'constants/dashboard';
import GetQueryName from 'lib/query/GetQueryName';
import React from 'react';
import { Query } from 'types/api/dashboard/getAll';
import { WIDGET_CLICKHOUSE_QUERY_KEY_NAME } from '../../constants';
import { QueryButton } from '../../styles';
import { IHandleUpdatedQuery } from '../../types';
import ClickHouseQueryBuilder from './query';
import { IClickHouseQueryHandleChange } from './types';
interface IClickHouseQueryContainerProps {
queryData: Query;
updateQueryData: (args: IHandleUpdatedQuery) => void;
clickHouseQueries: Query['clickHouse'];
}
function ClickHouseQueryContainer({
queryData,
updateQueryData,
clickHouseQueries,
}: IClickHouseQueryContainerProps): JSX.Element | null {
const handleClickHouseQueryChange = ({
queryIndex,
rawQuery,
legend,
toggleDisable,
toggleDelete,
}: IClickHouseQueryHandleChange): void => {
const allQueries = queryData[WIDGET_CLICKHOUSE_QUERY_KEY_NAME];
const currentIndexQuery = allQueries[queryIndex];
if (rawQuery !== undefined) {
currentIndexQuery.rawQuery = rawQuery;
}
if (legend !== undefined) {
currentIndexQuery.legend = legend;
}
if (toggleDisable) {
currentIndexQuery.disabled = !currentIndexQuery.disabled;
}
if (toggleDelete) {
allQueries.splice(queryIndex, 1);
}
updateQueryData({ updatedQuery: { ...queryData } });
};
const addQueryHandler = (): void => {
queryData[WIDGET_CLICKHOUSE_QUERY_KEY_NAME].push({
name: GetQueryName(queryData[WIDGET_CLICKHOUSE_QUERY_KEY_NAME]) || '',
...ClickHouseQueryTemplate,
});
updateQueryData({ updatedQuery: { ...queryData } });
};
if (!clickHouseQueries) {
return null;
}
return (
<>
{clickHouseQueries.map((q, idx) => (
<ClickHouseQueryBuilder
key={q.name}
queryIndex={idx}
queryData={q}
handleQueryChange={handleClickHouseQueryChange}
/>
))}
<QueryButton onClick={addQueryHandler} icon={<PlusOutlined />}>
Query
</QueryButton>
</>
);
}
export default ClickHouseQueryContainer;

View File

@ -0,0 +1,60 @@
import { Input } from 'antd';
import MonacoEditor from 'components/Editor';
import React from 'react';
import { IClickHouseQuery } from 'types/api/dashboard/getAll';
import QueryHeader from '../QueryHeader';
import { IClickHouseQueryHandleChange } from './types';
interface IClickHouseQueryBuilderProps {
queryData: IClickHouseQuery;
queryIndex: number;
handleQueryChange: (args: IClickHouseQueryHandleChange) => void;
}
function ClickHouseQueryBuilder({
queryData,
queryIndex,
handleQueryChange,
}: IClickHouseQueryBuilderProps): JSX.Element | null {
if (queryData === undefined) {
return null;
}
return (
<QueryHeader
name={queryData.name}
disabled={queryData.disabled}
onDisable={(): void =>
handleQueryChange({ queryIndex, toggleDisable: true })
}
onDelete={(): void => {
handleQueryChange({ queryIndex, toggleDelete: true });
}}
>
<MonacoEditor
language="sql"
height="200px"
onChange={(value): void =>
handleQueryChange({ queryIndex, rawQuery: value })
}
value={queryData.rawQuery}
options={{
scrollbar: {
alwaysConsumeMouseWheel: false,
},
}}
/>
<Input
onChange={(event): void =>
handleQueryChange({ queryIndex, legend: event.target.value })
}
size="middle"
defaultValue={queryData.legend}
addonBefore="Legend Format"
/>
</QueryHeader>
);
}
export default ClickHouseQueryBuilder;

View File

@ -0,0 +1,9 @@
import { IClickHouseQuery } from 'types/api/dashboard/getAll';
export interface IClickHouseQueryHandleChange {
queryIndex: number;
rawQuery?: IClickHouseQuery['rawQuery'];
legend?: IClickHouseQuery['legend'];
toggleDisable?: IClickHouseQuery['disabled'];
toggleDelete?: boolean;
}

View File

@ -0,0 +1,74 @@
import { PlusOutlined } from '@ant-design/icons';
import { PromQLQueryTemplate } from 'constants/dashboard';
import GetQueryName from 'lib/query/GetQueryName';
import React from 'react';
import { IPromQLQuery, Query } from 'types/api/dashboard/getAll';
import { WIDGET_PROMQL_QUERY_KEY_NAME } from '../../constants';
import { QueryButton } from '../../styles';
import { IHandleUpdatedQuery } from '../../types';
import PromQLQueryBuilder from './query';
import { IPromQLQueryHandleChange } from './types';
interface IPromQLQueryContainerProps {
queryData: Query;
updateQueryData: (args: IHandleUpdatedQuery) => void;
promQLQueries: IPromQLQuery[];
}
function PromQLQueryContainer({
queryData,
updateQueryData,
promQLQueries,
}: IPromQLQueryContainerProps): JSX.Element | null {
const handlePromQLQueryChange = ({
queryIndex,
query,
legend,
toggleDisable,
toggleDelete,
}: IPromQLQueryHandleChange): void => {
const allQueries = queryData[WIDGET_PROMQL_QUERY_KEY_NAME];
const currentIndexQuery = allQueries[queryIndex];
if (query) currentIndexQuery.query = query;
if (legend) currentIndexQuery.legend = legend;
if (toggleDisable) {
currentIndexQuery.disabled = !currentIndexQuery.disabled;
}
if (toggleDelete) {
allQueries.splice(queryIndex, 1);
}
updateQueryData({ updatedQuery: { ...queryData } });
};
const addQueryHandler = (): void => {
queryData[WIDGET_PROMQL_QUERY_KEY_NAME].push({
name: GetQueryName(queryData[WIDGET_PROMQL_QUERY_KEY_NAME]) || '',
...PromQLQueryTemplate,
});
updateQueryData({ updatedQuery: { ...queryData } });
};
if (!promQLQueries) {
return null;
}
return (
<>
{promQLQueries.map(
(q: IPromQLQuery, idx: number): JSX.Element => (
<PromQLQueryBuilder
key={q.name}
queryIndex={idx}
queryData={q}
handleQueryChange={handlePromQLQueryChange}
/>
),
)}
<QueryButton onClick={addQueryHandler} icon={<PlusOutlined />}>
Query
</QueryButton>
</>
);
}
export default PromQLQueryContainer;

View File

@ -0,0 +1,53 @@
import { Input } from 'antd';
import React from 'react';
import { IPromQLQuery } from 'types/api/dashboard/getAll';
import QueryHeader from '../QueryHeader';
import { IPromQLQueryHandleChange } from './types';
interface IPromQLQueryBuilderProps {
queryData: IPromQLQuery;
queryIndex: number;
handleQueryChange: (args: IPromQLQueryHandleChange) => void;
}
function PromQLQueryBuilder({
queryData,
queryIndex,
handleQueryChange,
}: IPromQLQueryBuilderProps): JSX.Element {
return (
<QueryHeader
name={queryData.name}
disabled={queryData.disabled}
onDisable={(): void =>
handleQueryChange({ queryIndex, toggleDisable: true })
}
onDelete={(): void => {
handleQueryChange({ queryIndex, toggleDelete: true });
}}
>
<Input
onChange={(event): void =>
handleQueryChange({ queryIndex, query: event.target.value })
}
size="middle"
defaultValue={queryData.query}
addonBefore="PromQL Query"
style={{ marginBottom: '0.5rem' }}
/>
<Input
onChange={(event): void =>
handleQueryChange({ queryIndex, legend: event.target.value })
}
size="middle"
defaultValue={queryData.legend}
addonBefore="Legend Format"
style={{ marginBottom: '0.5rem' }}
/>
</QueryHeader>
);
}
export default PromQLQueryBuilder;

View File

@ -0,0 +1,9 @@
import { IPromQLQuery } from 'types/api/dashboard/getAll';
export interface IPromQLQueryHandleChange {
queryIndex: number;
query?: IPromQLQuery['query'];
legend?: IPromQLQuery['legend'];
toggleDisable?: IPromQLQuery['disabled'];
toggleDelete?: boolean;
}

View File

@ -0,0 +1,61 @@
import { createMachine } from 'xstate';
export const ResourceAttributesFilterMachine =
/** @xstate-layout N4IgpgJg5mDOIC5QBECGsAWAjA9qgThAAQDKYBAxhkQIIB2xAYgJYA2ALmPgHQAqqUANJgAngGIAcgFEAGr0SgADjljN2zHHQUgAHogAcAFgAM3AOz6ATAEYAzJdsA2Y4cOWAnABoQIxAFpDR2tuQ319AFYTcKdbFycAX3jvNExcAmIySmp6JjZOHn4hUTFNACFWAFd8bWVVdU1tPQQzY1MXY2tDdzNHM3dHd0NvXwR7biMTa313S0i+63DE5PRsPEJScnwqWgYiFg4uPgFhcQAlKRIpeSQQWrUNLRumx3Czbg8TR0sbS31jfUcw38fW47gBHmm4XCVms3SWIBSq3SGyyO1yBx4AHlFFxUOwcPhJLJrkoVPcGk9ENYFuF3i5YR0wtEHECEAEgiEmV8zH1DLYzHZ4Yi0utMltsrt9vluNjcfjCWVKtUbnd6o9QE1rMYBtxbGFvsZ3NrZj1WdYOfotUZLX0XEFHEKViKMpttjk9nlDrL8HiCWJzpcSbcyWrGoh3NCQj0zK53P1ph1WeFLLqnJZ2s5vmZLA6kginWsXaj3VLDoUAGqoSpgEp0cpVGohh5hhDWDy0sz8zruakzamWVm-Qyg362V5-AZOayO1KFlHitEejFHKCV6v+i5XRt1ZuU1s52zjNOOaZfdOWIY+RDZ0Hc6ZmKEXqyLPPCudit2Sz08ACSEFYNbSHI27kuquiIOEjiONwjJgrM3RWJYZisgEIJgnYPTmuEdi2OaiR5nQOAQHA2hvsiH4Sui0qFCcIGhnuLSmP0YJuJ2xjJsmKELG8XZTK0tjdHG06vgW5GupRS7St6vrKqSO4UhqVL8TBWp8o4eqdl0A5Xmy3G6gK56-B4uERDOSKiuJi6lgUAhrhUYB0buimtrEKZBDYrxaS0OZca8+ltheybOI4hivGZzrzp+VGHH+AGOQp4EIHy+ghNYnawtG4TsbYvk8QKfHGAJfQ9uF76WSW37xWBTSGJ0qXpd0vRZdEKGPqC2YeO2-zfO4+HxEAA */
createMachine({
tsTypes: {} as import('./MetricTagKey.machine.typegen').Typegen0,
initial: 'Idle',
states: {
TagKey: {
on: {
NEXT: {
actions: 'onSelectOperator',
target: 'Operator',
},
onBlur: {
actions: 'onBlurPurge',
target: 'Idle',
},
RESET: {
target: 'Idle',
},
},
},
Operator: {
on: {
NEXT: {
actions: 'onSelectTagValue',
target: 'TagValue',
},
// onBlur: {
// actions: 'onBlurPurge',
// target: 'Idle',
// },
RESET: {
target: 'Idle',
},
},
},
TagValue: {
on: {
onBlur: {
actions: ['onValidateQuery'],
// target: 'Idle',
},
RESET: {
target: 'Idle',
},
},
},
Idle: {
on: {
NEXT: {
actions: 'onSelectTagKey',
description: 'Select Category',
target: 'TagKey',
},
},
},
},
id: 'Dashboard Search And Filter',
});

View File

@ -0,0 +1,32 @@
// This file was automatically generated. Edits will be overwritten
export interface Typegen0 {
'@@xstate/typegen': true;
eventsCausingActions: {
onSelectOperator: 'NEXT';
onBlurPurge: 'onBlur';
onSelectTagValue: 'NEXT';
onValidateQuery: 'onBlur';
onSelectTagKey: 'NEXT';
};
internalEvents: {
'xstate.init': { type: 'xstate.init' };
};
invokeSrcNameMap: {};
missingImplementations: {
actions:
| 'onSelectOperator'
| 'onBlurPurge'
| 'onSelectTagValue'
| 'onValidateQuery'
| 'onSelectTagKey';
services: never;
guards: never;
delays: never;
};
eventsCausingServices: {};
eventsCausingGuards: {};
eventsCausingDelays: {};
matchesStates: 'TagKey' | 'Operator' | 'TagValue' | 'Idle';
tags: never;
}

View File

@ -0,0 +1,34 @@
import React from 'react';
import { QueryChipContainer, QueryChipItem } from './styles';
import { ITagKeyValueQuery } from './types';
interface IQueryChipProps {
queryData: ITagKeyValueQuery;
onClose: (id: string) => void;
disabled?: boolean;
}
export default function QueryChip({
queryData,
onClose,
disabled,
}: IQueryChipProps): JSX.Element {
return (
<QueryChipContainer>
<QueryChipItem>{queryData.key}</QueryChipItem>
<QueryChipItem>{queryData.op}</QueryChipItem>
<QueryChipItem
closable={!disabled}
onClose={(): void => {
if (!disabled) onClose(queryData.id);
}}
>
{queryData.value.join(', ')}
</QueryChipItem>
</QueryChipContainer>
);
}
QueryChip.defaultProps = {
disabled: false,
};

View File

@ -0,0 +1,213 @@
import { CloseCircleFilled } from '@ant-design/icons';
import { useMachine } from '@xstate/react';
import { Button, Select, Spin } from 'antd';
import { map } from 'lodash-es';
import React, { useCallback, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { IMetricsBuilderQuery } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app';
import { v4 as uuid } from 'uuid';
import { ResourceAttributesFilterMachine } from './MetricTagKey.machine';
import QueryChip from './QueryChip';
import { QueryChipItem, SearchContainer } from './styles';
import { IOption, ITagKeyValueQuery } from './types';
import {
createQuery,
GetTagKeys,
GetTagValues,
OperatorSchema,
SingleValueOperators,
} from './utils';
interface IMetricTagKeyFilterProps {
metricName: IMetricsBuilderQuery['metricName'];
onSetQuery: (args: IMetricsBuilderQuery['tagFilters']['items']) => void;
selectedTagFilters: IMetricsBuilderQuery['tagFilters']['items'];
}
function MetricTagKeyFilter({
metricName,
onSetQuery,
selectedTagFilters: selectedTagQueries,
}: IMetricTagKeyFilterProps): JSX.Element | null {
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
const [loading, setLoading] = useState(true);
const [selectedValues, setSelectedValues] = useState<string[]>([]);
const [staging, setStaging] = useState<string[]>([]);
const [queries, setQueries] = useState<ITagKeyValueQuery[]>([]);
const [optionsData, setOptionsData] = useState<{
mode: undefined | 'tags' | 'multiple';
options: IOption[];
}>({
mode: undefined,
options: [],
});
const dispatchQueries = (
updatedQueries: IMetricsBuilderQuery['tagFilters']['items'],
): void => {
onSetQuery(updatedQueries);
setQueries(updatedQueries);
};
const handleLoading = (isLoading: boolean): void => {
setLoading(isLoading);
if (isLoading) {
setOptionsData({ mode: undefined, options: [] });
}
};
const [state, send] = useMachine(ResourceAttributesFilterMachine, {
actions: {
onSelectTagKey: () => {
handleLoading(true);
GetTagKeys(metricName || '')
.then((tagKeys) => setOptionsData({ options: tagKeys, mode: undefined }))
.finally(() => {
handleLoading(false);
});
},
onSelectOperator: () => {
setOptionsData({ options: OperatorSchema, mode: undefined });
},
onSelectTagValue: () => {
handleLoading(true);
GetTagValues(staging[0], metricName || '')
.then((tagValuesOptions) =>
setOptionsData({ options: tagValuesOptions, mode: 'tags' }),
)
.finally(() => {
handleLoading(false);
});
},
onBlurPurge: () => {
setSelectedValues([]);
setStaging([]);
},
onValidateQuery: (): void => {
if (staging.length < 2 || selectedValues.length === 0) {
return;
}
const generatedQuery = createQuery([...staging, selectedValues]);
if (generatedQuery) {
dispatchQueries([...queries, generatedQuery]);
setSelectedValues([]);
setStaging([]);
send('RESET');
}
},
},
});
useEffect(() => {
setQueries(selectedTagQueries);
}, [selectedTagQueries]);
const handleFocus = (): void => {
if (state.value === 'Idle') {
send('NEXT');
}
};
const handleBlur = useCallback((): void => {
send('onBlur');
}, [send]);
useEffect(() => {
handleBlur();
}, [handleBlur, metricName]);
const handleChange = (value: never | string[]): void => {
if (!optionsData.mode) {
setStaging((prevStaging) => [...prevStaging, String(value)]);
setSelectedValues([]);
send('NEXT');
return;
}
if (
state.value === 'TagValue' &&
SingleValueOperators.includes(staging[staging.length - 1]) &&
Array.isArray(value)
) {
setSelectedValues([value[value.length - 1]]);
return;
}
setSelectedValues([...value]);
};
const handleClose = (id: string): void => {
dispatchQueries(queries.filter((queryData) => queryData.id !== id));
};
const handleClearAll = (): void => {
send('RESET');
dispatchQueries([]);
setStaging([]);
setSelectedValues([]);
};
return (
<SearchContainer isDarkMode={isDarkMode}>
<div style={{ display: 'inline-flex', flexWrap: 'wrap' }}>
{queries.length > 0 &&
map(
queries,
(query): JSX.Element => {
return (
<QueryChip key={query.id} queryData={query} onClose={handleClose} />
);
},
)}
</div>
<div>
{map(staging, (item) => {
return <QueryChipItem key={uuid()}>{item}</QueryChipItem>;
})}
</div>
<div style={{ display: 'flex', width: '100%' }}>
<Select
disabled={!metricName}
placeholder={`Select ${
state.value === 'Idle' ? 'Tag Key Pair' : state.value
}`}
onChange={handleChange}
bordered={false}
value={selectedValues as never}
style={{ flex: 1 }}
options={optionsData.options}
mode={optionsData?.mode}
showArrow={false}
onFocus={handleFocus}
onBlur={handleBlur}
notFoundContent={
loading ? (
<span>
<Spin size="small" /> Loading...{' '}
</span>
) : (
<span>
No resource attributes available to filter. Please refer docs to send
attributes.
</span>
)
}
/>
{queries.length || staging.length || selectedValues.length ? (
<Button
onClick={handleClearAll}
icon={<CloseCircleFilled />}
type="text"
/>
) : null}
</div>
</SearchContainer>
);
}
export default MetricTagKeyFilter;

View File

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

View File

@ -0,0 +1,18 @@
export interface IOption {
label: string;
value: string;
}
export interface IMetricBuilderTagKeyQuery {
id: string;
tagKey: string;
operator: string;
tagValue: string[];
}
export interface ITagKeyValueQuery {
id: string;
key: string;
op: string;
value: string[];
}

View File

@ -0,0 +1,55 @@
import {
getResourceAttributesTagKeys,
getResourceAttributesTagValues,
} from 'api/metrics/getResourceAttributes';
import { v4 as uuid } from 'uuid';
import { TagKeyOperator } from '../../Options';
import { IOption, ITagKeyValueQuery } from './types';
export const OperatorSchema: IOption[] = TagKeyOperator;
export const GetTagKeys = async (metricName: string): Promise<IOption[]> => {
const { payload } = await getResourceAttributesTagKeys({ metricName });
if (!payload || !payload?.data) {
return [];
}
return payload.data.map((tagKey: string) => ({
label: tagKey,
value: tagKey,
}));
};
export const GetTagValues = async (
tagKey: string,
metricName: string,
): Promise<IOption[]> => {
const { payload } = await getResourceAttributesTagValues({
tagKey,
metricName,
});
if (!payload || !payload?.data) {
return [];
}
return payload.data.map((tagValue: string) => ({
label: tagValue,
value: tagValue,
}));
};
export const createQuery = (
selectedItems: Array<string | string[]> = [],
): ITagKeyValueQuery | null => {
if (selectedItems.length === 3) {
return {
id: uuid().slice(0, 8),
key: typeof selectedItems[0] === 'string' ? selectedItems[0] : '',
op: typeof selectedItems[1] === 'string' ? selectedItems[1] : '',
value: selectedItems[2] as string[],
};
}
return null;
};
export const SingleValueOperators = ['LIKE', 'NLIKE'];

View File

@ -0,0 +1,44 @@
import { Input } from 'antd';
import React from 'react';
import { IMetricsBuilderFormula } from 'types/api/dashboard/getAll';
import QueryHeader from '../QueryHeader';
import { IQueryBuilderFormulaHandleChange } from './types';
const { TextArea } = Input;
interface IMetricsBuilderFormulaProps {
formulaData: IMetricsBuilderFormula;
formulaIndex: number;
handleFormulaChange: (args: IQueryBuilderFormulaHandleChange) => void;
}
function MetricsBuilderFormula({
formulaData,
formulaIndex,
handleFormulaChange,
}: IMetricsBuilderFormulaProps): JSX.Element {
return (
<QueryHeader
name={formulaData.name}
disabled={formulaData.disabled}
onDisable={(): void =>
handleFormulaChange({ formulaIndex, toggleDisable: true })
}
onDelete={(): void => {
handleFormulaChange({ formulaIndex, toggleDelete: true });
}}
>
<TextArea
onChange={(event): void =>
handleFormulaChange({ formulaIndex, expression: event.target.value })
}
size="middle"
defaultValue={formulaData.expression}
style={{ marginBottom: '0.5rem' }}
rows={2}
/>
</QueryHeader>
);
}
export default MetricsBuilderFormula;

View File

@ -0,0 +1,183 @@
import { PlusOutlined } from '@ant-design/icons';
import { notification } from 'antd';
import {
QueryBuilderFormulaTemplate,
QueryBuilderQueryTemplate,
} from 'constants/dashboard';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import GetFormulaName from 'lib/query/GetFormulaName';
import GetQueryName from 'lib/query/GetQueryName';
import React from 'react';
import { Query } from 'types/api/dashboard/getAll';
import {
WIDGET_QUERY_BUILDER_FORMULA_KEY_NAME,
WIDGET_QUERY_BUILDER_QUERY_KEY_NAME,
} from '../../constants';
import { QueryButton } from '../../styles';
import { IHandleUpdatedQuery } from '../../types';
import MetricsBuilderFormula from './formula';
import MetricsBuilder from './query';
import {
IQueryBuilderFormulaHandleChange,
IQueryBuilderQueryHandleChange,
} from './types';
import { canCreateQueryAndFormula } from './utils';
interface IQueryBuilderQueryContainerProps {
queryData: Query;
updateQueryData: (args: IHandleUpdatedQuery) => void;
metricsBuilderQueries: Query['metricsBuilder'];
selectedGraph: GRAPH_TYPES;
}
function QueryBuilderQueryContainer({
queryData,
updateQueryData,
metricsBuilderQueries,
selectedGraph,
}: IQueryBuilderQueryContainerProps): JSX.Element | null {
const handleQueryBuilderQueryChange = ({
queryIndex,
aggregateFunction,
metricName,
tagFilters,
groupBy,
legend,
toggleDisable,
toggleDelete,
reduceTo,
}: IQueryBuilderQueryHandleChange): void => {
const allQueries =
queryData[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME].queryBuilder;
const currentIndexQuery = allQueries[queryIndex];
if (aggregateFunction) {
currentIndexQuery.aggregateOperator = aggregateFunction;
}
if (metricName) {
currentIndexQuery.metricName = metricName;
}
if (tagFilters) {
currentIndexQuery.tagFilters.items = tagFilters;
}
if (groupBy) {
currentIndexQuery.groupBy = groupBy;
}
if (reduceTo) {
currentIndexQuery.reduceTo = reduceTo;
}
if (legend !== undefined) {
currentIndexQuery.legend = legend;
}
if (toggleDisable) {
currentIndexQuery.disabled = !currentIndexQuery.disabled;
}
if (toggleDelete) {
allQueries.splice(queryIndex, 1);
}
updateQueryData({ updatedQuery: { ...queryData } });
};
const handleQueryBuilderFormulaChange = ({
formulaIndex,
expression,
toggleDisable,
toggleDelete,
}: IQueryBuilderFormulaHandleChange): void => {
const allFormulas =
queryData[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME][
WIDGET_QUERY_BUILDER_FORMULA_KEY_NAME
];
const currentIndexFormula = allFormulas[formulaIndex];
if (expression) {
currentIndexFormula.expression = expression;
}
if (toggleDisable) {
currentIndexFormula.disabled = !currentIndexFormula.disabled;
}
if (toggleDelete) {
allFormulas.splice(formulaIndex, 1);
}
updateQueryData({ updatedQuery: { ...queryData } });
};
const addQueryHandler = (): void => {
if (!canCreateQueryAndFormula(queryData)) {
notification.error({
message:
'Unable to create query. You can create at max 10 queries and formulae.',
});
return;
}
queryData[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME].queryBuilder.push({
name:
GetQueryName(queryData[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME].queryBuilder) ||
'',
...QueryBuilderQueryTemplate,
});
updateQueryData({ updatedQuery: { ...queryData } });
};
const addFormulaHandler = (): void => {
if (!canCreateQueryAndFormula(queryData)) {
notification.error({
message:
'Unable to create formula. You can create at max 10 queries and formulae.',
});
return;
}
queryData[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME][
WIDGET_QUERY_BUILDER_FORMULA_KEY_NAME
].push({
name:
GetFormulaName(
queryData[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME][
WIDGET_QUERY_BUILDER_FORMULA_KEY_NAME
],
) || '',
...QueryBuilderFormulaTemplate,
});
updateQueryData({ updatedQuery: { ...queryData } });
};
if (!metricsBuilderQueries) {
return null;
}
return (
<>
{metricsBuilderQueries.queryBuilder.map((q, idx) => (
<MetricsBuilder
key={q.name}
queryIndex={idx}
queryData={q}
handleQueryChange={handleQueryBuilderQueryChange}
selectedGraph={selectedGraph}
/>
))}
<QueryButton onClick={addQueryHandler} icon={<PlusOutlined />}>
Query
</QueryButton>
<div style={{ marginTop: '1rem' }}>
{metricsBuilderQueries.formulas.map((f, idx) => (
<MetricsBuilderFormula
key={f.name}
formulaIndex={idx}
formulaData={f}
handleFormulaChange={handleQueryBuilderFormulaChange}
/>
))}
<QueryButton onClick={addFormulaHandler} icon={<PlusOutlined />}>
Formula
</QueryButton>
</div>
</>
);
}
export default QueryBuilderQueryContainer;

View File

@ -0,0 +1,214 @@
import { AutoComplete, Col, Input, Row, Select, Spin } from 'antd';
import { getMetricName } from 'api/metrics/getMetricName';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import React, { useEffect, useState } from 'react';
import { IMetricsBuilderQuery } from 'types/api/dashboard/getAll';
import { EReduceOperator } from 'types/common/dashboard';
import { AggregateFunctions } from '../Options';
import QueryHeader from '../QueryHeader';
import MetricTagKeyFilter from './MetricTagKeyFilter';
import { IOption } from './MetricTagKeyFilter/types';
import { GetTagKeys } from './MetricTagKeyFilter/utils';
import { IQueryBuilderQueryHandleChange } from './types';
const { Option } = Select;
interface IMetricsBuilderProps {
queryIndex: number;
selectedGraph: GRAPH_TYPES;
queryData: IMetricsBuilderQuery;
handleQueryChange: (args: IQueryBuilderQueryHandleChange) => void;
}
function MetricsBuilder({
queryIndex,
selectedGraph,
queryData,
handleQueryChange,
}: IMetricsBuilderProps): JSX.Element {
const [groupByOptions, setGroupByOptions] = useState<IOption[]>([]);
const [metricName, setMetricName] = useState<string | null>(
queryData.metricName,
);
const [metricNameList, setMetricNameList] = useState<string[]>([]);
const [metricNameLoading, setMetricNameLoading] = useState(false);
const handleMetricNameSelect = (e: string): void => {
handleQueryChange({ queryIndex, metricName: e });
setMetricName(e);
};
const handleMetricNameSearch = async (searchQuery = ''): Promise<void> => {
handleMetricNameSelect(searchQuery);
setMetricNameList([]);
setMetricNameLoading(true);
const { payload } = await getMetricName(searchQuery);
setMetricNameLoading(false);
if (!payload || !payload.data) {
return;
}
setMetricNameList(payload.data);
};
const [aggregateFunctionList, setAggregateFunctionList] = useState(
AggregateFunctions,
);
const handleAggregateFunctionsSearch = (searchQuery = ''): void => {
setAggregateFunctionList(
AggregateFunctions.filter(({ label }) =>
label.includes(searchQuery.toUpperCase()),
) || [],
);
};
useEffect(() => {
GetTagKeys(metricName || '').then((tagKeys) => {
setGroupByOptions(tagKeys);
});
}, [metricName]);
return (
<QueryHeader
name={queryData.name}
disabled={queryData.disabled}
onDisable={(): void =>
handleQueryChange({ queryIndex, toggleDisable: true })
}
onDelete={(): void => {
handleQueryChange({ queryIndex, toggleDelete: true });
}}
>
<div style={{ display: 'flex', flexDirection: 'column', padding: '0.5rem' }}>
<div>
<Select
onChange={(e): void =>
handleQueryChange({ queryIndex, aggregateFunction: e })
}
defaultValue={queryData.aggregateOperator || AggregateFunctions[0]}
style={{ minWidth: 150 }}
options={aggregateFunctionList}
showSearch
onSearch={handleAggregateFunctionsSearch}
filterOption={false}
/>
</div>
<Row style={{ gap: '3%', margin: '0.5rem 0' }}>
<Row style={{ flex: 2, gap: '3%' }}>
<Select
defaultValue="metrics"
showArrow={false}
dropdownStyle={{ display: 'none' }}
>
<Option value="metrics">Metrics</Option>
</Select>
<AutoComplete
showSearch
placeholder="Metric Name (Start typing to get suggestions)"
style={{ flex: 1, minWidth: 200 }}
showArrow={false}
filterOption={false}
onSearch={handleMetricNameSearch}
notFoundContent={metricNameLoading ? <Spin size="small" /> : null}
options={metricNameList.map((option) => ({
label: option,
value: option,
}))}
defaultValue={queryData.metricName}
value={metricName}
onSelect={handleMetricNameSelect}
/>
</Row>
<Col style={{ flex: 3 }}>
<Row style={{ gap: '3%', marginBottom: '1rem' }}>
<Select
defaultValue="WHERE"
showArrow={false}
dropdownStyle={{ display: 'none' }}
>
<Option value="WHERE">WHERE</Option>
</Select>
<MetricTagKeyFilter
metricName={metricName}
selectedTagFilters={queryData.tagFilters.items}
onSetQuery={(
updatedTagFilters: IMetricsBuilderQuery['tagFilters']['items'],
): void =>
handleQueryChange({ queryIndex, tagFilters: updatedTagFilters })
}
/>
</Row>
<Row style={{ gap: '3%', marginBottom: '1rem' }}>
{selectedGraph === 'TIME_SERIES' ? (
<>
{' '}
<Select
defaultValue="GROUP BY"
showArrow={false}
dropdownStyle={{ display: 'none' }}
>
<Option value="GROUP BY">GROUP BY</Option>
</Select>
<Select
mode="multiple"
showSearch
style={{ flex: 1 }}
defaultActiveFirstOption={false}
filterOption={false}
notFoundContent={metricNameLoading ? <Spin size="small" /> : null}
options={groupByOptions}
defaultValue={queryData.groupBy}
onChange={(e): void => {
handleQueryChange({ queryIndex, groupBy: e });
}}
/>
</>
) : (
<>
<Select
defaultValue="REDUCE TO"
showArrow={false}
dropdownStyle={{ display: 'none' }}
>
<Option value="GROUP BY">REDUCE TO</Option>
</Select>
<Select
placeholder="Latest of values in timeframe"
style={{ flex: 1 }}
options={Object.keys(EReduceOperator)
.filter((op) => !(parseInt(op, 10) >= 0))
.map((op) => ({
label: op,
value: EReduceOperator[op as keyof typeof EReduceOperator],
}))}
defaultValue={
EReduceOperator[
(queryData.reduceTo as unknown) as keyof typeof EReduceOperator
]
}
onChange={(e): void => {
handleQueryChange({ queryIndex, reduceTo: e });
}}
/>
</>
)}
</Row>
</Col>
</Row>
<Row style={{ margin: '0.5rem 0' }}>
<Input
onChange={(e): void => {
handleQueryChange({ queryIndex, legend: e.target.value });
}}
size="middle"
defaultValue={queryData.legend}
addonBefore="Legend Format"
/>
</Row>
</div>
</QueryHeader>
);
}
export default MetricsBuilder;

View File

@ -0,0 +1,23 @@
import {
IMetricsBuilderFormula,
IMetricsBuilderQuery,
} from 'types/api/dashboard/getAll';
export interface IQueryBuilderQueryHandleChange {
queryIndex: number;
aggregateFunction?: IMetricsBuilderQuery['aggregateOperator'];
metricName?: IMetricsBuilderQuery['metricName'];
tagFilters?: IMetricsBuilderQuery['tagFilters']['items'];
groupBy?: IMetricsBuilderQuery['groupBy'];
legend?: IMetricsBuilderQuery['legend'];
toggleDisable?: boolean;
toggleDelete?: boolean;
reduceTo?: IMetricsBuilderQuery['reduceTo'];
}
export interface IQueryBuilderFormulaHandleChange {
formulaIndex: number;
expression?: IMetricsBuilderFormula['expression'];
toggleDisable?: IMetricsBuilderFormula['disabled'];
toggleDelete?: boolean;
}

View File

@ -0,0 +1,18 @@
import { Query } from 'types/api/dashboard/getAll';
import {
WIDGET_QUERY_BUILDER_FORMULA_KEY_NAME,
WIDGET_QUERY_BUILDER_QUERY_KEY_NAME,
} from '../../constants';
const QUERY_AND_FORMULA_LIMIT = 10;
export const canCreateQueryAndFormula = (query: Query): boolean => {
const queries = query[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME].queryBuilder;
const formulas =
query[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME][
WIDGET_QUERY_BUILDER_FORMULA_KEY_NAME
];
return queries.length + formulas.length < QUERY_AND_FORMULA_LIMIT;
};

View File

@ -0,0 +1,39 @@
import { Tooltip } from 'antd';
import React from 'react';
interface ITabHeaderProps {
tabName: string;
hasUnstagedChanges: boolean;
}
function TabHeader({
tabName,
hasUnstagedChanges,
}: ITabHeaderProps): JSX.Element {
return (
<div
style={{
display: 'flex',
gap: '0.5rem',
justifyContent: 'center',
alignItems: 'center',
}}
>
{tabName}
{hasUnstagedChanges && (
<Tooltip title="Looks like you have un-staged changes. Make sure you click 'Stage & Run Query' if you want to save these changes.">
<div
style={{
height: '0.6rem',
width: '0.6rem',
borderRadius: '1rem',
background: 'orange',
}}
/>
</Tooltip>
)}
</div>
);
}
export default TabHeader;

View File

@ -0,0 +1,21 @@
/* eslint-disable */
// @ts-ignore
// @ts-nocheck
import { EQueryType } from 'types/common/dashboard';
import { EQueryTypeToQueryKeyMapping } from './types';
export const WIDGET_PROMQL_QUERY_KEY_NAME: EQueryTypeToQueryKeyMapping.PROM =
EQueryTypeToQueryKeyMapping[EQueryType[EQueryType.PROM]];
export const WIDGET_CLICKHOUSE_QUERY_KEY_NAME: EQueryTypeToQueryKeyMapping.CLICKHOUSE = EQueryTypeToQueryKeyMapping[
EQueryType[EQueryType.CLICKHOUSE]
] as string;
export const WIDGET_QUERY_BUILDER_QUERY_KEY_NAME: EQueryTypeToQueryKeyMapping.QUERY_BUILDER = EQueryTypeToQueryKeyMapping[
EQueryType[EQueryType.QUERY_BUILDER]
] as string;
type TFormulas = 'formulas';
export const WIDGET_QUERY_BUILDER_FORMULA_KEY_NAME: TFormulas = 'formulas';

View File

@ -1,20 +1,53 @@
import { PlusOutlined } from '@ant-design/icons';
/* eslint-disable */
//@ts-nocheck
import { Button, Tabs } from 'antd';
import TextToolTip from 'components/TextToolTip';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
import React, { useCallback, useMemo } from 'react';
import { cloneDeep, isEqual } from 'lodash-es';
import React, { useCallback, useEffect, 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 { CreateQuery, CreateQueryProps } from 'store/actions';
import {
UpdateQuery,
UpdateQueryProps,
} from 'store/actions/dashboard/updateQuery';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { Widgets } from 'types/api/dashboard/getAll';
import { Query, Widgets } from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard';
import DashboardReducer from 'types/reducer/dashboards';
import { v4 as uuid } from 'uuid';
import Query from './Query';
import { QueryButton } from './styles';
import {
WIDGET_CLICKHOUSE_QUERY_KEY_NAME,
WIDGET_PROMQL_QUERY_KEY_NAME,
WIDGET_QUERY_BUILDER_QUERY_KEY_NAME,
} from './constants';
import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
import PromQLQueryContainer from './QueryBuilder/promQL';
import QueryBuilderQueryContainer from './QueryBuilder/queryBuilder';
import TabHeader from './TabHeader';
import { getQueryKey } from './utils/getQueryKey';
import { showUnstagedStashConfirmBox } from './utils/userSettings';
function QuerySection({ selectedTime, createQuery }: QueryProps): JSX.Element {
const { TabPane } = Tabs;
function QuerySection({
handleUnstagedChanges,
updateQuery,
selectedGraph,
}: QueryProps): JSX.Element {
const [localQueryChanges, setLocalQueryChanges] = useState<Query>({} as Query);
const [rctTabKey, setRctTabKey] = useState<
Record<keyof typeof EQueryType, string>
>({
QUERY_BUILDER: uuid(),
CLICKHOUSE: uuid(),
PROM: uuid(),
});
const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
@ -32,50 +65,232 @@ function QuerySection({ selectedTime, createQuery }: QueryProps): JSX.Element {
}, [widgets, urlQuery]);
const selectedWidget = getWidget() as Widgets;
const [queryCategory, setQueryCategory] = useState<EQueryType>(
selectedWidget.query.queryType,
);
const { query = [] } = selectedWidget || {};
const { query } = selectedWidget || {};
useEffect(() => {
setLocalQueryChanges(cloneDeep(query) as Query);
}, [query]);
const queryOnClickHandler = useCallback(() => {
const widgetId = urlQuery.get('widgetId');
createQuery({
widgetId: String(widgetId),
const queryDiff = (
queryA: Query,
queryB: Query,
queryCategory: EQueryType,
): boolean => {
const keyOfConcern = getQueryKey(queryCategory);
return !isEqual(queryA[keyOfConcern], queryB[keyOfConcern]);
};
useEffect(() => {
handleUnstagedChanges(
queryDiff(query, localQueryChanges, parseInt(`${queryCategory}`, 10)),
);
}, [handleUnstagedChanges, localQueryChanges, query, queryCategory]);
const regenRctKeys = (): void => {
setRctTabKey((prevState) => {
const newState = prevState;
Object.keys(newState).forEach((key) => {
newState[key as keyof typeof EQueryType] = uuid();
});
}, [createQuery, urlQuery]);
return cloneDeep(newState);
});
};
const handleStageQuery = (): void => {
updateQuery({
updatedQuery: localQueryChanges,
widgetId: urlQuery.get('widgetId') || '',
yAxisUnit: selectedWidget.yAxisUnit,
});
};
const handleQueryCategoryChange = (qCategory: string): void => {
// If true, then it means that the user has made some changes and haven't staged them
const unstagedChanges = queryDiff(
query,
localQueryChanges,
parseInt(`${queryCategory}`, 10),
);
if (unstagedChanges && showUnstagedStashConfirmBox()) {
// eslint-disable-next-line no-alert
window.confirm(
"You are trying to navigate to different tab with unstaged changes. Your current changes will be purged. Press 'Stage & Run Query' to stage them.",
);
return;
}
setQueryCategory(parseInt(`${qCategory}`, 10));
const newLocalQuery = {
...cloneDeep(query),
queryType: parseInt(`${qCategory}`, 10),
};
setLocalQueryChanges(newLocalQuery);
regenRctKeys();
updateQuery({
updatedQuery: newLocalQuery,
widgetId: urlQuery.get('widgetId') || '',
yAxisUnit: selectedWidget.yAxisUnit,
});
};
const handleLocalQueryUpdate = ({
updatedQuery,
}: IHandleUpdatedQuery): void => {
setLocalQueryChanges(updatedQuery);
};
return (
<>
{query.map((e, index) => (
<Query
currentIndex={index}
selectedTime={selectedTime}
key={`${e.query} ${e.query.length}`}
preQuery={e.query}
preLegend={e.legend || ''}
<div style={{ display: 'flex' }}>
<Tabs
type="card"
style={{ width: '100%' }}
defaultActiveKey={queryCategory.toString()}
activeKey={queryCategory.toString()}
onChange={handleQueryCategoryChange}
tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<TextToolTip
{...{
text: `This will temporarily save the current query and graph state. This will persist across tab change`,
}}
/>
))}
<QueryButton onClick={queryOnClickHandler} icon={<PlusOutlined />}>
Query
</QueryButton>
<Button type="primary" onClick={handleStageQuery}>
Stage & Run Query
</Button>
</span>
}
>
<TabPane
tab={
<TabHeader
tabName="Query Builder"
hasUnstagedChanges={queryDiff(
query,
localQueryChanges,
EQueryType.QUERY_BUILDER,
)}
/>
}
key={EQueryType.QUERY_BUILDER.toString()}
>
<QueryBuilderQueryContainer
key={rctTabKey.QUERY_BUILDER}
queryData={localQueryChanges}
updateQueryData={({ updatedQuery }: IHandleUpdatedQuery): void => {
handleLocalQueryUpdate({ updatedQuery });
}}
metricsBuilderQueries={
localQueryChanges[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME]
}
selectedGraph={selectedGraph}
/>
</TabPane>
<TabPane
tab={
<TabHeader
tabName="ClickHouse Query"
hasUnstagedChanges={queryDiff(
query,
localQueryChanges,
EQueryType.CLICKHOUSE,
)}
/>
}
key={EQueryType.CLICKHOUSE.toString()}
>
<ClickHouseQueryContainer
key={rctTabKey.CLICKHOUSE}
queryData={localQueryChanges}
updateQueryData={({ updatedQuery }: IHandleUpdatedQuery): void => {
handleLocalQueryUpdate({ updatedQuery });
}}
clickHouseQueries={localQueryChanges[WIDGET_CLICKHOUSE_QUERY_KEY_NAME]}
/>
</TabPane>
<TabPane
tab={
<TabHeader
tabName="PromQL"
hasUnstagedChanges={queryDiff(
query,
localQueryChanges,
EQueryType.PROM,
)}
/>
}
key={EQueryType.PROM.toString()}
>
<PromQLQueryContainer
key={rctTabKey.PROM}
queryData={localQueryChanges}
updateQueryData={({ updatedQuery }: IHandleUpdatedQuery): void => {
handleLocalQueryUpdate({ updatedQuery });
}}
promQLQueries={localQueryChanges[WIDGET_PROMQL_QUERY_KEY_NAME]}
/>
</TabPane>
</Tabs>
</div>
{/* {localQueryChanges.map((e, index) => (
// <Query
// name={e.name}
// currentIndex={index}
// selectedTime={selectedTime}
// key={JSON.stringify(e)}
// queryInput={e}
// updatedLocalQuery={handleLocalQueryUpdate}
// queryCategory={queryCategory}
// />
<QueryBuilder
key={`${JSON.stringify(e)}`}
name={e.name}
updateQueryData={(updatedQuery) =>
handleLocalQueryUpdate({ currentIndex: index, updatedQuery })
}
onDelete={() => handleDeleteQuery({ currentIndex: index })}
queryData={e}
queryCategory={queryCategory}
/>
))} */}
</>
);
}
interface DispatchProps {
createQuery: ({
widgetId,
}: CreateQueryProps) => (dispatch: Dispatch<AppActions>) => void;
// createQuery: ({
// widgetId,
// }: CreateQueryProps) => (dispatch: Dispatch<AppActions>) => void;
updateQuery: (
props: UpdateQueryProps,
) => (dispatch: Dispatch<AppActions>) => void;
// getQueryResults: (
// props: GetQueryResultsProps,
// ) => (dispatch: Dispatch<AppActions>) => void;
// updateQueryType: (
// props: UpdateQueryTypeProps,
// ) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
createQuery: bindActionCreators(CreateQuery, dispatch),
// createQuery: bindActionCreators(CreateQuery, dispatch),
updateQuery: bindActionCreators(UpdateQuery, dispatch),
// getQueryResults: bindActionCreators(GetQueryResults, dispatch),
// updateQueryType: bindActionCreators(UpdateQueryType, dispatch),
});
interface QueryProps extends DispatchProps {
selectedGraph: GRAPH_TYPES;
selectedTime: timePreferance;
handleUnstagedChanges: (arg0: boolean) => void;
}
export default connect(null, mapDispatchToProps)(QuerySection);

View File

@ -8,6 +8,7 @@ export const InputContainer = styled.div`
export const Container = styled.div`
margin-top: 1rem;
display: flex;
flex-direction: column;
`;
export const QueryButton = styled(Button)`
@ -18,11 +19,15 @@ export const QueryButton = styled(Button)`
`;
export const QueryWrapper = styled.div`
width: 100%; // parent need to 100%
width: 100%;
margin: 1rem 0;
padding: 1rem 0.5rem;
display: flex;
flex-direction: column;
`;
> div {
width: 95%; // each child is taking 95% of the parent
}
export const QueryBuilderWrapper = styled.div<{ isDarkMode: boolean }>`
background: ${({ isDarkMode }): string => (isDarkMode ? '#000' : '#efefef')};
`;
export const ButtonContainer = styled.div`

View File

@ -0,0 +1,19 @@
import { Query } from 'types/api/dashboard/getAll';
export type TQueryCategories = 'query_builder' | 'clickhouse_query' | 'promql';
export enum EQueryCategories {
query_builder = 0,
clickhouse_query,
promql,
}
export enum EQueryTypeToQueryKeyMapping {
QUERY_BUILDER = 'metricsBuilder',
CLICKHOUSE = 'clickHouse',
PROM = 'promQL',
}
export interface IHandleUpdatedQuery {
updatedQuery: Query;
}

View File

@ -0,0 +1,11 @@
import { EQueryType } from 'types/common/dashboard';
import { EQueryTypeToQueryKeyMapping } from '../types';
export const getQueryKey = (
queryCategory: EQueryType,
): EQueryTypeToQueryKeyMapping => {
return EQueryTypeToQueryKeyMapping[
EQueryType[queryCategory] as keyof typeof EQueryTypeToQueryKeyMapping
];
};

View File

@ -0,0 +1,23 @@
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
const UNSTAGE_CONFIRM_BOX_SHOW_COUNT = 2;
const UNSTAGE_CONFIRM_BOX_KEY =
'DASHBOARD_METRICS_BUILDER_UNSTAGE_STASH_CONFIRM_SHOW_COUNT';
export const showUnstagedStashConfirmBox = (): boolean => {
const showCountTillNow: number = parseInt(
getLocalStorageApi(UNSTAGE_CONFIRM_BOX_KEY) || '',
10,
);
if (Number.isNaN(showCountTillNow)) {
setLocalStorageApi(UNSTAGE_CONFIRM_BOX_KEY, '1');
return true;
}
if (showCountTillNow >= UNSTAGE_CONFIRM_BOX_SHOW_COUNT) {
return false;
}
setLocalStorageApi(UNSTAGE_CONFIRM_BOX_KEY, `${showCountTillNow + 1}`);
return true;
};

View File

@ -0,0 +1,35 @@
import React from 'react';
import { EQueryType } from 'types/common/dashboard';
import { Tag } from '../styles';
interface IQueryTypeTagProps {
queryType: EQueryType | undefined;
}
function QueryTypeTag({ queryType }: IQueryTypeTagProps): JSX.Element {
switch (queryType) {
case EQueryType.QUERY_BUILDER:
return (
<span>
<Tag color="geekblue">Query Builder</Tag>
</span>
);
case EQueryType.CLICKHOUSE:
return (
<span>
<Tag color="orange">ClickHouse Query</Tag>
</span>
);
case EQueryType.PROM:
return (
<span>
<Tag color="green">PromQL</Tag>
</span>
);
default:
return <span />;
}
}
export default QueryTypeTag;

View File

@ -0,0 +1,22 @@
import React from 'react';
import { EQueryType } from 'types/common/dashboard';
import QueryTypeTag from '../QueryTypeTag';
interface IPlotTagProps {
queryType: EQueryType;
}
function PlotTag({ queryType }: IPlotTagProps): JSX.Element | null {
if (queryType === undefined) {
return null;
}
return (
<div style={{ marginLeft: '2rem', position: 'absolute', top: '1rem' }}>
Plotted using <QueryTypeTag queryType={queryType} />
</div>
);
}
export default PlotTag;

View File

@ -34,7 +34,14 @@ function WidgetGraph({
const { queryData, title, opacity, isStacked } = selectedWidget;
if (queryData.data.length === 0) {
if (queryData.error) {
return (
<NotFoundContainer>
<Typography>{queryData.errorMessage}</Typography>
</NotFoundContainer>
);
}
if (queryData.data.queryData.length === 0) {
return (
<NotFoundContainer>
<Typography>No Data</Typography>
@ -43,7 +50,7 @@ function WidgetGraph({
}
const chartDataSet = getChartData({
queryData: queryData.data,
queryData: [queryData.data],
});
return (

View File

@ -8,6 +8,7 @@ import { AppState } from 'store/reducers';
import DashboardReducer from 'types/reducer/dashboards';
import { NewWidgetProps } from '../../index';
import PlotTag from './PlotTag';
import { AlertIconContainer, Container, NotFoundContainer } from './styles';
import WidgetGraphComponent from './WidgetGraph';
@ -35,9 +36,9 @@ function WidgetGraph({
}
const { queryData } = selectedWidget;
return (
<Container>
<PlotTag queryType={selectedWidget.query.queryType} />
{queryData.error && (
<AlertIconContainer color="red" title={queryData.errorMessage}>
<InfoCircleOutlined />

View File

@ -7,8 +7,8 @@ export const Container = styled(Card)`
}
.ant-card-body {
padding: 0;
height: 55vh;
padding: 1.5rem 0;
height: 57vh;
/* padding-bottom: 2rem; */
}
`;

View File

@ -10,13 +10,17 @@ function LeftContainer({
selectedGraph,
selectedTime,
yAxisUnit,
handleUnstagedChanges,
}: LeftContainerProps): JSX.Element {
return (
<>
<WidgetGraph selectedGraph={selectedGraph} yAxisUnit={yAxisUnit} />
<QueryContainer>
<QuerySection selectedTime={selectedTime} />
<QuerySection
selectedTime={selectedTime}
handleUnstagedChanges={handleUnstagedChanges}
selectedGraph={selectedGraph}
/>
</QueryContainer>
</>
);
@ -24,6 +28,7 @@ function LeftContainer({
interface LeftContainerProps extends NewWidgetProps {
selectedTime: timePreferance;
handleUnstagedChanges: (arg0: boolean) => void;
}
export default memo(LeftContainer);

View File

@ -1,14 +1,13 @@
import { Button } from 'antd';
import { Button, Modal, Typography } from 'antd';
import ROUTES from 'constants/routes';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
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 { connect, useDispatch, useSelector } from 'react-redux';
import { generatePath, useLocation, useParams } from 'react-router-dom';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { ApplySettingsToPanel, ApplySettingsToPanelProps } from 'store/actions';
import {
GetQueryResults,
GetQueryResultsProps,
@ -17,17 +16,15 @@ import {
SaveDashboard,
SaveDashboardProps,
} from 'store/actions/dashboard/saveDashboard';
import {
UpdateQuery,
UpdateQueryProps,
} from 'store/actions/dashboard/updateQuery';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { FLUSH_DASHBOARD } from 'types/actions/dashboard';
import { Widgets } from 'types/api/dashboard/getAll';
import DashboardReducer from 'types/reducer/dashboards';
import { GlobalReducer } from 'types/reducer/globalTime';
import LeftContainer from './LeftContainer';
import QueryTypeTag from './LeftContainer/QueryTypeTag';
import RightContainer from './RightContainer';
import TimeItems, { timePreferance } from './RightContainer/timeItems';
import {
@ -36,15 +33,15 @@ import {
LeftContainerWrapper,
PanelContainer,
RightContainerWrapper,
Tag,
} from './styles';
function NewWidget({
selectedGraph,
applySettingsToPanel,
saveSettingOfPanel,
getQueryResults,
updateQuery,
}: Props): JSX.Element {
const dispatch = useDispatch();
const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
@ -87,6 +84,8 @@ function NewWidget({
const [selectedNullZeroValue, setSelectedNullZeroValue] = useState<string>(
selectedWidget?.nullZeroValues || 'zero',
);
const [saveModal, setSaveModal] = useState(false);
const [hasUnstagedChanges, setHasUnstagedChanges] = useState(false);
const getSelectedTime = useCallback(
() =>
@ -116,50 +115,30 @@ function NewWidget({
dashboardId,
});
}, [
opacity,
description,
query,
selectedTime,
stacked,
title,
selectedNullZeroValue,
saveSettingOfPanel,
selectedDashboard,
dashboardId,
selectedDashboard.uuid,
description,
stacked,
selectedNullZeroValue,
opacity,
selectedTime.enum,
title,
yAxisUnit,
query,
dashboardId,
]);
const onClickApplyHandler = (): void => {
selectedWidget?.query.forEach((element, index) => {
updateQuery({
widgetId: selectedWidget?.id || '',
query: element.query || '',
legend: element.legend || '',
currentIndex: index,
yAxisUnit,
});
});
applySettingsToPanel({
description,
isStacked: stacked,
nullZeroValues: selectedNullZeroValue,
opacity,
timePreferance: selectedTime.enum,
title,
widgetId: selectedWidget?.id || '',
yAxisUnit,
});
};
const onClickDiscardHandler = useCallback(() => {
dispatch({
type: FLUSH_DASHBOARD,
});
history.push(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId]);
}, [dashboardId, dispatch]);
const getQueryResult = useCallback(() => {
if (selectedWidget?.id.length !== 0) {
if (selectedWidget?.id.length !== 0 && selectedWidget?.query) {
getQueryResults({
query: selectedWidget?.query || [],
query: selectedWidget?.query,
selectedTime: selectedTime.enum,
widgetId: selectedWidget?.id || '',
graphType: selectedGraph,
@ -182,14 +161,17 @@ function NewWidget({
return (
<Container>
<ButtonContainer>
<Button onClick={onClickSaveHandler}>Save</Button>
<Button onClick={onClickApplyHandler}>Apply</Button>
<Button type="primary" onClick={(): void => setSaveModal(true)}>
Save
</Button>
{/* <Button onClick={onClickApplyHandler}>Apply</Button> */}
<Button onClick={onClickDiscardHandler}>Discard</Button>
</ButtonContainer>
<PanelContainer>
<LeftContainerWrapper flex={5}>
<LeftContainer
handleUnstagedChanges={setHasUnstagedChanges}
selectedTime={selectedTime}
selectedGraph={selectedGraph}
yAxisUnit={yAxisUnit}
@ -218,6 +200,34 @@ function NewWidget({
/>
</RightContainerWrapper>
</PanelContainer>
<Modal
title="Save Changes"
focusTriggerAfterClose
forceRender
destroyOnClose
closable
onCancel={(): void => setSaveModal(false)}
onOk={(): void => {
onClickSaveHandler();
}}
centered
visible={saveModal}
width={600}
>
{hasUnstagedChanges ? (
<Typography>
Looks like you have unstaged changes. Would you like to SAVE the last
staged changes? If you want to stage new changes - Press{' '}
<Tag>Stage & Run Query</Tag> and then try saving again.
</Typography>
) : (
<Typography>
Your graph built with{' '}
<QueryTypeTag queryType={selectedWidget?.query.queryType} /> query will be
saved. Press OK to confirm.
</Typography>
)}
</Modal>
</Container>
);
}
@ -228,27 +238,19 @@ export interface NewWidgetProps {
}
interface DispatchProps {
applySettingsToPanel: (
props: ApplySettingsToPanelProps,
) => (dispatch: Dispatch<AppActions>) => void;
saveSettingOfPanel: (
props: SaveDashboardProps,
) => (dispatch: Dispatch<AppActions>) => void;
getQueryResults: (
props: GetQueryResultsProps,
) => (dispatch: Dispatch<AppActions>) => void;
updateQuery: (
props: UpdateQueryProps,
) => (dispatch: Dispatch<AppActions>) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
applySettingsToPanel: bindActionCreators(ApplySettingsToPanel, dispatch),
saveSettingOfPanel: bindActionCreators(SaveDashboard, dispatch),
getQueryResults: bindActionCreators(GetQueryResults, dispatch),
updateQuery: bindActionCreators(UpdateQuery, dispatch),
});
type Props = DispatchProps & NewWidgetProps;

View File

@ -1,4 +1,4 @@
import { Col } from 'antd';
import { Col, Tag as AntDTag } from 'antd';
import styled from 'styled-components';
export const Container = styled.div`
@ -31,3 +31,7 @@ export const ButtonContainer = styled.div`
export const PanelContainer = styled.div`
display: flex;
`;
export const Tag = styled(AntDTag)`
margin: 0;
`;

View File

@ -15,6 +15,7 @@ import AppReducer from 'types/reducer/app';
import menus from './menuItems';
import Slack from './Slack';
import {
Name,
RedDot,
Sider,
SlackButton,
@ -103,8 +104,8 @@ function SideNav(): JSX.Element {
icon={<Icon />}
onClick={(): void => onClickHandler(to)}
>
<Space style={{ position: 'relative' }}>
<Typography>{name}</Typography>
<Space>
<Name ellipsis>{name}</Name>
{tags &&
tags.map((e) => (
<Tags style={{ lineHeight: '1rem' }} color="#177DDC" key={e}>

View File

@ -15,7 +15,7 @@ const menus: SidebarMenu[] = [
{
Icon: BarChartOutlined,
to: ROUTES.APPLICATION,
name: 'Metrics',
name: 'Services',
},
{
Icon: AlignLeftOutlined,

View File

@ -83,3 +83,9 @@ export const Tags = styled(Tag)`
border-radius: 0.5rem;
}
`;
export const Name = styled(Typography.Paragraph)`
&&& {
margin: 0;
}
`;

View File

@ -16,6 +16,8 @@ const breadcrumbNameMap = {
[ROUTES.ORG_SETTINGS]: 'Organization Settings',
[ROUTES.MY_SETTINGS]: 'My Settings',
[ROUTES.ERROR_DETAIL]: 'Errors',
[ROUTES.LIST_ALL_ALERT]: 'Alerts',
[ROUTES.ALL_DASHBOARD]: 'Dashboard',
};
function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element {

View File

@ -155,10 +155,9 @@ function CheckBoxComponent(props: CheckBoxProps): JSX.Element {
const isCheckBoxSelected = isUserSelected;
const TooTipOverLay = useMemo(
(): JSX.Element => <Typography>{keyValue}</Typography>,
[keyValue],
);
const TooTipOverLay = useMemo((): JSX.Element => <div>{keyValue}</div>, [
keyValue,
]);
return (
<CheckBoxContainer>

View File

@ -1,12 +1,19 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { Button, Input } from 'antd';
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import { INITIAL_FILTER_VALUE } from 'store/reducers/trace';
import AppActions from 'types/actions';
import { UPDATE_SPAN_UPDATE_FILTER_DISPLAY_VALUE } from 'types/actions/trace';
import { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
import CheckBoxComponent from '../Common/Checkbox';
const { Search } = Input;
function CommonCheckBox(props: CommonCheckBoxProps): JSX.Element {
const { filter } = useSelector<AppState, TraceReducer>(
const { filter, filterDisplayValue } = useSelector<AppState, TraceReducer>(
(state) => state.traces,
);
@ -15,9 +22,40 @@ function CommonCheckBox(props: CommonCheckBoxProps): JSX.Element {
const status = filter.get(name) || {};
const statusObj = Object.keys(status);
const numberOfFilters = filterDisplayValue.get(name) || 0;
const dispatch = useDispatch<Dispatch<AppActions>>();
const [searchFilter, setSearchFilter] = useState<string>('');
const onClickMoreHandler = (): void => {
const newFilterDisplayValue = new Map(filterDisplayValue);
const preValue =
(newFilterDisplayValue.get(name) || 0) + INITIAL_FILTER_VALUE;
newFilterDisplayValue.set(name, preValue);
dispatch({
type: UPDATE_SPAN_UPDATE_FILTER_DISPLAY_VALUE,
payload: newFilterDisplayValue,
});
};
const isMoreButtonAvilable = Boolean(
numberOfFilters && statusObj.length > numberOfFilters,
);
return (
<>
{statusObj.length > 0 && (
<Search
value={searchFilter}
onChange={(e): void => setSearchFilter(e.target.value)}
style={{
padding: '0 3%',
}}
placeholder="Filter Values"
/>
)}
{statusObj
.sort((a, b) => {
const countA = +status[a];
@ -28,6 +66,15 @@ function CommonCheckBox(props: CommonCheckBoxProps): JSX.Element {
}
return countA - countB;
})
.filter((filter) => {
if (searchFilter.length === 0) {
return true;
}
return filter
.toLocaleLowerCase()
.includes(searchFilter.toLocaleLowerCase());
})
.filter((_, index) => index < numberOfFilters)
.map((e) => (
<CheckBoxComponent
key={e}
@ -38,6 +85,12 @@ function CommonCheckBox(props: CommonCheckBoxProps): JSX.Element {
}}
/>
))}
{isMoreButtonAvilable && (
<Button onClick={onClickMoreHandler} type="link">
More
</Button>
)}
</>
);
}

View File

@ -1,11 +1,14 @@
/* eslint-disable react/no-unstable-nested-components */
import { Input, Slider } from 'antd';
import { Slider } from 'antd';
import { SliderRangeProps } from 'antd/lib/slider';
import getFilters from 'api/trace/getFilters';
import dayjs from 'dayjs';
import durationPlugin from 'dayjs/plugin/duration';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux';
import { getFilter, updateURL } from 'store/actions/trace/util';
@ -15,19 +18,8 @@ import { UPDATE_ALL_FILTERS } from 'types/actions/trace';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
import { Container, InputContainer, Text } from './styles';
dayjs.extend(durationPlugin);
const getMs = (value: string): string => {
return parseFloat(
dayjs
.duration({
milliseconds: parseInt(value, 10) / 1000000,
})
.format('SSS'),
).toFixed(2);
};
import { Container, InputComponent, InputContainer, Text } from './styles';
import { getMs } from './util';
function Duration(): JSX.Element {
const {
@ -77,17 +69,18 @@ function Duration(): JSX.Element {
preLocalMinDuration.current = parseFloat(minDuration);
}
setPreMax(maxDuration);
setPreMin(minDuration);
setPreMax(getMs(maxDuration));
setPreMin(getMs(minDuration));
}, [getDuration]);
const defaultValue = [parseFloat(preMin), parseFloat(preMax)];
const updatedUrl = async (min: number, max: number): Promise<void> => {
const preSelectedFilter = new Map(selectedFilter);
const preUserSelected = new Map(userSelectedFilter);
preSelectedFilter.set('duration', [String(max), String(min)]);
preSelectedFilter.set('duration', [
String(max * 1000000),
String(min * 1000000),
]);
const response = await getFilters({
end: String(globalTime.maxTime),
@ -137,18 +130,18 @@ function Duration(): JSX.Element {
}
};
const onRangeSliderHandler = (number: [number, number]): void => {
const onRangeSliderHandler = (number: [string, string]): void => {
const [min, max] = number;
setPreMin(min.toString());
setPreMax(max.toString());
setPreMin(min);
setPreMax(max);
};
const debouncedFunction = useDebouncedFn(
(min, max) => {
updatedUrl(min as number, max as number);
},
500,
1500,
undefined,
);
@ -156,8 +149,8 @@ function Duration(): JSX.Element {
event,
) => {
const { value } = event.target;
const min = parseFloat(preMin);
const max = parseFloat(value) * 1000000;
const min = preMin;
const max = value;
onRangeSliderHandler([min, max]);
debouncedFunction(min, max);
@ -167,8 +160,9 @@ function Duration(): JSX.Element {
event,
) => {
const { value } = event.target;
const min = parseFloat(value) * 1000000;
const max = parseFloat(preMax);
const min = value;
const max = preMax;
onRangeSliderHandler([min, max]);
debouncedFunction(min, max);
};
@ -177,45 +171,48 @@ function Duration(): JSX.Element {
updatedUrl(min, max);
};
const TipComponent = useCallback((value) => {
if (value === undefined) {
return <div />;
}
return <div>{`${getMs(value?.toString())}ms`}</div>;
}, []);
return (
<div>
<Container>
<InputContainer>
<Text>Min</Text>
</InputContainer>
<Input
<InputComponent
addonAfter="ms"
type="number"
onChange={onChangeMinHandler}
value={getMs(preMin)}
value={preMin}
/>
<InputContainer>
<Text>Max</Text>
</InputContainer>
<Input
<InputComponent
addonAfter="ms"
type="number"
onChange={onChangeMaxHandler}
value={getMs(preMax)}
value={preMax}
/>
</Container>
<Container>
<Slider
defaultValue={[defaultValue[0], defaultValue[1]]}
min={parseFloat((preLocalMinDuration.current || 0).toString())}
max={parseFloat((preLocalMaxDuration.current || 0).toString())}
min={Number(getMs(String(preLocalMinDuration.current || 0)))}
max={Number(getMs(String(preLocalMaxDuration.current || 0)))}
range
tipFormatter={(value): JSX.Element => {
if (value === undefined) {
return <div />;
}
return <div>{`${getMs(value?.toString())}ms`}</div>;
}}
tipFormatter={TipComponent}
onChange={([min, max]): void => {
onRangeSliderHandler([min, max]);
onRangeSliderHandler([String(min), String(max)]);
}}
onAfterChange={onRangeHandler}
value={[parseFloat(preMin), parseFloat(preMax)]}
value={[Number(preMin), Number(preMax)]}
/>
</Container>
</div>

View File

@ -1,4 +1,4 @@
import { Typography } from 'antd';
import { Input, Typography } from 'antd';
import styled from 'styled-components';
export const DurationText = styled.div`
@ -9,6 +9,19 @@ export const DurationText = styled.div`
flex-direction: column;
`;
export const InputComponent = styled(Input)`
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type='number'] {
-moz-appearance: textfield;
}
`;
export const InputContainer = styled.div`
width: 100%;
margin-top: 0.5rem;

View File

@ -0,0 +1,13 @@
import dayjs from 'dayjs';
import durationPlugin from 'dayjs/plugin/duration';
dayjs.extend(durationPlugin);
export const getMs = (value: string): string =>
parseFloat(
dayjs
.duration({
milliseconds: parseInt(value, 10) / 1000000,
})
.format('SSS'),
).toFixed(2);

View File

@ -73,11 +73,24 @@ function TagsKey(props: TagsKeysProps): JSX.Element {
<AutoComplete
dropdownClassName="certain-category-search-dropdown"
dropdownMatchSelectWidth={500}
style={{ width: 300 }}
options={options}
style={{ width: '100%' }}
value={selectedKey}
onChange={(value): void => {
if (options && options.find((option) => option.value === value)) {
allowClear
showSearch
options={options?.map((e) => ({
label: e.label?.toString(),
value: e.value,
}))}
filterOption={(inputValue, option): boolean =>
option?.label?.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
onChange={(e): void => setSelectedKey(e)}
onSelect={(value: unknown): void => {
if (
typeof value === 'string' &&
options &&
options.find((option) => option.value === value)
) {
setSelectedKey(value);
setLocalSelectedTags((tags) => [
@ -89,8 +102,6 @@ function TagsKey(props: TagsKeysProps): JSX.Element {
},
...tags.slice(index + 1, tags.length),
]);
} else {
setSelectedKey('');
}
}}
>

View File

@ -1,13 +1,13 @@
import { Select } from 'antd';
import getTagValue from 'api/trace/getTagValue';
import React from 'react';
import React, { useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
import { SelectComponent } from './styles';
import { AutoCompleteComponent } from './styles';
function TagValue(props: TagValueProps): JSX.Element {
const { tag, setLocalSelectedTags, index, tagKey } = props;
@ -16,6 +16,7 @@ function TagValue(props: TagValueProps): JSX.Element {
Operator: selectedOperator,
Values: selectedValues,
} = tag;
const [localValue, setLocalValue] = useState<string>(selectedValues[0]);
const globalReducer = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
@ -34,22 +35,38 @@ function TagValue(props: TagValueProps): JSX.Element {
);
return (
<SelectComponent
value={selectedValues[0]}
<AutoCompleteComponent
options={data?.payload?.map((e) => ({
label: e.tagValues,
value: e.tagValues,
}))}
allowClear
defaultOpen
showSearch
filterOption={(inputValue, option): boolean =>
option?.label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
disabled={isLoading}
value={localValue}
onChange={(values): void => {
if (typeof values === 'string') {
setLocalValue(values);
}
}}
onSelect={(value: unknown): void => {
if (typeof value === 'string') {
setLocalValue(value);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: selectedKey,
Operator: selectedOperator,
Values: [...selectedValues, value],
Values: [value],
},
...tags.slice(index + 1, tags.length),
]);
}
}}
loading={isLoading || false}
>
{data &&
data.payload &&
@ -58,7 +75,7 @@ function TagValue(props: TagValueProps): JSX.Element {
{suggestion.tagValues}
</Select.Option>
))}
</SelectComponent>
</AutoCompleteComponent>
);
}

View File

@ -1,4 +1,4 @@
import { Select, Space } from 'antd';
import { AutoComplete, Select, Space } from 'antd';
import styled from 'styled-components';
export const SpaceComponent = styled(Space)`
@ -9,18 +9,23 @@ export const SpaceComponent = styled(Space)`
export const SelectComponent = styled(Select)`
&&& {
min-width: 170px;
margin-right: 21.91px;
margin-left: 21.92px;
width: 100%;
}
`;
export const Container = styled.div`
export const Container = styled(Space)`
&&& {
display: flex;
margin-top: 1rem;
margin-bottom: 1rem;
}
.ant-space-item:not(:last-child, :nth-child(2)) {
width: 100%;
}
.ant-space-item:nth-child(2) {
width: 50%;
}
`;
export const IconContainer = styled.div`
@ -31,3 +36,9 @@ export const IconContainer = styled.div`
margin-left: 1.125rem;
`;
export const AutoCompleteComponent = styled(AutoComplete)`
&&& {
width: 100%;
}
`;

View File

@ -1,7 +1,7 @@
import { Space, Tabs, Typography } from 'antd';
import { Tabs, Tooltip, Typography } from 'antd';
import { StyledSpace } from 'components/Styled';
import useThemeMode from 'hooks/useThemeMode';
import React from 'react';
import React, { useMemo } from 'react';
import { ITraceTree } from 'types/api/trace/getTraceItem';
import ErrorTag from './ErrorTag';
@ -19,29 +19,38 @@ const { TabPane } = Tabs;
function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
const { tree } = props;
const { isDarkMode } = useThemeMode();
const OverLayComponentName = useMemo(() => tree?.name, [tree?.name]);
const OverLayComponentServiceName = useMemo(() => tree?.serviceName, [
tree?.serviceName,
]);
if (!tree) {
return <div />;
}
const { name, tags, serviceName } = tree;
const { tags } = tree;
return (
<CardContainer>
<StyledSpace
styledclass={[styles.selectedSpanDetailsContainer]}
styledclass={[styles.selectedSpanDetailsContainer, styles.overflow]}
direction="vertical"
style={{ marginLeft: '0.5rem' }}
>
<strong> Details for selected Span </strong>
<Space direction="vertical" size={2}>
<CustomTitle>Service</CustomTitle>
<CustomText>{serviceName}</CustomText>
</Space>
<Space direction="vertical" size={2}>
<Tooltip overlay={OverLayComponentServiceName}>
<CustomText ellipsis>{tree.serviceName}</CustomText>
</Tooltip>
<CustomTitle>Operation</CustomTitle>
<CustomText>{name}</CustomText>
</Space>
<Tooltip overlay={OverLayComponentName}>
<CustomText ellipsis>{tree.name}</CustomText>
</Tooltip>
</StyledSpace>
<Tabs defaultActiveKey="1">
<TabPane tab="Tags" key="1">
{tags.length !== 0 ? (

View File

@ -1,7 +1,7 @@
import { Typography } from 'antd';
import { Space, Typography } from 'antd';
import styled, { css } from 'styled-components';
const { Text, Title, Paragraph } = Typography;
const { Title, Paragraph } = Typography;
export const CustomTitle = styled(Title)`
&&& {
@ -9,7 +9,7 @@ export const CustomTitle = styled(Title)`
}
`;
export const CustomText = styled(Text)`
export const CustomText = styled(Paragraph)`
&&& {
color: #2d9cdb;
}
@ -17,7 +17,6 @@ export const CustomText = styled(Text)`
export const CustomSubTitle = styled(Title)`
&&& {
/* color: #bdbdbd; */
font-size: 14px;
margin-bottom: 8px;
}
@ -44,6 +43,17 @@ export const CardContainer = styled.div`
width: 100%;
flex: 1;
overflow-y: auto;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`;
export const CustomSpace = styled(Space)`
&&& {
.ant-space-item {
width: 100%;
}
}
`;
const removeMargin = css`
@ -60,9 +70,21 @@ const selectedSpanDetailsContainer = css`
const spanEventsTabsContainer = css`
margin-top: 1rem;
`;
const overflow = css`
width: 95%;
> div.ant-space-item:nth-child(4) {
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`;
export const styles = {
removeMargin,
removePadding,
selectedSpanDetailsContainer,
spanEventsTabsContainer,
overflow,
};

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