mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-06 01:56:05 +08:00
commit
90566360ae
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
@ -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
|
||||
|
2
.github/workflows/build.yaml
vendored
2
.github/workflows/build.yaml
vendored
@ -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
17
.github/workflows/codeball.yml
vendored
Normal 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"
|
18
.github/workflows/playwright.yaml
vendored
18
.github/workflows/playwright.yaml
vendored
@ -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
28
deploy/docker-swarm/clickhouse-setup/clickhouse-storage.xml
Normal file
28
deploy/docker-swarm/clickhouse-setup/clickhouse-storage.xml
Normal 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>
|
123
deploy/docker-swarm/clickhouse-setup/clickhouse-users.xml
Normal file
123
deploy/docker-swarm/clickhouse-setup/clickhouse-users.xml
Normal 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>
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
28
deploy/docker/clickhouse-setup/clickhouse-storage.xml
Normal file
28
deploy/docker/clickhouse-setup/clickhouse-storage.xml
Normal 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>
|
123
deploy/docker/clickhouse-setup/clickhouse-users.xml
Normal file
123
deploy/docker/clickhouse-setup/clickhouse-users.xml
Normal 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>
|
1304
deploy/docker/clickhouse-setup/config.xml
Normal file
1304
deploy/docker/clickhouse-setup/config.xml
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
123
deploy/docker/clickhouse-setup/users.xml
Normal file
123
deploy/docker/clickhouse-setup/users.xml
Normal 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>
|
@ -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;
|
||||
|
@ -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 "+++++++++++++++++++++++++++++++++++++++++++++++++"
|
||||
|
@ -2,3 +2,4 @@
|
||||
* Adds custom matchers from the react testing library to all tests
|
||||
*/
|
||||
import '@testing-library/jest-dom';
|
||||
import 'jest-styled-components';
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
27
frontend/src/api/metrics/getMetricName.ts
Normal file
27
frontend/src/api/metrics/getMetricName.ts
Normal 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);
|
||||
}
|
||||
};
|
25
frontend/src/api/metrics/getQueryRange.ts
Normal file
25
frontend/src/api/metrics/getQueryRange.ts
Normal 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);
|
||||
}
|
||||
};
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -22,7 +22,6 @@ const getOrCreateLegendList = (
|
||||
listContainer.style.height = '100%';
|
||||
listContainer.style.flexWrap = 'wrap';
|
||||
listContainer.style.justifyContent = 'center';
|
||||
|
||||
legendContainer?.appendChild(listContainer);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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('');
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
33
frontend/src/constants/dashboard.ts
Normal file
33
frontend/src/constants/dashboard.ts
Normal 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,
|
||||
};
|
@ -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',
|
||||
|
@ -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;
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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(() => {
|
||||
|
@ -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,
|
||||
|
@ -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> = [
|
||||
|
@ -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,
|
||||
|
@ -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]);
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}));
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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);
|
@ -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' },
|
||||
];
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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',
|
||||
});
|
@ -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;
|
||||
}
|
@ -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,
|
||||
};
|
@ -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;
|
@ -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;
|
||||
`;
|
@ -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[];
|
||||
}
|
@ -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'];
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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;
|
||||
};
|
@ -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;
|
@ -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';
|
@ -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);
|
||||
|
@ -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`
|
||||
|
@ -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;
|
||||
}
|
@ -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
|
||||
];
|
||||
};
|
@ -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;
|
||||
};
|
@ -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;
|
@ -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;
|
@ -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 (
|
||||
|
@ -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 />
|
||||
|
@ -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; */
|
||||
}
|
||||
`;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
`;
|
||||
|
@ -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}>
|
||||
|
@ -15,7 +15,7 @@ const menus: SidebarMenu[] = [
|
||||
{
|
||||
Icon: BarChartOutlined,
|
||||
to: ROUTES.APPLICATION,
|
||||
name: 'Metrics',
|
||||
name: 'Services',
|
||||
},
|
||||
{
|
||||
Icon: AlignLeftOutlined,
|
||||
|
@ -83,3 +83,9 @@ export const Tags = styled(Tag)`
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Name = styled(Typography.Paragraph)`
|
||||
&&& {
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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);
|
@ -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('');
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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%;
|
||||
}
|
||||
`;
|
||||
|
@ -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 ? (
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user