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
|
# Owners are automatically requested for review for PRs that changes code
|
||||||
# that they own.
|
# that they own.
|
||||||
* @ankitnayan
|
* @ankitnayan
|
||||||
/frontend/ @palash-signoz @pranshuchittora
|
/frontend/ @palashgdev @pranshuchittora
|
||||||
/deploy/ @prashant-shahi
|
/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
|
run: cd frontend && yarn install
|
||||||
- name: Run ESLint
|
- name: Run ESLint
|
||||||
run: cd frontend && npm run lint
|
run: cd frontend && npm run lint
|
||||||
|
- name: Run Jest
|
||||||
|
run: cd frontend && npm run jest
|
||||||
- name: TSC
|
- name: TSC
|
||||||
run: yarn tsc
|
run: yarn tsc
|
||||||
working-directory: ./frontend
|
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
|
name: Playwright Tests
|
||||||
on:
|
on: [pull_request]
|
||||||
deployment_status:
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
playwright:
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: frontend
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event.deployment_status.state == 'success'
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: "14.x"
|
node-version: "16.x"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: CI=1 yarn install
|
||||||
- name: Install Playwright
|
- name: Install Playwright
|
||||||
run: npx playwright install --with-deps
|
run: npx playwright install --with-deps
|
||||||
- name: Run Playwright tests
|
- name: Run Playwright tests
|
||||||
run: npm run test:e2e
|
run: yarn playwright
|
||||||
env:
|
env:
|
||||||
# This might depend on your test-runner/language binding
|
# 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:
|
services:
|
||||||
clickhouse:
|
clickhouse:
|
||||||
image: yandex/clickhouse-server:21.12.3.32
|
image: clickhouse/clickhouse-server:22.4.5-alpine
|
||||||
# ports:
|
# ports:
|
||||||
# - "9000:9000"
|
# - "9000:9000"
|
||||||
# - "8123:8123"
|
# - "8123:8123"
|
||||||
volumes:
|
volumes:
|
||||||
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
|
- ./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/
|
- ./data/clickhouse/:/var/lib/clickhouse/
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
@ -37,7 +39,7 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.8.2
|
image: signoz/query-service:0.9.0
|
||||||
command: ["-config=/root/config/prometheus.yml"]
|
command: ["-config=/root/config/prometheus.yml"]
|
||||||
# ports:
|
# ports:
|
||||||
# - "6060:6060" # pprof port
|
# - "6060:6060" # pprof port
|
||||||
@ -65,7 +67,7 @@ services:
|
|||||||
- clickhouse
|
- clickhouse
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:0.8.2
|
image: signoz/frontend:0.9.0
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
@ -78,7 +80,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector:
|
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"]
|
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||||
@ -104,7 +106,7 @@ services:
|
|||||||
- clickhouse
|
- clickhouse
|
||||||
|
|
||||||
otel-collector-metrics:
|
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"]
|
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||||
|
@ -12,7 +12,7 @@ receivers:
|
|||||||
grpc:
|
grpc:
|
||||||
thrift_http:
|
thrift_http:
|
||||||
hostmetrics:
|
hostmetrics:
|
||||||
collection_interval: 30s
|
collection_interval: 60s
|
||||||
scrapers:
|
scrapers:
|
||||||
cpu:
|
cpu:
|
||||||
load:
|
load:
|
||||||
@ -22,7 +22,8 @@ receivers:
|
|||||||
network:
|
network:
|
||||||
processors:
|
processors:
|
||||||
batch:
|
batch:
|
||||||
send_batch_size: 1000
|
send_batch_size: 10000
|
||||||
|
send_batch_max_size: 11000
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
signozspanmetrics/prometheus:
|
signozspanmetrics/prometheus:
|
||||||
metrics_exporter: prometheus
|
metrics_exporter: prometheus
|
||||||
|
@ -9,12 +9,13 @@ receivers:
|
|||||||
config:
|
config:
|
||||||
scrape_configs:
|
scrape_configs:
|
||||||
- job_name: "otel-collector"
|
- job_name: "otel-collector"
|
||||||
scrape_interval: 30s
|
scrape_interval: 60s
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: ["otel-collector:8889"]
|
- targets: ["otel-collector:8889"]
|
||||||
processors:
|
processors:
|
||||||
batch:
|
batch:
|
||||||
send_batch_size: 1000
|
send_batch_size: 10000
|
||||||
|
send_batch_max_size: 11000
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
# memory_limiter:
|
# memory_limiter:
|
||||||
# # 80% of maximum memory up to 2G
|
# # 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:
|
services:
|
||||||
clickhouse:
|
clickhouse:
|
||||||
image: yandex/clickhouse-server:21.12.3.32
|
image: clickhouse/clickhouse-server:22.4.5-alpine
|
||||||
# ports:
|
# ports:
|
||||||
# - "9000:9000"
|
# - "9000:9000"
|
||||||
# - "8123:8123"
|
# - "8123:8123"
|
||||||
volumes:
|
volumes:
|
||||||
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
|
- ./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/
|
- ./data/clickhouse/:/var/lib/clickhouse/
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
logging:
|
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`
|
# 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:
|
query-service:
|
||||||
image: signoz/query-service:0.8.2
|
image: signoz/query-service:0.9.0
|
||||||
container_name: query-service
|
container_name: query-service
|
||||||
command: ["-config=/root/config/prometheus.yml"]
|
command: ["-config=/root/config/prometheus.yml"]
|
||||||
# ports:
|
# ports:
|
||||||
@ -63,7 +65,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:0.8.2
|
image: signoz/frontend:0.9.0
|
||||||
container_name: frontend
|
container_name: frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@ -75,7 +77,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector:
|
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"]
|
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||||
@ -96,7 +98,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
otel-collector-metrics:
|
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"]
|
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||||
|
@ -12,7 +12,7 @@ receivers:
|
|||||||
grpc:
|
grpc:
|
||||||
thrift_http:
|
thrift_http:
|
||||||
hostmetrics:
|
hostmetrics:
|
||||||
collection_interval: 30s
|
collection_interval: 60s
|
||||||
scrapers:
|
scrapers:
|
||||||
cpu:
|
cpu:
|
||||||
load:
|
load:
|
||||||
@ -22,7 +22,8 @@ receivers:
|
|||||||
network:
|
network:
|
||||||
processors:
|
processors:
|
||||||
batch:
|
batch:
|
||||||
send_batch_size: 1000
|
send_batch_size: 10000
|
||||||
|
send_batch_max_size: 11000
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
signozspanmetrics/prometheus:
|
signozspanmetrics/prometheus:
|
||||||
metrics_exporter: prometheus
|
metrics_exporter: prometheus
|
||||||
|
@ -9,12 +9,13 @@ receivers:
|
|||||||
config:
|
config:
|
||||||
scrape_configs:
|
scrape_configs:
|
||||||
- job_name: "otel-collector"
|
- job_name: "otel-collector"
|
||||||
scrape_interval: 30s
|
scrape_interval: 60s
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: ["otel-collector:8889"]
|
- targets: ["otel-collector:8889"]
|
||||||
processors:
|
processors:
|
||||||
batch:
|
batch:
|
||||||
send_batch_size: 1000
|
send_batch_size: 10000
|
||||||
|
send_batch_max_size: 11000
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
# memory_limiter:
|
# memory_limiter:
|
||||||
# # 80% of maximum memory up to 2G
|
# # 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_buffers 16 8k;
|
||||||
gzip_http_version 1.1;
|
gzip_http_version 1.1;
|
||||||
|
|
||||||
|
# to handle uri issue 414 from nginx
|
||||||
|
client_max_body_size 24M;
|
||||||
|
|
||||||
|
large_client_header_buffers 8 16k;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
if ( $uri = '/index.html' ) {
|
if ( $uri = '/index.html' ) {
|
||||||
add_header Cache-Control no-store always;
|
add_header Cache-Control no-store always;
|
||||||
|
@ -36,9 +36,9 @@ is_mac() {
|
|||||||
[[ $OSTYPE == darwin* ]]
|
[[ $OSTYPE == darwin* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
is_arm64(){
|
# is_arm64(){
|
||||||
[[ `uname -m` == 'arm64' ]]
|
# [[ `uname -m` == 'arm64' ]]
|
||||||
}
|
# }
|
||||||
|
|
||||||
check_os() {
|
check_os() {
|
||||||
if is_mac; then
|
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 "🔴 The containers didn't seem to start correctly. Please run the following command to check containers that may have errored out:"
|
||||||
echo ""
|
echo ""
|
||||||
if is_arm64; then
|
echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml ps -a"
|
||||||
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 "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"
|
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 ""
|
||||||
echo -e "\n🟡 Pulling the latest container images for SigNoz.\n"
|
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.yaml pull
|
||||||
$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 ""
|
||||||
echo "🟡 Starting the SigNoz containers. It may take a few minutes ..."
|
echo "🟡 Starting the SigNoz containers. It may take a few minutes ..."
|
||||||
echo
|
echo
|
||||||
# The docker-compose command does some nasty stuff for the `--detach` functionality. So we add a `|| true` so that the
|
# 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.
|
# 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.yaml up --detach --remove-orphans || true
|
||||||
$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
|
wait_for_containers_start 60
|
||||||
echo ""
|
echo ""
|
||||||
@ -510,11 +498,7 @@ else
|
|||||||
echo -e "🟢 Your frontend is running on http://localhost:3301"
|
echo -e "🟢 Your frontend is running on http://localhost:3301"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if is_arm64; then
|
echo "ℹ️ To bring down SigNoz and clean volumes : $sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml down -v"
|
||||||
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 ""
|
||||||
echo "+++++++++++++++++++++++++++++++++++++++++++++++++"
|
echo "+++++++++++++++++++++++++++++++++++++++++++++++++"
|
||||||
|
@ -2,3 +2,4 @@
|
|||||||
* Adds custom matchers from the react testing library to all tests
|
* Adds custom matchers from the react testing library to all tests
|
||||||
*/
|
*/
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
|
import 'jest-styled-components';
|
||||||
|
@ -159,6 +159,7 @@
|
|||||||
"husky": "^7.0.4",
|
"husky": "^7.0.4",
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
"jest-playwright-preset": "^1.7.0",
|
"jest-playwright-preset": "^1.7.0",
|
||||||
|
"jest-styled-components": "^7.0.8",
|
||||||
"less-plugin-npm-import": "^2.1.0",
|
"less-plugin-npm-import": "^2.1.0",
|
||||||
"lint-staged": "^12.3.7",
|
"lint-staged": "^12.3.7",
|
||||||
"portfinder-sync": "^0.0.2",
|
"portfinder-sync": "^0.0.2",
|
||||||
|
@ -11,7 +11,7 @@ const config: PlaywrightTestConfig = {
|
|||||||
testDir: './tests',
|
testDir: './tests',
|
||||||
use: {
|
use: {
|
||||||
trace: 'retain-on-failure',
|
trace: 'retain-on-failure',
|
||||||
baseURL: process.env.FRONTEND_API_ENDPOINT,
|
baseURL: process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3301',
|
||||||
},
|
},
|
||||||
updateSnapshots: 'all',
|
updateSnapshots: 'all',
|
||||||
fullyParallel: false,
|
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 { AxiosError } from 'axios';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import {
|
import {
|
||||||
|
TagKeyProps,
|
||||||
TagKeysPayloadProps,
|
TagKeysPayloadProps,
|
||||||
TagValueProps,
|
TagValueProps,
|
||||||
TagValuesPayloadProps,
|
TagValuesPayloadProps,
|
||||||
} from 'types/api/metrics/getResourceAttributes';
|
} from 'types/api/metrics/getResourceAttributes';
|
||||||
|
|
||||||
export const getResourceAttributesTagKeys = async (): Promise<
|
export const getResourceAttributesTagKeys = async (
|
||||||
SuccessResponse<TagKeysPayloadProps> | ErrorResponse
|
props: TagKeyProps,
|
||||||
> => {
|
): Promise<SuccessResponse<TagKeysPayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(
|
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 {
|
return {
|
||||||
@ -32,7 +35,7 @@ export const getResourceAttributesTagValues = async (
|
|||||||
): Promise<SuccessResponse<TagValuesPayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<TagValuesPayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(
|
const response = await axios.get(
|
||||||
`/metrics/autocomplete/tagValue?metricName=signoz_calls_total&tagKey=${props}`,
|
`/metrics/autocomplete/tagValue?metricName=${props.metricName}&tagKey=${props.tagKey}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,38 +1,46 @@
|
|||||||
import MEditor from '@monaco-editor/react';
|
import MEditor, { EditorProps } from '@monaco-editor/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
function Editor({
|
function Editor({
|
||||||
value,
|
value,
|
||||||
language = 'yaml',
|
language,
|
||||||
onChange,
|
onChange,
|
||||||
readOnly = false,
|
readOnly,
|
||||||
}: EditorProps): JSX.Element {
|
height,
|
||||||
|
options,
|
||||||
|
}: MEditorProps): JSX.Element {
|
||||||
|
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
return (
|
return (
|
||||||
<MEditor
|
<MEditor
|
||||||
theme="vs-dark"
|
theme={isDarkMode ? 'vs-dark' : 'vs-light'}
|
||||||
language={language}
|
language={language}
|
||||||
value={value}
|
value={value}
|
||||||
options={{ fontSize: 16, automaticLayout: true, readOnly }}
|
options={{ fontSize: 16, automaticLayout: true, readOnly, ...options }}
|
||||||
height="40vh"
|
height={height}
|
||||||
onChange={(newValue): void => {
|
onChange={(newValue): void => {
|
||||||
if (newValue) {
|
if (typeof newValue === 'string') onChange(newValue);
|
||||||
onChange(newValue);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditorProps {
|
interface MEditorProps {
|
||||||
value: string;
|
value: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
height?: string;
|
||||||
|
options?: EditorProps['options'];
|
||||||
}
|
}
|
||||||
|
|
||||||
Editor.defaultProps = {
|
Editor.defaultProps = {
|
||||||
language: undefined,
|
language: 'yaml',
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
|
height: '40vh',
|
||||||
|
options: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Editor;
|
export default Editor;
|
||||||
|
@ -22,7 +22,6 @@ const getOrCreateLegendList = (
|
|||||||
listContainer.style.height = '100%';
|
listContainer.style.height = '100%';
|
||||||
listContainer.style.flexWrap = 'wrap';
|
listContainer.style.flexWrap = 'wrap';
|
||||||
listContainer.style.justifyContent = 'center';
|
listContainer.style.justifyContent = 'center';
|
||||||
|
|
||||||
legendContainer?.appendChild(listContainer);
|
legendContainer?.appendChild(listContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,11 +182,10 @@ function Graph({
|
|||||||
};
|
};
|
||||||
const chartHasData = hasData(data);
|
const chartHasData = hasData(data);
|
||||||
const chartPlugins = [];
|
const chartPlugins = [];
|
||||||
if (chartHasData) {
|
|
||||||
chartPlugins.push(legend(name, data.datasets.length > 3));
|
if (!chartHasData) chartPlugins.push(emptyGraph);
|
||||||
} else {
|
chartPlugins.push(legend(name, data.datasets.length > 3));
|
||||||
chartPlugins.push(emptyGraph);
|
|
||||||
}
|
|
||||||
lineChartRef.current = new Chart(chartRef.current, {
|
lineChartRef.current = new Chart(chartRef.current, {
|
||||||
type,
|
type,
|
||||||
data,
|
data,
|
||||||
|
@ -109,14 +109,14 @@ export const useXAxisTimeUnit = (data: Chart['data']): IAxisTimeConfig => {
|
|||||||
let minTime = Number.POSITIVE_INFINITY;
|
let minTime = Number.POSITIVE_INFINITY;
|
||||||
let maxTime = Number.NEGATIVE_INFINITY;
|
let maxTime = Number.NEGATIVE_INFINITY;
|
||||||
data?.labels?.forEach((timeStamp: unknown): void => {
|
data?.labels?.forEach((timeStamp: unknown): void => {
|
||||||
const getTimeStamp = (time: string | number): Date | number | string => {
|
const getTimeStamp = (time: Date | number): Date | number | string => {
|
||||||
if (typeof timeStamp === 'string') {
|
if (time instanceof Date) {
|
||||||
return Date.parse(timeStamp);
|
return Date.parse(time.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return time;
|
return time;
|
||||||
};
|
};
|
||||||
const time = getTimeStamp(timeStamp as string | number);
|
const time = getTimeStamp(timeStamp as Date | number);
|
||||||
|
|
||||||
minTime = Math.min(parseInt(time.toString(), 10), minTime);
|
minTime = Math.min(parseInt(time.toString(), 10), minTime);
|
||||||
maxTime = Math.max(parseInt(time.toString(), 10), maxTime);
|
maxTime = Math.max(parseInt(time.toString(), 10), maxTime);
|
||||||
|
@ -7,13 +7,17 @@ export const getYAxisFormattedValue = (
|
|||||||
let decimalPrecision: number | undefined;
|
let decimalPrecision: number | undefined;
|
||||||
const parsedValue = getValueFormat(format)(
|
const parsedValue = getValueFormat(format)(
|
||||||
parseFloat(value),
|
parseFloat(value),
|
||||||
undefined,
|
12,
|
||||||
undefined,
|
12,
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const decimalSplitted = parsedValue.text.split('.');
|
const decimalSplitted = parsedValue.text.split('.');
|
||||||
if (decimalSplitted.length === 1) {
|
if (
|
||||||
|
decimalSplitted.length === 1 ||
|
||||||
|
parseFloat(parsedValue.text) === parseInt(parsedValue.text, 10)
|
||||||
|
) {
|
||||||
decimalPrecision = 0;
|
decimalPrecision = 0;
|
||||||
} else {
|
} else {
|
||||||
const decimalDigits = decimalSplitted[1].split('');
|
const decimalDigits = decimalSplitted[1].split('');
|
||||||
|
@ -2,8 +2,102 @@
|
|||||||
|
|
||||||
exports[`Not Found page test should render Not Found page without errors 1`] = `
|
exports[`Not Found page test should render Not Found page without errors 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
.c3 {
|
||||||
class="sc-gsDKAQ cLXpIa"
|
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="c0"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
fill="none"
|
fill="none"
|
||||||
@ -272,21 +366,21 @@ exports[`Not Found page test should render Not Found page without errors 1`] = `
|
|||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
<div
|
<div
|
||||||
class="sc-hKwDye foaleg"
|
class="c1"
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
class="sc-dkPtRN fcyVIq"
|
class="c2"
|
||||||
>
|
>
|
||||||
Ah, seems like we reached a dead end!
|
Ah, seems like we reached a dead end!
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
class="sc-dkPtRN fcyVIq"
|
class="c2"
|
||||||
>
|
>
|
||||||
Page Not Found
|
Page Not Found
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
class="sc-bdvvtL dbTZkj"
|
class="c3"
|
||||||
href="/application"
|
href="/application"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
|
@ -10,9 +10,11 @@ function TextToolTip({ text, url }: TextToolTipProps): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{`${text} `}
|
{`${text} `}
|
||||||
<a href={url} rel="noopener noreferrer" target="_blank">
|
{url && (
|
||||||
here
|
<a href={url} rel="noopener noreferrer" target="_blank">
|
||||||
</a>
|
here
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@ -22,8 +24,11 @@ function TextToolTip({ text, url }: TextToolTipProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextToolTip.defaultProps = {
|
||||||
|
url: '',
|
||||||
|
};
|
||||||
interface TextToolTipProps {
|
interface TextToolTipProps {
|
||||||
url: string;
|
url?: string;
|
||||||
text: 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',
|
ALL_DASHBOARD: '/dashboard',
|
||||||
DASHBOARD: '/dashboard/:dashboardId',
|
DASHBOARD: '/dashboard/:dashboardId',
|
||||||
DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId',
|
DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId',
|
||||||
EDIT_ALERTS: '/alerts/edit/:ruleId',
|
EDIT_ALERTS: '/alerts/edit',
|
||||||
LIST_ALL_ALERT: '/alerts',
|
LIST_ALL_ALERT: '/alerts',
|
||||||
ALERTS_NEW: '/alerts/new',
|
ALERTS_NEW: '/alerts/new',
|
||||||
ALL_CHANNELS: '/settings/channels',
|
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 { useQueries } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
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 { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
import { NotFoundContainer, TimeContainer } from './styles';
|
import { NotFoundContainer, TimeContainer } from './styles';
|
||||||
@ -57,7 +57,10 @@ function FullView({
|
|||||||
time: timePreferenceType,
|
time: timePreferenceType,
|
||||||
): { min: string | number; max: string | number } => {
|
): { min: string | number; max: string | number } => {
|
||||||
if (time === 'GLOBAL_TIME') {
|
if (time === 'GLOBAL_TIME') {
|
||||||
const minMax = GetMinMax(globalSelectedTime);
|
const minMax = GetMinMax(globalSelectedTime, [
|
||||||
|
minTime / 1000000,
|
||||||
|
maxTime / 1000000,
|
||||||
|
]);
|
||||||
return {
|
return {
|
||||||
min: convertToNanoSecondsToSecond(minMax.minTime / 1000),
|
min: convertToNanoSecondsToSecond(minMax.minTime / 1000),
|
||||||
max: convertToNanoSecondsToSecond(minMax.maxTime / 1000),
|
max: convertToNanoSecondsToSecond(minMax.maxTime / 1000),
|
||||||
@ -170,7 +173,7 @@ function FullView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface FullViewProps {
|
interface FullViewProps {
|
||||||
widget: Widgets;
|
widget: PromQLWidgets;
|
||||||
fullViewOptions?: boolean;
|
fullViewOptions?: boolean;
|
||||||
onClickHandler?: GraphOnClickHandler;
|
onClickHandler?: GraphOnClickHandler;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import { Typography } from 'antd';
|
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 Spinner from 'components/Spinner';
|
||||||
import GridGraphComponent from 'container/GridGraphComponent';
|
import GridGraphComponent from 'container/GridGraphComponent';
|
||||||
import getChartData from 'lib/getChartData';
|
import getChartData from 'lib/getChartData';
|
||||||
import GetMaxMinTime from 'lib/getMaxMinTime';
|
|
||||||
import GetStartAndEndTime from 'lib/getStartAndEndTime';
|
|
||||||
import isEmpty from 'lodash-es/isEmpty';
|
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 { Layout } from 'react-grid-layout';
|
||||||
import { useQueries } from 'react-query';
|
|
||||||
import { connect, useSelector } from 'react-redux';
|
import { connect, useSelector } from 'react-redux';
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
import { ThunkDispatch } from 'redux-thunk';
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
@ -16,15 +14,17 @@ import {
|
|||||||
DeleteWidget,
|
DeleteWidget,
|
||||||
DeleteWidgetProps,
|
DeleteWidgetProps,
|
||||||
} from 'store/actions/dashboard/deleteWidget';
|
} from 'store/actions/dashboard/deleteWidget';
|
||||||
|
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import { GlobalTime } from 'types/actions/globalTime';
|
import { GlobalTime } from 'types/actions/globalTime';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
import { LayoutProps } from '..';
|
import { LayoutProps } from '..';
|
||||||
import EmptyWidget from '../EmptyWidget';
|
import EmptyWidget from '../EmptyWidget';
|
||||||
import WidgetHeader from '../WidgetHeader';
|
import WidgetHeader from '../WidgetHeader';
|
||||||
import FullView from './FullView';
|
import FullView from './FullView/index.metricsBuilder';
|
||||||
import { ErrorContainer, FullViewContainer, Modal } from './styles';
|
import { ErrorContainer, FullViewContainer, Modal } from './styles';
|
||||||
|
|
||||||
function GridCardGraph({
|
function GridCardGraph({
|
||||||
@ -35,60 +35,118 @@ function GridCardGraph({
|
|||||||
layout = [],
|
layout = [],
|
||||||
setLayout,
|
setLayout,
|
||||||
}: GridCardGraphProps): JSX.Element {
|
}: GridCardGraphProps): JSX.Element {
|
||||||
|
const [state, setState] = useState<GridCardGraphState>({
|
||||||
|
loading: true,
|
||||||
|
errorMessage: '',
|
||||||
|
error: false,
|
||||||
|
payload: undefined,
|
||||||
|
});
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const [modal, setModal] = useState(false);
|
const [modal, setModal] = useState(false);
|
||||||
|
const [deleteModal, setDeleteModal] = useState(false);
|
||||||
|
|
||||||
const { minTime, maxTime } = useSelector<AppState, GlobalTime>(
|
const { minTime, maxTime } = useSelector<AppState, GlobalTime>(
|
||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
const [deleteModal, setDeleteModal] = useState(false);
|
const { selectedTime: globalSelectedInterval } = useSelector<
|
||||||
|
AppState,
|
||||||
|
GlobalReducer
|
||||||
|
>((state) => state.globalTime);
|
||||||
|
|
||||||
const getMaxMinTime = GetMaxMinTime({
|
// const getMaxMinTime = GetMaxMinTime({
|
||||||
graphType: widget?.panelTypes,
|
// graphType: widget?.panelTypes,
|
||||||
maxTime,
|
// maxTime,
|
||||||
minTime,
|
// minTime,
|
||||||
});
|
// });
|
||||||
|
|
||||||
const { start, end } = GetStartAndEndTime({
|
// const { start, end } = GetStartAndEndTime({
|
||||||
type: widget?.timePreferance,
|
// type: widget?.timePreferance,
|
||||||
maxTime: getMaxMinTime.maxTime,
|
// maxTime: getMaxMinTime.maxTime,
|
||||||
minTime: getMaxMinTime.minTime,
|
// minTime: getMaxMinTime.minTime,
|
||||||
});
|
// });
|
||||||
|
|
||||||
const queryLength = widget?.query?.filter((e) => e.query.length !== 0) || [];
|
// const queryLength = widget?.query?.filter((e) => e.query.length !== 0) || [];
|
||||||
|
|
||||||
const response = useQueries(
|
// const response = useQueries(
|
||||||
queryLength?.map((query) => {
|
// queryLength?.map((query) => {
|
||||||
return {
|
// return {
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
// // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
queryFn: () => {
|
// queryFn: () => {
|
||||||
return getQueryResult({
|
// return getQueryResult({
|
||||||
end,
|
// end,
|
||||||
query: query?.query,
|
// query: query?.query,
|
||||||
start,
|
// start,
|
||||||
step: '60',
|
// 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 isError = response.error;
|
||||||
|
|
||||||
|
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 =
|
setState((state) => ({
|
||||||
response.find((e) => e?.data?.statusCode !== 200) !== undefined ||
|
...state,
|
||||||
response.some((e) => e.isError === true);
|
loading: false,
|
||||||
|
payload: chartDataSet,
|
||||||
const isLoading = response.some((e) => e.isLoading === true);
|
}));
|
||||||
|
}
|
||||||
const errorMessage = response.find((e) => e.data?.error !== null)?.data?.error;
|
} catch (error) {
|
||||||
|
setState((state) => ({
|
||||||
const data = response.map((responseOfQuery) =>
|
...state,
|
||||||
responseOfQuery?.data?.payload?.result.map((e, index) => ({
|
error: true,
|
||||||
query: queryLength[index]?.query,
|
errorMessage: (error as AxiosError).toString(),
|
||||||
queryData: e,
|
loading: false,
|
||||||
legend: queryLength[index]?.legend,
|
}));
|
||||||
})),
|
}
|
||||||
);
|
})();
|
||||||
|
}, [widget, maxTime, minTime, globalSelectedInterval]);
|
||||||
|
|
||||||
const onToggleModal = useCallback(
|
const onToggleModal = useCallback(
|
||||||
(func: React.Dispatch<React.SetStateAction<boolean>>) => {
|
(func: React.Dispatch<React.SetStateAction<boolean>>) => {
|
||||||
@ -144,14 +202,7 @@ function GridCardGraph({
|
|||||||
|
|
||||||
const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget);
|
const isEmptyLayout = widget?.id === 'empty' || isEmpty(widget);
|
||||||
|
|
||||||
if (isLoading) {
|
if (state.error && !isEmptyLayout) {
|
||||||
return <Spinner height="20vh" tip="Loading..." />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(isError || data === undefined || data[0] === undefined) &&
|
|
||||||
!isEmptyLayout
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{getModals()}
|
{getModals()}
|
||||||
@ -163,18 +214,17 @@ function GridCardGraph({
|
|||||||
onDelete={(): void => onToggleModal(setDeleteModal)}
|
onDelete={(): void => onToggleModal(setDeleteModal)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ErrorContainer>{errorMessage}</ErrorContainer>
|
<ErrorContainer>{state.errorMessage}</ErrorContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartData = getChartData({
|
if (
|
||||||
queryData: data.map((e) => ({
|
(state.loading === true || state.payload === undefined) &&
|
||||||
query: e?.map((e) => e.query).join(' ') || '',
|
!isEmptyLayout
|
||||||
queryData: e?.map((e) => e.queryData) || [],
|
) {
|
||||||
legend: e?.map((e) => e.legend).join('') || '',
|
return <Spinner height="20vh" tip="Loading..." />;
|
||||||
})),
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
@ -203,11 +253,11 @@ function GridCardGraph({
|
|||||||
|
|
||||||
{!isEmptyLayout && getModals()}
|
{!isEmptyLayout && getModals()}
|
||||||
|
|
||||||
{!isEmpty(widget) && (
|
{!isEmpty(widget) && !!state.payload && (
|
||||||
<GridGraphComponent
|
<GridGraphComponent
|
||||||
{...{
|
{...{
|
||||||
GRAPH_TYPES: widget.panelTypes,
|
GRAPH_TYPES: widget.panelTypes,
|
||||||
data: chartData,
|
data: state.payload,
|
||||||
isStacked: widget.isStacked,
|
isStacked: widget.isStacked,
|
||||||
opacity: widget.opacity,
|
opacity: widget.opacity,
|
||||||
title: ' ', // empty title to accommodate absolutely positioned widget header
|
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 {
|
interface DispatchProps {
|
||||||
deleteWidget: ({
|
deleteWidget: ({
|
||||||
widgetId,
|
widgetId,
|
||||||
|
@ -113,20 +113,21 @@ function GridGraph(props: Props): JSX.Element {
|
|||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
loading: true,
|
loading: true,
|
||||||
}));
|
}));
|
||||||
|
const updatedDashboard: Dashboard = {
|
||||||
|
...selectedDashboard,
|
||||||
|
data: {
|
||||||
|
title: data.title,
|
||||||
|
description: data.description,
|
||||||
|
name: data.name,
|
||||||
|
tags: data.tags,
|
||||||
|
widgets: data.widgets,
|
||||||
|
layout,
|
||||||
|
},
|
||||||
|
uuid: selectedDashboard.uuid,
|
||||||
|
};
|
||||||
// Save layout only when users has the has the permission to do so.
|
// Save layout only when users has the has the permission to do so.
|
||||||
if (saveLayoutPermission) {
|
if (saveLayoutPermission) {
|
||||||
const response = await updateDashboardApi({
|
const response = await updateDashboardApi(updatedDashboard);
|
||||||
data: {
|
|
||||||
title: data.title,
|
|
||||||
description: data.description,
|
|
||||||
name: data.name,
|
|
||||||
tags: data.tags,
|
|
||||||
widgets: data.widgets,
|
|
||||||
layout,
|
|
||||||
},
|
|
||||||
uuid: selectedDashboard.uuid,
|
|
||||||
});
|
|
||||||
if (response.statusCode === 200) {
|
if (response.statusCode === 200) {
|
||||||
setSaveLayoutState((state) => ({
|
setSaveLayoutState((state) => ({
|
||||||
...state,
|
...state,
|
||||||
@ -134,6 +135,10 @@ function GridGraph(props: Props): JSX.Element {
|
|||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
loading: false,
|
loading: false,
|
||||||
}));
|
}));
|
||||||
|
dispatch({
|
||||||
|
type: UPDATE_DASHBOARD,
|
||||||
|
payload: updatedDashboard,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setSaveLayoutState((state) => ({
|
setSaveLayoutState((state) => ({
|
||||||
...state,
|
...state,
|
||||||
@ -153,8 +158,9 @@ function GridGraph(props: Props): JSX.Element {
|
|||||||
data.tags,
|
data.tags,
|
||||||
data.title,
|
data.title,
|
||||||
data.widgets,
|
data.widgets,
|
||||||
|
dispatch,
|
||||||
saveLayoutPermission,
|
saveLayoutPermission,
|
||||||
selectedDashboard.uuid,
|
selectedDashboard,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -218,7 +224,7 @@ function GridGraph(props: Props): JSX.Element {
|
|||||||
const onLayoutChangeHandler = async (layout: Layout[]): Promise<void> => {
|
const onLayoutChangeHandler = async (layout: Layout[]): Promise<void> => {
|
||||||
setLayoutFunction(layout);
|
setLayoutFunction(layout);
|
||||||
|
|
||||||
await onLayoutSaveHandler(layout);
|
// await onLayoutSaveHandler(layout);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAddPanelHandler = useCallback(() => {
|
const onAddPanelHandler = useCallback(() => {
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
import { notification } from 'antd';
|
import { notification } from 'antd';
|
||||||
import updateDashboardApi from 'api/dashboard/update';
|
import updateDashboardApi from 'api/dashboard/update';
|
||||||
|
import {
|
||||||
|
ClickHouseQueryTemplate,
|
||||||
|
PromQLQueryTemplate,
|
||||||
|
QueryBuilderQueryTemplate,
|
||||||
|
} from 'constants/dashboard';
|
||||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||||
|
import GetQueryName from 'lib/query/GetQueryName';
|
||||||
import { Layout } from 'react-grid-layout';
|
import { Layout } from 'react-grid-layout';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
|
|
||||||
export const UpdateDashboard = async ({
|
export const UpdateDashboard = async ({
|
||||||
data,
|
data,
|
||||||
@ -29,14 +36,32 @@ export const UpdateDashboard = async ({
|
|||||||
nullZeroValues: '',
|
nullZeroValues: '',
|
||||||
opacity: '',
|
opacity: '',
|
||||||
panelTypes: graphType,
|
panelTypes: graphType,
|
||||||
query: [
|
query: {
|
||||||
{
|
queryType: EQueryType.QUERY_BUILDER,
|
||||||
query: '',
|
promQL: [
|
||||||
legend: '',
|
{
|
||||||
|
name: GetQueryName([]) || '',
|
||||||
|
...PromQLQueryTemplate,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
clickHouse: [
|
||||||
|
{
|
||||||
|
name: GetQueryName([]) || '',
|
||||||
|
...ClickHouseQueryTemplate,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
metricsBuilder: {
|
||||||
|
formulas: [],
|
||||||
|
queryBuilder: [
|
||||||
|
{
|
||||||
|
name: GetQueryName([]) || '',
|
||||||
|
...QueryBuilderQueryTemplate,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
queryData: {
|
queryData: {
|
||||||
data: [],
|
data: { queryData: [] },
|
||||||
error: false,
|
error: false,
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -11,7 +11,6 @@ import React, { useCallback, useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { UseQueryResult } from 'react-query';
|
import { UseQueryResult } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { generatePath } from 'react-router-dom';
|
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { Alerts } from 'types/api/alerts/getAll';
|
import { Alerts } from 'types/api/alerts/getAll';
|
||||||
@ -51,11 +50,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
const [notifications, Element] = notification.useNotification();
|
const [notifications, Element] = notification.useNotification();
|
||||||
|
|
||||||
const onEditHandler = (id: string): void => {
|
const onEditHandler = (id: string): void => {
|
||||||
history.push(
|
history.push(`${ROUTES.EDIT_ALERTS}?ruleId=${id}`);
|
||||||
generatePath(ROUTES.EDIT_ALERTS, {
|
|
||||||
ruleId: id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: ColumnsType<Alerts> = [
|
const columns: ColumnsType<Alerts> = [
|
||||||
|
@ -73,10 +73,7 @@ function ImportJSON({
|
|||||||
...e,
|
...e,
|
||||||
queryData: {
|
queryData: {
|
||||||
...e.queryData,
|
...e.queryData,
|
||||||
data: e.queryData.data.map((queryData) => ({
|
data: e.queryData.data,
|
||||||
...queryData,
|
|
||||||
queryData: [],
|
|
||||||
})),
|
|
||||||
error: false,
|
error: false,
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
loading: false,
|
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) {
|
if (!searchData.length || !queries.length) {
|
||||||
return searchData;
|
return searchData;
|
||||||
}
|
}
|
||||||
|
const escapeRegExp = (regExp: string): string =>
|
||||||
|
regExp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
|
||||||
queries.forEach((query: IQueryStructure) => {
|
queries.forEach((query: IQueryStructure) => {
|
||||||
const { operator } = query;
|
const { operator } = query;
|
||||||
@ -61,7 +63,7 @@ export const executeSearchQueries = (
|
|||||||
for (const searchSpaceItem of searchSpace) {
|
for (const searchSpaceItem of searchSpace) {
|
||||||
if (searchSpaceItem)
|
if (searchSpaceItem)
|
||||||
for (const queryValue of value) {
|
for (const queryValue of value) {
|
||||||
if (searchSpaceItem.match(queryValue)) {
|
if (searchSpaceItem.match(escapeRegExp(queryValue))) {
|
||||||
return resolveOperator(true, operator);
|
return resolveOperator(true, operator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,10 @@ export const GetTagKeys = async (): Promise<IOption[]> => {
|
|||||||
// resolve(TagKeysCache);
|
// resolve(TagKeysCache);
|
||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
const { payload } = await getResourceAttributesTagKeys();
|
const { payload } = await getResourceAttributesTagKeys({
|
||||||
|
metricName: 'signoz_calls_total',
|
||||||
|
match: 'resource_',
|
||||||
|
});
|
||||||
if (!payload || !payload?.data) {
|
if (!payload || !payload?.data) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@ -32,12 +35,15 @@ export const GetTagKeys = async (): Promise<IOption[]> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const GetTagValues = async (tagKey: string): 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) {
|
if (!payload || !payload?.data) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return payload.data.filter(Boolean).map((tagValue: string) => ({
|
return payload.data.map((tagValue: string) => ({
|
||||||
label: tagValue,
|
label: tagValue,
|
||||||
value: tagValue,
|
value: tagValue,
|
||||||
}));
|
}));
|
||||||
|
@ -4,7 +4,7 @@ import React from 'react';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
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 MetricReducer from 'types/reducer/metrics';
|
||||||
|
|
||||||
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
|
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
|
||||||
@ -58,7 +58,7 @@ function DBCall({ getWidget }: DBCallProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface DBCallProps {
|
interface DBCallProps {
|
||||||
getWidget: (query: Widgets['query']) => Widgets;
|
getWidget: (query: PromQLWidgets['query']) => PromQLWidgets;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DBCall;
|
export default DBCall;
|
||||||
|
@ -4,7 +4,7 @@ import React from 'react';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
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 MetricReducer from 'types/reducer/metrics';
|
||||||
|
|
||||||
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
|
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
|
||||||
@ -29,7 +29,7 @@ function External({ getWidget }: ExternalProps): JSX.Element {
|
|||||||
widget={getWidget([
|
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)`,
|
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="%"
|
yAxisUnit="%"
|
||||||
@ -102,7 +102,7 @@ function External({ getWidget }: ExternalProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ExternalProps {
|
interface ExternalProps {
|
||||||
getWidget: (query: Widgets['query']) => Widgets;
|
getWidget: (query: PromQLWidgets['query']) => PromQLWidgets;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default External;
|
export default External;
|
||||||
|
@ -11,7 +11,7 @@ import React, { useRef } from 'react';
|
|||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
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 MetricReducer from 'types/reducer/metrics';
|
||||||
|
|
||||||
import { Card, Col, GraphContainer, GraphTitle, Row } from '../styles';
|
import { Card, Col, GraphContainer, GraphTitle, Row } from '../styles';
|
||||||
@ -248,7 +248,7 @@ function Application({ getWidget }: DashboardProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface DashboardProps {
|
interface DashboardProps {
|
||||||
getWidget: (query: Widgets['query']) => Widgets;
|
getWidget: (query: PromQLWidgets['query']) => PromQLWidgets;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Application;
|
export default Application;
|
||||||
|
@ -3,14 +3,14 @@ import ROUTES from 'constants/routes';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { generatePath, useParams } from 'react-router-dom';
|
import { generatePath, useParams } from 'react-router-dom';
|
||||||
import { useLocation } from 'react-use';
|
import { useLocation } from 'react-use';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { PromQLWidgets } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
import ResourceAttributesFilter from './ResourceAttributesFilter';
|
import ResourceAttributesFilter from './ResourceAttributesFilter';
|
||||||
import DBCall from './Tabs/DBCall';
|
import DBCall from './Tabs/DBCall';
|
||||||
import External from './Tabs/External';
|
import External from './Tabs/External';
|
||||||
import Overview from './Tabs/Overview';
|
import Overview from './Tabs/Overview';
|
||||||
|
|
||||||
const getWidget = (query: Widgets['query']): Widgets => {
|
const getWidget = (query: PromQLWidgets['query']): PromQLWidgets => {
|
||||||
return {
|
return {
|
||||||
description: '',
|
description: '',
|
||||||
id: '',
|
id: '',
|
||||||
@ -20,7 +20,7 @@ const getWidget = (query: Widgets['query']): Widgets => {
|
|||||||
panelTypes: 'TIME_SERIES',
|
panelTypes: 'TIME_SERIES',
|
||||||
query,
|
query,
|
||||||
queryData: {
|
queryData: {
|
||||||
data: [],
|
data: { queryData: [] },
|
||||||
error: false,
|
error: false,
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
loading: false,
|
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 { 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 { connect, useSelector } from 'react-redux';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
import { ThunkDispatch } from 'redux-thunk';
|
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 { AppState } from 'store/reducers';
|
||||||
import AppActions from 'types/actions';
|
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 DashboardReducer from 'types/reducer/dashboards';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import Query from './Query';
|
import {
|
||||||
import { QueryButton } from './styles';
|
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>(
|
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||||
(state) => state.dashboards,
|
(state) => state.dashboards,
|
||||||
);
|
);
|
||||||
@ -32,50 +65,232 @@ function QuerySection({ selectedTime, createQuery }: QueryProps): JSX.Element {
|
|||||||
}, [widgets, urlQuery]);
|
}, [widgets, urlQuery]);
|
||||||
|
|
||||||
const selectedWidget = getWidget() as Widgets;
|
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({
|
const queryDiff = (
|
||||||
widgetId: String(widgetId),
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
|
return cloneDeep(newState);
|
||||||
});
|
});
|
||||||
}, [createQuery, urlQuery]);
|
};
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{query.map((e, index) => (
|
<div style={{ display: 'flex' }}>
|
||||||
<Query
|
<Tabs
|
||||||
currentIndex={index}
|
type="card"
|
||||||
selectedTime={selectedTime}
|
style={{ width: '100%' }}
|
||||||
key={`${e.query} ${e.query.length}`}
|
defaultActiveKey={queryCategory.toString()}
|
||||||
preQuery={e.query}
|
activeKey={queryCategory.toString()}
|
||||||
preLegend={e.legend || ''}
|
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`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<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}
|
||||||
/>
|
/>
|
||||||
))}
|
))} */}
|
||||||
|
|
||||||
<QueryButton onClick={queryOnClickHandler} icon={<PlusOutlined />}>
|
|
||||||
Query
|
|
||||||
</QueryButton>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DispatchProps {
|
interface DispatchProps {
|
||||||
createQuery: ({
|
// createQuery: ({
|
||||||
widgetId,
|
// widgetId,
|
||||||
}: CreateQueryProps) => (dispatch: Dispatch<AppActions>) => void;
|
// }: 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 = (
|
const mapDispatchToProps = (
|
||||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||||
): DispatchProps => ({
|
): 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 {
|
interface QueryProps extends DispatchProps {
|
||||||
|
selectedGraph: GRAPH_TYPES;
|
||||||
selectedTime: timePreferance;
|
selectedTime: timePreferance;
|
||||||
|
handleUnstagedChanges: (arg0: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(QuerySection);
|
export default connect(null, mapDispatchToProps)(QuerySection);
|
||||||
|
@ -8,6 +8,7 @@ export const InputContainer = styled.div`
|
|||||||
export const Container = styled.div`
|
export const Container = styled.div`
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const QueryButton = styled(Button)`
|
export const QueryButton = styled(Button)`
|
||||||
@ -18,11 +19,15 @@ export const QueryButton = styled(Button)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const QueryWrapper = styled.div`
|
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 {
|
export const QueryBuilderWrapper = styled.div<{ isDarkMode: boolean }>`
|
||||||
width: 95%; // each child is taking 95% of the parent
|
background: ${({ isDarkMode }): string => (isDarkMode ? '#000' : '#efefef')};
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ButtonContainer = styled.div`
|
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;
|
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 (
|
return (
|
||||||
<NotFoundContainer>
|
<NotFoundContainer>
|
||||||
<Typography>No Data</Typography>
|
<Typography>No Data</Typography>
|
||||||
@ -43,7 +50,7 @@ function WidgetGraph({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const chartDataSet = getChartData({
|
const chartDataSet = getChartData({
|
||||||
queryData: queryData.data,
|
queryData: [queryData.data],
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -8,6 +8,7 @@ import { AppState } from 'store/reducers';
|
|||||||
import DashboardReducer from 'types/reducer/dashboards';
|
import DashboardReducer from 'types/reducer/dashboards';
|
||||||
|
|
||||||
import { NewWidgetProps } from '../../index';
|
import { NewWidgetProps } from '../../index';
|
||||||
|
import PlotTag from './PlotTag';
|
||||||
import { AlertIconContainer, Container, NotFoundContainer } from './styles';
|
import { AlertIconContainer, Container, NotFoundContainer } from './styles';
|
||||||
import WidgetGraphComponent from './WidgetGraph';
|
import WidgetGraphComponent from './WidgetGraph';
|
||||||
|
|
||||||
@ -35,9 +36,9 @@ function WidgetGraph({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { queryData } = selectedWidget;
|
const { queryData } = selectedWidget;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
<PlotTag queryType={selectedWidget.query.queryType} />
|
||||||
{queryData.error && (
|
{queryData.error && (
|
||||||
<AlertIconContainer color="red" title={queryData.errorMessage}>
|
<AlertIconContainer color="red" title={queryData.errorMessage}>
|
||||||
<InfoCircleOutlined />
|
<InfoCircleOutlined />
|
||||||
|
@ -7,8 +7,8 @@ export const Container = styled(Card)`
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
padding: 0;
|
padding: 1.5rem 0;
|
||||||
height: 55vh;
|
height: 57vh;
|
||||||
/* padding-bottom: 2rem; */
|
/* padding-bottom: 2rem; */
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -10,13 +10,17 @@ function LeftContainer({
|
|||||||
selectedGraph,
|
selectedGraph,
|
||||||
selectedTime,
|
selectedTime,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
|
handleUnstagedChanges,
|
||||||
}: LeftContainerProps): JSX.Element {
|
}: LeftContainerProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<WidgetGraph selectedGraph={selectedGraph} yAxisUnit={yAxisUnit} />
|
<WidgetGraph selectedGraph={selectedGraph} yAxisUnit={yAxisUnit} />
|
||||||
|
|
||||||
<QueryContainer>
|
<QueryContainer>
|
||||||
<QuerySection selectedTime={selectedTime} />
|
<QuerySection
|
||||||
|
selectedTime={selectedTime}
|
||||||
|
handleUnstagedChanges={handleUnstagedChanges}
|
||||||
|
selectedGraph={selectedGraph}
|
||||||
|
/>
|
||||||
</QueryContainer>
|
</QueryContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -24,6 +28,7 @@ function LeftContainer({
|
|||||||
|
|
||||||
interface LeftContainerProps extends NewWidgetProps {
|
interface LeftContainerProps extends NewWidgetProps {
|
||||||
selectedTime: timePreferance;
|
selectedTime: timePreferance;
|
||||||
|
handleUnstagedChanges: (arg0: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(LeftContainer);
|
export default memo(LeftContainer);
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import { Button } from 'antd';
|
import { Button, Modal, Typography } from 'antd';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
|
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
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 { generatePath, useLocation, useParams } from 'react-router-dom';
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
import { ThunkDispatch } from 'redux-thunk';
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
import { ApplySettingsToPanel, ApplySettingsToPanelProps } from 'store/actions';
|
|
||||||
import {
|
import {
|
||||||
GetQueryResults,
|
GetQueryResults,
|
||||||
GetQueryResultsProps,
|
GetQueryResultsProps,
|
||||||
@ -17,17 +16,15 @@ import {
|
|||||||
SaveDashboard,
|
SaveDashboard,
|
||||||
SaveDashboardProps,
|
SaveDashboardProps,
|
||||||
} from 'store/actions/dashboard/saveDashboard';
|
} from 'store/actions/dashboard/saveDashboard';
|
||||||
import {
|
|
||||||
UpdateQuery,
|
|
||||||
UpdateQueryProps,
|
|
||||||
} from 'store/actions/dashboard/updateQuery';
|
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
|
import { FLUSH_DASHBOARD } from 'types/actions/dashboard';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
import DashboardReducer from 'types/reducer/dashboards';
|
import DashboardReducer from 'types/reducer/dashboards';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
import LeftContainer from './LeftContainer';
|
import LeftContainer from './LeftContainer';
|
||||||
|
import QueryTypeTag from './LeftContainer/QueryTypeTag';
|
||||||
import RightContainer from './RightContainer';
|
import RightContainer from './RightContainer';
|
||||||
import TimeItems, { timePreferance } from './RightContainer/timeItems';
|
import TimeItems, { timePreferance } from './RightContainer/timeItems';
|
||||||
import {
|
import {
|
||||||
@ -36,15 +33,15 @@ import {
|
|||||||
LeftContainerWrapper,
|
LeftContainerWrapper,
|
||||||
PanelContainer,
|
PanelContainer,
|
||||||
RightContainerWrapper,
|
RightContainerWrapper,
|
||||||
|
Tag,
|
||||||
} from './styles';
|
} from './styles';
|
||||||
|
|
||||||
function NewWidget({
|
function NewWidget({
|
||||||
selectedGraph,
|
selectedGraph,
|
||||||
applySettingsToPanel,
|
|
||||||
saveSettingOfPanel,
|
saveSettingOfPanel,
|
||||||
getQueryResults,
|
getQueryResults,
|
||||||
updateQuery,
|
|
||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
|
const dispatch = useDispatch();
|
||||||
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
const { dashboards } = useSelector<AppState, DashboardReducer>(
|
||||||
(state) => state.dashboards,
|
(state) => state.dashboards,
|
||||||
);
|
);
|
||||||
@ -87,6 +84,8 @@ function NewWidget({
|
|||||||
const [selectedNullZeroValue, setSelectedNullZeroValue] = useState<string>(
|
const [selectedNullZeroValue, setSelectedNullZeroValue] = useState<string>(
|
||||||
selectedWidget?.nullZeroValues || 'zero',
|
selectedWidget?.nullZeroValues || 'zero',
|
||||||
);
|
);
|
||||||
|
const [saveModal, setSaveModal] = useState(false);
|
||||||
|
const [hasUnstagedChanges, setHasUnstagedChanges] = useState(false);
|
||||||
|
|
||||||
const getSelectedTime = useCallback(
|
const getSelectedTime = useCallback(
|
||||||
() =>
|
() =>
|
||||||
@ -116,50 +115,30 @@ function NewWidget({
|
|||||||
dashboardId,
|
dashboardId,
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
opacity,
|
|
||||||
description,
|
|
||||||
query,
|
|
||||||
selectedTime,
|
|
||||||
stacked,
|
|
||||||
title,
|
|
||||||
selectedNullZeroValue,
|
|
||||||
saveSettingOfPanel,
|
saveSettingOfPanel,
|
||||||
selectedDashboard,
|
selectedDashboard.uuid,
|
||||||
dashboardId,
|
description,
|
||||||
|
stacked,
|
||||||
|
selectedNullZeroValue,
|
||||||
|
opacity,
|
||||||
|
selectedTime.enum,
|
||||||
|
title,
|
||||||
yAxisUnit,
|
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(() => {
|
const onClickDiscardHandler = useCallback(() => {
|
||||||
|
dispatch({
|
||||||
|
type: FLUSH_DASHBOARD,
|
||||||
|
});
|
||||||
history.push(generatePath(ROUTES.DASHBOARD, { dashboardId }));
|
history.push(generatePath(ROUTES.DASHBOARD, { dashboardId }));
|
||||||
}, [dashboardId]);
|
}, [dashboardId, dispatch]);
|
||||||
|
|
||||||
const getQueryResult = useCallback(() => {
|
const getQueryResult = useCallback(() => {
|
||||||
if (selectedWidget?.id.length !== 0) {
|
if (selectedWidget?.id.length !== 0 && selectedWidget?.query) {
|
||||||
getQueryResults({
|
getQueryResults({
|
||||||
query: selectedWidget?.query || [],
|
query: selectedWidget?.query,
|
||||||
selectedTime: selectedTime.enum,
|
selectedTime: selectedTime.enum,
|
||||||
widgetId: selectedWidget?.id || '',
|
widgetId: selectedWidget?.id || '',
|
||||||
graphType: selectedGraph,
|
graphType: selectedGraph,
|
||||||
@ -182,14 +161,17 @@ function NewWidget({
|
|||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<Button onClick={onClickSaveHandler}>Save</Button>
|
<Button type="primary" onClick={(): void => setSaveModal(true)}>
|
||||||
<Button onClick={onClickApplyHandler}>Apply</Button>
|
Save
|
||||||
|
</Button>
|
||||||
|
{/* <Button onClick={onClickApplyHandler}>Apply</Button> */}
|
||||||
<Button onClick={onClickDiscardHandler}>Discard</Button>
|
<Button onClick={onClickDiscardHandler}>Discard</Button>
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
|
|
||||||
<PanelContainer>
|
<PanelContainer>
|
||||||
<LeftContainerWrapper flex={5}>
|
<LeftContainerWrapper flex={5}>
|
||||||
<LeftContainer
|
<LeftContainer
|
||||||
|
handleUnstagedChanges={setHasUnstagedChanges}
|
||||||
selectedTime={selectedTime}
|
selectedTime={selectedTime}
|
||||||
selectedGraph={selectedGraph}
|
selectedGraph={selectedGraph}
|
||||||
yAxisUnit={yAxisUnit}
|
yAxisUnit={yAxisUnit}
|
||||||
@ -218,6 +200,34 @@ function NewWidget({
|
|||||||
/>
|
/>
|
||||||
</RightContainerWrapper>
|
</RightContainerWrapper>
|
||||||
</PanelContainer>
|
</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>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -228,27 +238,19 @@ export interface NewWidgetProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface DispatchProps {
|
interface DispatchProps {
|
||||||
applySettingsToPanel: (
|
|
||||||
props: ApplySettingsToPanelProps,
|
|
||||||
) => (dispatch: Dispatch<AppActions>) => void;
|
|
||||||
saveSettingOfPanel: (
|
saveSettingOfPanel: (
|
||||||
props: SaveDashboardProps,
|
props: SaveDashboardProps,
|
||||||
) => (dispatch: Dispatch<AppActions>) => void;
|
) => (dispatch: Dispatch<AppActions>) => void;
|
||||||
getQueryResults: (
|
getQueryResults: (
|
||||||
props: GetQueryResultsProps,
|
props: GetQueryResultsProps,
|
||||||
) => (dispatch: Dispatch<AppActions>) => void;
|
) => (dispatch: Dispatch<AppActions>) => void;
|
||||||
updateQuery: (
|
|
||||||
props: UpdateQueryProps,
|
|
||||||
) => (dispatch: Dispatch<AppActions>) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = (
|
const mapDispatchToProps = (
|
||||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||||
): DispatchProps => ({
|
): DispatchProps => ({
|
||||||
applySettingsToPanel: bindActionCreators(ApplySettingsToPanel, dispatch),
|
|
||||||
saveSettingOfPanel: bindActionCreators(SaveDashboard, dispatch),
|
saveSettingOfPanel: bindActionCreators(SaveDashboard, dispatch),
|
||||||
getQueryResults: bindActionCreators(GetQueryResults, dispatch),
|
getQueryResults: bindActionCreators(GetQueryResults, dispatch),
|
||||||
updateQuery: bindActionCreators(UpdateQuery, dispatch),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type Props = DispatchProps & NewWidgetProps;
|
type Props = DispatchProps & NewWidgetProps;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Col } from 'antd';
|
import { Col, Tag as AntDTag } from 'antd';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const Container = styled.div`
|
export const Container = styled.div`
|
||||||
@ -31,3 +31,7 @@ export const ButtonContainer = styled.div`
|
|||||||
export const PanelContainer = styled.div`
|
export const PanelContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const Tag = styled(AntDTag)`
|
||||||
|
margin: 0;
|
||||||
|
`;
|
||||||
|
@ -15,6 +15,7 @@ import AppReducer from 'types/reducer/app';
|
|||||||
import menus from './menuItems';
|
import menus from './menuItems';
|
||||||
import Slack from './Slack';
|
import Slack from './Slack';
|
||||||
import {
|
import {
|
||||||
|
Name,
|
||||||
RedDot,
|
RedDot,
|
||||||
Sider,
|
Sider,
|
||||||
SlackButton,
|
SlackButton,
|
||||||
@ -103,8 +104,8 @@ function SideNav(): JSX.Element {
|
|||||||
icon={<Icon />}
|
icon={<Icon />}
|
||||||
onClick={(): void => onClickHandler(to)}
|
onClick={(): void => onClickHandler(to)}
|
||||||
>
|
>
|
||||||
<Space style={{ position: 'relative' }}>
|
<Space>
|
||||||
<Typography>{name}</Typography>
|
<Name ellipsis>{name}</Name>
|
||||||
{tags &&
|
{tags &&
|
||||||
tags.map((e) => (
|
tags.map((e) => (
|
||||||
<Tags style={{ lineHeight: '1rem' }} color="#177DDC" key={e}>
|
<Tags style={{ lineHeight: '1rem' }} color="#177DDC" key={e}>
|
||||||
|
@ -15,7 +15,7 @@ const menus: SidebarMenu[] = [
|
|||||||
{
|
{
|
||||||
Icon: BarChartOutlined,
|
Icon: BarChartOutlined,
|
||||||
to: ROUTES.APPLICATION,
|
to: ROUTES.APPLICATION,
|
||||||
name: 'Metrics',
|
name: 'Services',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Icon: AlignLeftOutlined,
|
Icon: AlignLeftOutlined,
|
||||||
|
@ -83,3 +83,9 @@ export const Tags = styled(Tag)`
|
|||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const Name = styled(Typography.Paragraph)`
|
||||||
|
&&& {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -16,6 +16,8 @@ const breadcrumbNameMap = {
|
|||||||
[ROUTES.ORG_SETTINGS]: 'Organization Settings',
|
[ROUTES.ORG_SETTINGS]: 'Organization Settings',
|
||||||
[ROUTES.MY_SETTINGS]: 'My Settings',
|
[ROUTES.MY_SETTINGS]: 'My Settings',
|
||||||
[ROUTES.ERROR_DETAIL]: 'Errors',
|
[ROUTES.ERROR_DETAIL]: 'Errors',
|
||||||
|
[ROUTES.LIST_ALL_ALERT]: 'Alerts',
|
||||||
|
[ROUTES.ALL_DASHBOARD]: 'Dashboard',
|
||||||
};
|
};
|
||||||
|
|
||||||
function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element {
|
function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element {
|
||||||
|
@ -155,10 +155,9 @@ function CheckBoxComponent(props: CheckBoxProps): JSX.Element {
|
|||||||
|
|
||||||
const isCheckBoxSelected = isUserSelected;
|
const isCheckBoxSelected = isUserSelected;
|
||||||
|
|
||||||
const TooTipOverLay = useMemo(
|
const TooTipOverLay = useMemo((): JSX.Element => <div>{keyValue}</div>, [
|
||||||
(): JSX.Element => <Typography>{keyValue}</Typography>,
|
keyValue,
|
||||||
[keyValue],
|
]);
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CheckBoxContainer>
|
<CheckBoxContainer>
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
import React from 'react';
|
import { Button, Input } from 'antd';
|
||||||
import { useSelector } from 'react-redux';
|
import React, { useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
import { AppState } from 'store/reducers';
|
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 { TraceFilterEnum, TraceReducer } from 'types/reducer/trace';
|
||||||
|
|
||||||
import CheckBoxComponent from '../Common/Checkbox';
|
import CheckBoxComponent from '../Common/Checkbox';
|
||||||
|
|
||||||
|
const { Search } = Input;
|
||||||
|
|
||||||
function CommonCheckBox(props: CommonCheckBoxProps): JSX.Element {
|
function CommonCheckBox(props: CommonCheckBoxProps): JSX.Element {
|
||||||
const { filter } = useSelector<AppState, TraceReducer>(
|
const { filter, filterDisplayValue } = useSelector<AppState, TraceReducer>(
|
||||||
(state) => state.traces,
|
(state) => state.traces,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -15,9 +22,40 @@ function CommonCheckBox(props: CommonCheckBoxProps): JSX.Element {
|
|||||||
const status = filter.get(name) || {};
|
const status = filter.get(name) || {};
|
||||||
|
|
||||||
const statusObj = Object.keys(status);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{statusObj.length > 0 && (
|
||||||
|
<Search
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={(e): void => setSearchFilter(e.target.value)}
|
||||||
|
style={{
|
||||||
|
padding: '0 3%',
|
||||||
|
}}
|
||||||
|
placeholder="Filter Values"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{statusObj
|
{statusObj
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const countA = +status[a];
|
const countA = +status[a];
|
||||||
@ -28,6 +66,15 @@ function CommonCheckBox(props: CommonCheckBoxProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
return countA - countB;
|
return countA - countB;
|
||||||
})
|
})
|
||||||
|
.filter((filter) => {
|
||||||
|
if (searchFilter.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return filter
|
||||||
|
.toLocaleLowerCase()
|
||||||
|
.includes(searchFilter.toLocaleLowerCase());
|
||||||
|
})
|
||||||
|
.filter((_, index) => index < numberOfFilters)
|
||||||
.map((e) => (
|
.map((e) => (
|
||||||
<CheckBoxComponent
|
<CheckBoxComponent
|
||||||
key={e}
|
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 { Slider } from 'antd';
|
||||||
import { Input, Slider } from 'antd';
|
|
||||||
import { SliderRangeProps } from 'antd/lib/slider';
|
import { SliderRangeProps } from 'antd/lib/slider';
|
||||||
import getFilters from 'api/trace/getFilters';
|
import getFilters from 'api/trace/getFilters';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import durationPlugin from 'dayjs/plugin/duration';
|
|
||||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
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 { useDispatch, useSelector } from 'react-redux';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import { getFilter, updateURL } from 'store/actions/trace/util';
|
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 { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import { TraceReducer } from 'types/reducer/trace';
|
import { TraceReducer } from 'types/reducer/trace';
|
||||||
|
|
||||||
import { Container, InputContainer, Text } from './styles';
|
import { Container, InputComponent, InputContainer, Text } from './styles';
|
||||||
|
import { getMs } from './util';
|
||||||
dayjs.extend(durationPlugin);
|
|
||||||
|
|
||||||
const getMs = (value: string): string => {
|
|
||||||
return parseFloat(
|
|
||||||
dayjs
|
|
||||||
.duration({
|
|
||||||
milliseconds: parseInt(value, 10) / 1000000,
|
|
||||||
})
|
|
||||||
.format('SSS'),
|
|
||||||
).toFixed(2);
|
|
||||||
};
|
|
||||||
|
|
||||||
function Duration(): JSX.Element {
|
function Duration(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
@ -77,17 +69,18 @@ function Duration(): JSX.Element {
|
|||||||
preLocalMinDuration.current = parseFloat(minDuration);
|
preLocalMinDuration.current = parseFloat(minDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
setPreMax(maxDuration);
|
setPreMax(getMs(maxDuration));
|
||||||
setPreMin(minDuration);
|
setPreMin(getMs(minDuration));
|
||||||
}, [getDuration]);
|
}, [getDuration]);
|
||||||
|
|
||||||
const defaultValue = [parseFloat(preMin), parseFloat(preMax)];
|
|
||||||
|
|
||||||
const updatedUrl = async (min: number, max: number): Promise<void> => {
|
const updatedUrl = async (min: number, max: number): Promise<void> => {
|
||||||
const preSelectedFilter = new Map(selectedFilter);
|
const preSelectedFilter = new Map(selectedFilter);
|
||||||
const preUserSelected = new Map(userSelectedFilter);
|
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({
|
const response = await getFilters({
|
||||||
end: String(globalTime.maxTime),
|
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;
|
const [min, max] = number;
|
||||||
|
|
||||||
setPreMin(min.toString());
|
setPreMin(min);
|
||||||
setPreMax(max.toString());
|
setPreMax(max);
|
||||||
};
|
};
|
||||||
|
|
||||||
const debouncedFunction = useDebouncedFn(
|
const debouncedFunction = useDebouncedFn(
|
||||||
(min, max) => {
|
(min, max) => {
|
||||||
updatedUrl(min as number, max as number);
|
updatedUrl(min as number, max as number);
|
||||||
},
|
},
|
||||||
500,
|
1500,
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -156,8 +149,8 @@ function Duration(): JSX.Element {
|
|||||||
event,
|
event,
|
||||||
) => {
|
) => {
|
||||||
const { value } = event.target;
|
const { value } = event.target;
|
||||||
const min = parseFloat(preMin);
|
const min = preMin;
|
||||||
const max = parseFloat(value) * 1000000;
|
const max = value;
|
||||||
|
|
||||||
onRangeSliderHandler([min, max]);
|
onRangeSliderHandler([min, max]);
|
||||||
debouncedFunction(min, max);
|
debouncedFunction(min, max);
|
||||||
@ -167,8 +160,9 @@ function Duration(): JSX.Element {
|
|||||||
event,
|
event,
|
||||||
) => {
|
) => {
|
||||||
const { value } = event.target;
|
const { value } = event.target;
|
||||||
const min = parseFloat(value) * 1000000;
|
const min = value;
|
||||||
const max = parseFloat(preMax);
|
const max = preMax;
|
||||||
|
|
||||||
onRangeSliderHandler([min, max]);
|
onRangeSliderHandler([min, max]);
|
||||||
debouncedFunction(min, max);
|
debouncedFunction(min, max);
|
||||||
};
|
};
|
||||||
@ -177,45 +171,48 @@ function Duration(): JSX.Element {
|
|||||||
updatedUrl(min, max);
|
updatedUrl(min, max);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TipComponent = useCallback((value) => {
|
||||||
|
if (value === undefined) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
return <div>{`${getMs(value?.toString())}ms`}</div>;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Container>
|
<Container>
|
||||||
<InputContainer>
|
<InputContainer>
|
||||||
<Text>Min</Text>
|
<Text>Min</Text>
|
||||||
</InputContainer>
|
</InputContainer>
|
||||||
<Input
|
<InputComponent
|
||||||
addonAfter="ms"
|
addonAfter="ms"
|
||||||
|
type="number"
|
||||||
onChange={onChangeMinHandler}
|
onChange={onChangeMinHandler}
|
||||||
value={getMs(preMin)}
|
value={preMin}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputContainer>
|
<InputContainer>
|
||||||
<Text>Max</Text>
|
<Text>Max</Text>
|
||||||
</InputContainer>
|
</InputContainer>
|
||||||
<Input
|
<InputComponent
|
||||||
addonAfter="ms"
|
addonAfter="ms"
|
||||||
|
type="number"
|
||||||
onChange={onChangeMaxHandler}
|
onChange={onChangeMaxHandler}
|
||||||
value={getMs(preMax)}
|
value={preMax}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<Container>
|
<Container>
|
||||||
<Slider
|
<Slider
|
||||||
defaultValue={[defaultValue[0], defaultValue[1]]}
|
min={Number(getMs(String(preLocalMinDuration.current || 0)))}
|
||||||
min={parseFloat((preLocalMinDuration.current || 0).toString())}
|
max={Number(getMs(String(preLocalMaxDuration.current || 0)))}
|
||||||
max={parseFloat((preLocalMaxDuration.current || 0).toString())}
|
|
||||||
range
|
range
|
||||||
tipFormatter={(value): JSX.Element => {
|
tipFormatter={TipComponent}
|
||||||
if (value === undefined) {
|
|
||||||
return <div />;
|
|
||||||
}
|
|
||||||
return <div>{`${getMs(value?.toString())}ms`}</div>;
|
|
||||||
}}
|
|
||||||
onChange={([min, max]): void => {
|
onChange={([min, max]): void => {
|
||||||
onRangeSliderHandler([min, max]);
|
onRangeSliderHandler([String(min), String(max)]);
|
||||||
}}
|
}}
|
||||||
onAfterChange={onRangeHandler}
|
onAfterChange={onRangeHandler}
|
||||||
value={[parseFloat(preMin), parseFloat(preMax)]}
|
value={[Number(preMin), Number(preMax)]}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Typography } from 'antd';
|
import { Input, Typography } from 'antd';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const DurationText = styled.div`
|
export const DurationText = styled.div`
|
||||||
@ -9,6 +9,19 @@ export const DurationText = styled.div`
|
|||||||
flex-direction: column;
|
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`
|
export const InputContainer = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 0.5rem;
|
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
|
<AutoComplete
|
||||||
dropdownClassName="certain-category-search-dropdown"
|
dropdownClassName="certain-category-search-dropdown"
|
||||||
dropdownMatchSelectWidth={500}
|
dropdownMatchSelectWidth={500}
|
||||||
style={{ width: 300 }}
|
style={{ width: '100%' }}
|
||||||
options={options}
|
|
||||||
value={selectedKey}
|
value={selectedKey}
|
||||||
onChange={(value): void => {
|
allowClear
|
||||||
if (options && options.find((option) => option.value === value)) {
|
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);
|
setSelectedKey(value);
|
||||||
|
|
||||||
setLocalSelectedTags((tags) => [
|
setLocalSelectedTags((tags) => [
|
||||||
@ -89,8 +102,6 @@ function TagsKey(props: TagsKeysProps): JSX.Element {
|
|||||||
},
|
},
|
||||||
...tags.slice(index + 1, tags.length),
|
...tags.slice(index + 1, tags.length),
|
||||||
]);
|
]);
|
||||||
} else {
|
|
||||||
setSelectedKey('');
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { Select } from 'antd';
|
import { Select } from 'antd';
|
||||||
import getTagValue from 'api/trace/getTagValue';
|
import getTagValue from 'api/trace/getTagValue';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import { TraceReducer } from 'types/reducer/trace';
|
import { TraceReducer } from 'types/reducer/trace';
|
||||||
|
|
||||||
import { SelectComponent } from './styles';
|
import { AutoCompleteComponent } from './styles';
|
||||||
|
|
||||||
function TagValue(props: TagValueProps): JSX.Element {
|
function TagValue(props: TagValueProps): JSX.Element {
|
||||||
const { tag, setLocalSelectedTags, index, tagKey } = props;
|
const { tag, setLocalSelectedTags, index, tagKey } = props;
|
||||||
@ -16,6 +16,7 @@ function TagValue(props: TagValueProps): JSX.Element {
|
|||||||
Operator: selectedOperator,
|
Operator: selectedOperator,
|
||||||
Values: selectedValues,
|
Values: selectedValues,
|
||||||
} = tag;
|
} = tag;
|
||||||
|
const [localValue, setLocalValue] = useState<string>(selectedValues[0]);
|
||||||
|
|
||||||
const globalReducer = useSelector<AppState, GlobalReducer>(
|
const globalReducer = useSelector<AppState, GlobalReducer>(
|
||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
@ -34,22 +35,38 @@ function TagValue(props: TagValueProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectComponent
|
<AutoCompleteComponent
|
||||||
value={selectedValues[0]}
|
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 => {
|
onSelect={(value: unknown): void => {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
|
setLocalValue(value);
|
||||||
setLocalSelectedTags((tags) => [
|
setLocalSelectedTags((tags) => [
|
||||||
...tags.slice(0, index),
|
...tags.slice(0, index),
|
||||||
{
|
{
|
||||||
Key: selectedKey,
|
Key: selectedKey,
|
||||||
Operator: selectedOperator,
|
Operator: selectedOperator,
|
||||||
Values: [...selectedValues, value],
|
Values: [value],
|
||||||
},
|
},
|
||||||
...tags.slice(index + 1, tags.length),
|
...tags.slice(index + 1, tags.length),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
loading={isLoading || false}
|
|
||||||
>
|
>
|
||||||
{data &&
|
{data &&
|
||||||
data.payload &&
|
data.payload &&
|
||||||
@ -58,7 +75,7 @@ function TagValue(props: TagValueProps): JSX.Element {
|
|||||||
{suggestion.tagValues}
|
{suggestion.tagValues}
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
))}
|
))}
|
||||||
</SelectComponent>
|
</AutoCompleteComponent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Select, Space } from 'antd';
|
import { AutoComplete, Select, Space } from 'antd';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const SpaceComponent = styled(Space)`
|
export const SpaceComponent = styled(Space)`
|
||||||
@ -9,18 +9,23 @@ export const SpaceComponent = styled(Space)`
|
|||||||
|
|
||||||
export const SelectComponent = styled(Select)`
|
export const SelectComponent = styled(Select)`
|
||||||
&&& {
|
&&& {
|
||||||
min-width: 170px;
|
width: 100%;
|
||||||
margin-right: 21.91px;
|
|
||||||
margin-left: 21.92px;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Container = styled.div`
|
export const Container = styled(Space)`
|
||||||
&&& {
|
&&& {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
margin-bottom: 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`
|
export const IconContainer = styled.div`
|
||||||
@ -31,3 +36,9 @@ export const IconContainer = styled.div`
|
|||||||
|
|
||||||
margin-left: 1.125rem;
|
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 { StyledSpace } from 'components/Styled';
|
||||||
import useThemeMode from 'hooks/useThemeMode';
|
import useThemeMode from 'hooks/useThemeMode';
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { ITraceTree } from 'types/api/trace/getTraceItem';
|
import { ITraceTree } from 'types/api/trace/getTraceItem';
|
||||||
|
|
||||||
import ErrorTag from './ErrorTag';
|
import ErrorTag from './ErrorTag';
|
||||||
@ -19,29 +19,38 @@ const { TabPane } = Tabs;
|
|||||||
function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
|
function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
|
||||||
const { tree } = props;
|
const { tree } = props;
|
||||||
const { isDarkMode } = useThemeMode();
|
const { isDarkMode } = useThemeMode();
|
||||||
|
|
||||||
|
const OverLayComponentName = useMemo(() => tree?.name, [tree?.name]);
|
||||||
|
const OverLayComponentServiceName = useMemo(() => tree?.serviceName, [
|
||||||
|
tree?.serviceName,
|
||||||
|
]);
|
||||||
|
|
||||||
if (!tree) {
|
if (!tree) {
|
||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, tags, serviceName } = tree;
|
const { tags } = tree;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardContainer>
|
<CardContainer>
|
||||||
<StyledSpace
|
<StyledSpace
|
||||||
styledclass={[styles.selectedSpanDetailsContainer]}
|
styledclass={[styles.selectedSpanDetailsContainer, styles.overflow]}
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
style={{ marginLeft: '0.5rem' }}
|
style={{ marginLeft: '0.5rem' }}
|
||||||
>
|
>
|
||||||
<strong> Details for selected Span </strong>
|
<strong> Details for selected Span </strong>
|
||||||
<Space direction="vertical" size={2}>
|
|
||||||
<CustomTitle>Service</CustomTitle>
|
<CustomTitle>Service</CustomTitle>
|
||||||
<CustomText>{serviceName}</CustomText>
|
<Tooltip overlay={OverLayComponentServiceName}>
|
||||||
</Space>
|
<CustomText ellipsis>{tree.serviceName}</CustomText>
|
||||||
<Space direction="vertical" size={2}>
|
</Tooltip>
|
||||||
<CustomTitle>Operation</CustomTitle>
|
|
||||||
<CustomText>{name}</CustomText>
|
<CustomTitle>Operation</CustomTitle>
|
||||||
</Space>
|
<Tooltip overlay={OverLayComponentName}>
|
||||||
|
<CustomText ellipsis>{tree.name}</CustomText>
|
||||||
|
</Tooltip>
|
||||||
</StyledSpace>
|
</StyledSpace>
|
||||||
|
|
||||||
<Tabs defaultActiveKey="1">
|
<Tabs defaultActiveKey="1">
|
||||||
<TabPane tab="Tags" key="1">
|
<TabPane tab="Tags" key="1">
|
||||||
{tags.length !== 0 ? (
|
{tags.length !== 0 ? (
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Typography } from 'antd';
|
import { Space, Typography } from 'antd';
|
||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
const { Text, Title, Paragraph } = Typography;
|
const { Title, Paragraph } = Typography;
|
||||||
|
|
||||||
export const CustomTitle = styled(Title)`
|
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;
|
color: #2d9cdb;
|
||||||
}
|
}
|
||||||
@ -17,7 +17,6 @@ export const CustomText = styled(Text)`
|
|||||||
|
|
||||||
export const CustomSubTitle = styled(Title)`
|
export const CustomSubTitle = styled(Title)`
|
||||||
&&& {
|
&&& {
|
||||||
/* color: #bdbdbd; */
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
@ -44,6 +43,17 @@ export const CardContainer = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
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`
|
const removeMargin = css`
|
||||||
@ -60,9 +70,21 @@ const selectedSpanDetailsContainer = css`
|
|||||||
const spanEventsTabsContainer = css`
|
const spanEventsTabsContainer = css`
|
||||||
margin-top: 1rem;
|
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 = {
|
export const styles = {
|
||||||
removeMargin,
|
removeMargin,
|
||||||
removePadding,
|
removePadding,
|
||||||
selectedSpanDetailsContainer,
|
selectedSpanDetailsContainer,
|
||||||
spanEventsTabsContainer,
|
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