Compare commits

...

43 Commits

Author SHA1 Message Date
nikhilmantri0902
1948cdfaa4 chore: added for comparison across group by and merges debugging quantile exact 2025-08-19 01:16:24 +05:30
nikhilmantri0902
2fb7fe49ef chore: added for comparison across group by and merges debugging quantile exact 2025-08-19 00:56:43 +05:30
nikhilmantri0902
c4762045a6 chore: added debug functionality 2025-08-18 15:34:20 +05:30
nikhilmantri0902
1541734542 feat: match query building semantics for tags of 0 length 2025-08-18 15:26:12 +05:30
nikhilmantri0902
46e5b407f7 fix: added necessary 0 numCalls handling 2025-08-18 13:53:59 +05:30
nikhilmantri0902
f2c3946101 fix: added necessary 0 numCalls handling 2025-08-18 13:52:55 +05:30
nikhilmantri0902
4dca46de40 chore: added debugging funcs 2025-08-18 13:24:28 +05:30
nikhilmantri0902
6f420abe27 chore: removed comparison block 2025-08-18 13:09:12 +05:30
nikhilmantri0902
1d9b457af6 chore: removed comparison block 2025-08-18 12:40:51 +05:30
nikhilmantri0902
d437998750 chore: tuple issue fixed 2025-08-18 12:13:08 +05:30
nikhilmantri0902
e02d0cdd98 chore: tuple issue fixed 2025-08-18 11:44:45 +05:30
nikhilmantri0902
1ad4a6699a chore: added debug logs 2025-08-18 11:24:29 +05:30
nikhilmantri0902
00ae45022b fix: added filtering based on both name and serviceName pairs 2025-08-18 11:20:08 +05:30
nikhilmantri0902
6f4a965c6d fix: added filtering based on both name and serviceName pairs 2025-08-18 11:19:01 +05:30
nikhilmantri0902
4c29b03577 chore: added logs for debugging 2025-08-15 14:15:20 +05:30
nikhilmantri0902
ea1409bc4f fix: added query optimization 2025-08-14 20:12:48 +05:30
Abhi kumar
0e3ac2a179 fix: added loading indicators in traces pages when running query (#8782) 2025-08-14 13:53:39 +05:30
Amlan Kumar Nandy
249f8be845 fix: resolve infinite loading issue in metric view in messaging queues (#8779) 2025-08-14 04:16:39 +00:00
primus-bot[bot]
9c952942ad chore(release): bump to v0.92.1 (#8780)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-08-13 15:10:08 +05:30
Nityananda Gohain
dac46d82ff fix: check ch version (#8778)
Check the clickhouse version, before the setting secondary_indices_enable_bulk_filtering is used.
2025-08-13 14:57:25 +05:30
primus-bot[bot]
802ce6de01 chore(release): bump to v0.92.0 (#8776)
#### Summary
 - Release SigNoz v0.92.0
 - Bump SigNoz OTel Collector to v0.129.0
2025-08-13 12:17:43 +05:30
dependabot[bot]
6853f0c99d chore(deps): bump urllib3 from 2.4.0 to 2.5.0 in /tests/integration (#8296) 2025-08-13 04:58:39 +00:00
Srikanth Chekuri
3f8a2870e4 fix: key CONTAINS value doesn't work for numeric values (#8768) 2025-08-13 09:59:28 +05:30
Srikanth Chekuri
5fa70ea802 chore: use *_keys tables instead of tag_attributes_v2 for suggestions (#8753) 2025-08-12 18:10:35 +05:30
Yunus M
3a952fa330 fix: pass metric name to get value suggestions api (#8671)
* fix: pass metric name to get value suggestions api

* feat: add source to get value suggestions
2025-08-11 08:10:31 +00:00
Yunus M
6d97db1d9d fix: use localstorage value to avoid waiting for pref api to set the toggle state, add shortcut (#8751) 2025-08-11 10:26:27 +05:30
Shaheer Kochai
5412e7f70b feat: show count in span details drawer tabs (#8702)
* feat: show event count in Events tab of SpanDetailsDrawer

* feat: add count badges to all SpanDetailsDrawer tabs
2025-08-10 05:39:20 +00:00
aniketio-ctrl
8e5cb9046d fix(alert): added querier v5 in test notify (#8749) 2025-08-08 18:01:23 +05:30
Srikanth Chekuri
760eabb2dc chore: do not return err for meter source temporality (#8750) 2025-08-08 17:39:06 +05:30
Srikanth Chekuri
35ddaaa2fc chore: add env to override logs keys table name (#8748) 2025-08-08 11:34:09 +00:00
nikhilmantri0902
a51ee66c02 Improvement: Added Otel-collector setup for local dev environment (#8701)
* feat(devenv): add otel-collector support for local development

- Add .devenv/docker/otel-collector/ with compose.yaml and config
- Add devenv-otel-collector and devenv-up targets to Makefile
- Update development.md with otel-collector setup instructions
- Add README.md with usage documentation for otel-collector setup

This enables developers to run the complete SigNoz stack locally,
including the OpenTelemetry Collector for receiving telemetry data
on ports 4317 (gRPC) and 4318 (HTTP).

* docs: improve collector setup wordings

* chore: fixed comment and service name

* chore: docker service name updated otel-collector -> signoz-otel-collector
2025-08-08 16:54:05 +05:30
Yunus M
75d189162b feat: migrate old saved columns keys to name (#8747) 2025-08-08 14:41:34 +05:30
Yunus M
932918e3a4 feat: meter explorer (#8741)
* feat: meter explorer

* feat: meter explorer

* fix: remove meter as data source

* fix: change meter-explorer to meter - quick filter

* chore: delete test file

* fix: failing test cases
2025-08-08 12:03:26 +05:30
Vibhu Pandey
aa3bc16dcb test(integration): bump requests to 2.32.4 (#8743) 2025-08-08 00:25:38 +05:30
Yunus M
b5098e00a3 fix: logs explorer - should have atleast 1 column, discard empty key columns (#8740) 2025-08-07 20:17:34 +05:30
Abhi kumar
20dc561bfe fix: added fix for query becoming empty on time change (#8739) 2025-08-07 19:42:07 +05:30
Nityananda Gohain
99bbb87738 chore: add option to ignore data skipping indices (#8738)
* chore: add option to ignore data skipping indices

* fix: update example
2025-08-07 13:21:17 +00:00
Vikrant Gupta
f1ce93171c feat(telemetrymeter): add support for telemetry meter (#8667)
* feat(telemetry/meter): added base setup for telemetry meter signal

* feat(telemetry/meter): added metadata setup for meter

* feat(telemetry/meter): fix stmnt builder tests

* feat(telemetry/meter): test query range API fixes

* feat(telemetry/meter): improve error messages

* feat(telemetrymeter): step interval improvements

* feat(telemetrymeter): metadata changes and aggregate attribute changes

* feat(telemetrymeter): metadata changes and aggregate attribute changes

* feat(telemetrymeter): deprecate the signal and use aggregation instead

* feat(telemetrymeter): deprecate the signal and use aggregation instead

* feat(telemetrymeter): deprecate the signal and use aggregation instead

* feat(telemetrymeter): cleanup the types

* feat(telemetrymeter): introduce source for query

* feat(telemetrymeter): better naming for source in metadata

* feat(telemetrymeter): added quick filters for meter explorer

* feat(telemetrymeter): incorporate the new changes to stmnt builder

* feat(telemetrymeter): add the statement builder for the ranged cache queries

* feat(telemetrymeter): use meter aggregate keys

* feat(telemetrymeter): use meter aggregate keys

* feat(telemetrymeter): remove meter from complete bools

* feat(telemetrymeter): remove meter from complete bools

* feat(telemetrymeter): update the quick filters to use meter
2025-08-07 16:50:37 +05:30
Srikanth Chekuri
92794389d6 fix: limit keys for empty search key (#8728) 2025-08-07 00:34:44 +05:30
Srikanth Chekuri
bd02848623 chore: add sql migration for dashboards, alerts, and saved views (#8642)
## 📄 Summary

To reliably migrate the alerts and dashboards, we need access to the telemetrystore to fetch some metadata and while doing migration, I need to log some stuff to fix stuff later.

Key changes:
- Modified the migration to include telemetrystore and a logging provider (open to using a standard logger instead)
- To avoid the previous issues with imported dashboards failing during migration, I've ensured that imported JSON files are automatically transformed when migration is active
- Implemented detailed logic to handle dashboard migration cleanly and prevent unnecessary errors
- Separated the core migration logic from SQL migration code, as users from the dot metrics migration requested shareable code snippets for local migrations. This modular approach allows others to easily reuse the migration functionality.

Known: I didn't register the migration yet in this PR, and will not merge this yet, so please review with that in mid.
2025-08-06 23:05:39 +05:30
Abhi kumar
b5016b061b fix: added fix for key suggestions (#8727) 2025-08-06 11:48:43 +00:00
Abhi kumar
c308e8668c fix: added fix for query addon lightmode ui (#8725) 2025-08-06 16:21:35 +05:30
SagarRajput-7
41ee4176ad fix: fixed metric aggregation and value retention inconsistency in edit mode (#8718) 2025-08-06 13:55:16 +05:30
190 changed files with 6857 additions and 634 deletions

View File

@@ -40,7 +40,7 @@ services:
timeout: 5s
retries: 3
schema-migrator-sync:
image: signoz/signoz-schema-migrator:v0.128.2
image: signoz/signoz-schema-migrator:v0.129.0
container_name: schema-migrator-sync
command:
- sync
@@ -53,7 +53,7 @@ services:
condition: service_healthy
restart: on-failure
schema-migrator-async:
image: signoz/signoz-schema-migrator:v0.128.2
image: signoz/signoz-schema-migrator:v0.129.0
container_name: schema-migrator-async
command:
- async

View File

@@ -0,0 +1,29 @@
services:
signoz-otel-collector:
image: signoz/signoz-otel-collector:v0.128.2
container_name: signoz-otel-collector-dev
command:
- --config=/etc/otel-collector-config.yaml
- --feature-gates=-pkg.translator.prometheus.NormalizeName
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
- LOW_CARDINAL_EXCEPTION_GROUPING=false
ports:
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
- "13133:13133" # health check extension
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- localhost:13133
interval: 30s
timeout: 5s
retries: 3
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"

View File

@@ -0,0 +1,96 @@
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
prometheus:
config:
global:
scrape_interval: 60s
scrape_configs:
- job_name: otel-collector
static_configs:
- targets:
- localhost:8888
labels:
job_name: otel-collector
processors:
batch:
send_batch_size: 10000
send_batch_max_size: 11000
timeout: 10s
resourcedetection:
# Using OTEL_RESOURCE_ATTRIBUTES envvar, env detector adds custom labels.
detectors: [env, system]
timeout: 2s
signozspanmetrics/delta:
metrics_exporter: signozclickhousemetrics
metrics_flush_interval: 60s
latency_histogram_buckets: [100us, 1ms, 2ms, 6ms, 10ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 1400ms, 2000ms, 5s, 10s, 20s, 40s, 60s ]
dimensions_cache_size: 100000
aggregation_temporality: AGGREGATION_TEMPORALITY_DELTA
enable_exp_histogram: true
dimensions:
- name: service.namespace
default: default
- name: deployment.environment
default: default
# This is added to ensure the uniqueness of the timeseries
# Otherwise, identical timeseries produced by multiple replicas of
# collectors result in incorrect APM metrics
- name: signoz.collector.id
- name: service.version
- name: browser.platform
- name: browser.mobile
- name: k8s.cluster.name
- name: k8s.node.name
- name: k8s.namespace.name
- name: host.name
- name: host.type
- name: container.name
extensions:
health_check:
endpoint: 0.0.0.0:13133
pprof:
endpoint: 0.0.0.0:1777
exporters:
clickhousetraces:
datasource: tcp://host.docker.internal:9000/signoz_traces
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
use_new_schema: true
signozclickhousemetrics:
dsn: tcp://host.docker.internal:9000/signoz_metrics
clickhouselogsexporter:
dsn: tcp://host.docker.internal:9000/signoz_logs
timeout: 10s
use_new_schema: true
service:
telemetry:
logs:
encoding: json
extensions:
- health_check
- pprof
pipelines:
traces:
receivers: [otlp]
processors: [signozspanmetrics/delta, batch]
exporters: [clickhousetraces]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [signozclickhousemetrics]
metrics/prometheus:
receivers: [prometheus]
processors: [batch]
exporters: [signozclickhousemetrics]
logs:
receivers: [otlp]
processors: [batch]
exporters: [clickhouselogsexporter]

View File

@@ -61,6 +61,17 @@ devenv-postgres: ## Run postgres in devenv
@cd .devenv/docker/postgres; \
docker compose -f compose.yaml up -d
.PHONY: devenv-signoz-otel-collector
devenv-signoz-otel-collector: ## Run signoz-otel-collector in devenv (requires clickhouse to be running)
@cd .devenv/docker/signoz-otel-collector; \
docker compose -f compose.yaml up -d
.PHONY: devenv-up
devenv-up: devenv-clickhouse devenv-signoz-otel-collector ## Start both clickhouse and signoz-otel-collector for local development
@echo "Development environment is ready!"
@echo " - ClickHouse: http://localhost:8123"
@echo " - Signoz OTel Collector: grpc://localhost:4317, http://localhost:4318"
##############################################################
# go commands
##############################################################

View File

@@ -121,6 +121,8 @@ telemetrystore:
timeout_before_checking_execution_speed: 0
max_bytes_to_read: 0
max_result_rows: 0
ignore_data_skipping_indices: ""
secondary_indices_enable_bulk_filtering: false
##################### Prometheus #####################
prometheus:

View File

@@ -174,7 +174,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.91.0
image: signoz/signoz:v0.92.1
command:
- --config=/root/config/prometheus.yml
ports:
@@ -207,7 +207,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.128.2
image: signoz/signoz-otel-collector:v0.129.0
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -231,7 +231,7 @@ services:
- signoz
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:v0.128.2
image: signoz/signoz-schema-migrator:v0.129.0
deploy:
restart_policy:
condition: on-failure

View File

@@ -115,7 +115,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.91.0
image: signoz/signoz:v0.92.1
command:
- --config=/root/config/prometheus.yml
ports:
@@ -148,7 +148,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.128.2
image: signoz/signoz-otel-collector:v0.129.0
command:
- --config=/etc/otel-collector-config.yaml
- --manager-config=/etc/manager-config.yaml
@@ -174,7 +174,7 @@ services:
- signoz
schema-migrator:
!!merge <<: *common
image: signoz/signoz-schema-migrator:v0.128.2
image: signoz/signoz-schema-migrator:v0.129.0
deploy:
restart_policy:
condition: on-failure

View File

@@ -177,7 +177,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.91.0}
image: signoz/signoz:${VERSION:-v0.92.1}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
@@ -211,7 +211,7 @@ services:
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.2}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.0}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -237,7 +237,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
container_name: schema-migrator-sync
command:
- sync
@@ -248,7 +248,7 @@ services:
condition: service_healthy
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
container_name: schema-migrator-async
command:
- async

View File

@@ -110,7 +110,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.91.0}
image: signoz/signoz:${VERSION:-v0.92.1}
container_name: signoz
command:
- --config=/root/config/prometheus.yml
@@ -143,7 +143,7 @@ services:
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.128.2}
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.129.0}
container_name: signoz-otel-collector
command:
- --config=/etc/otel-collector-config.yaml
@@ -165,7 +165,7 @@ services:
condition: service_healthy
schema-migrator-sync:
!!merge <<: *common
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
container_name: schema-migrator-sync
command:
- sync
@@ -177,7 +177,7 @@ services:
restart: on-failure
schema-migrator-async:
!!merge <<: *db-depend
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.128.2}
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.129.0}
container_name: schema-migrator-async
command:
- async

View File

@@ -44,20 +44,35 @@ Before diving in, make sure you have these tools installed:
SigNoz has three main components: Clickhouse, Backend, and Frontend. Let's set them up one by one.
### 1. Setting up Clickhouse
### 1. Setting up ClickHouse
First, we need to get Clickhouse running:
First, we need to get ClickHouse running:
```bash
make devenv-clickhouse
```
This command:
- Starts Clickhouse in a single-shard, single-replica cluster
- Starts ClickHouse in a single-shard, single-replica cluster
- Sets up Zookeeper
- Runs the latest schema migrations
### 2. Starting the Backend
### 2. Setting up SigNoz OpenTelemetry Collector
Next, start the OpenTelemetry Collector to receive telemetry data:
```bash
make devenv-signoz-otel-collector
```
This command:
- Starts the SigNoz OpenTelemetry Collector
- Listens on port 4317 (gRPC) and 4318 (HTTP) for incoming telemetry data
- Forwards data to ClickHouse for storage
> 💡 **Quick Setup**: Use `make devenv-up` to start both ClickHouse and OTel Collector together
### 3. Starting the Backend
1. Run the backend server:
```bash
@@ -73,7 +88,7 @@ This command:
> 💡 **Tip**: The API server runs at `http://localhost:8080/` by default
### 3. Setting up the Frontend
### 4. Setting up the Frontend
1. Navigate to the frontend directory:
```bash
@@ -98,3 +113,25 @@ This command:
> 💡 **Tip**: `yarn dev` will automatically rebuild when you make changes to the code
Now you're all set to start developing! Happy coding! 🎉
## Verifying Your Setup
To verify everything is working correctly:
1. **Check ClickHouse**: `curl http://localhost:8123/ping` (should return "Ok.")
2. **Check OTel Collector**: `curl http://localhost:13133` (should return health status)
3. **Check Backend**: `curl http://localhost:8080/api/v1/health` (should return `{"status":"ok"}`)
4. **Check Frontend**: Open `http://localhost:3301` in your browser
## How to send test data?
You can now send telemetry data to your local SigNoz instance:
- **OTLP gRPC**: `localhost:4317`
- **OTLP HTTP**: `localhost:4318`
For example, using `curl` to send a test trace:
```bash
curl -X POST http://localhost:4318/v1/traces \
-H "Content-Type: application/json" \
-d '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"test-service"}}]},"scopeSpans":[{"spans":[{"traceId":"12345678901234567890123456789012","spanId":"1234567890123456","name":"test-span","startTimeUnixNano":"1609459200000000000","endTimeUnixNano":"1609459201000000000"}]}]}]}'
```

View File

@@ -46,5 +46,8 @@
"ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring",
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring"
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring",
"METER_EXPLORER": "SigNoz | Meter Explorer",
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer",
"METER_EXPLORER_BASE": "SigNoz | Meter Explorer"
}

View File

@@ -69,5 +69,8 @@
"METRICS_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_EXPLORER": "SigNoz | Metrics Explorer",
"METRICS_EXPLORER_VIEWS": "SigNoz | Metrics Explorer",
"API_MONITORING": "SigNoz | External APIs"
"API_MONITORING": "SigNoz | External APIs",
"METER_EXPLORER": "SigNoz | Meter Explorer",
"METER_EXPLORER_VIEWS": "SigNoz | Meter Explorer",
"METER_EXPLORER_BASE": "SigNoz | Meter Explorer"
}

View File

@@ -1,5 +1,6 @@
import ROUTES from 'constants/routes';
import MessagingQueues from 'pages/MessagingQueues';
import MeterExplorer from 'pages/MeterExplorer';
import { RouteProps } from 'react-router-dom';
import {
@@ -434,6 +435,28 @@ const routes: AppRoutes[] = [
key: 'METRICS_EXPLORER_VIEWS',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER_BASE,
exact: true,
component: MeterExplorer,
key: 'METER_EXPLORER_BASE',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER,
exact: true,
component: MeterExplorer,
key: 'METER_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.METER_EXPLORER_VIEWS,
exact: true,
component: MeterExplorer,
key: 'METER_EXPLORER_VIEWS',
isPrivate: true,
},
{
path: ROUTES.API_MONITORING,
exact: true,

View File

@@ -17,6 +17,7 @@ export const getAggregateAttribute = async ({
aggregateOperator,
searchText,
dataSource,
source,
}: IGetAggregateAttributePayload): Promise<
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
> => {
@@ -27,7 +28,7 @@ export const getAggregateAttribute = async ({
`/autocomplete/aggregate_attributes?${createQueryParams({
aggregateOperator,
searchText,
dataSource,
dataSource: source === 'meter' ? 'meter' : dataSource,
})}`,
);

View File

@@ -14,6 +14,7 @@ export const getKeySuggestions = (
metricName = '',
fieldContext = '',
fieldDataType = '',
signalSource = '',
} = props;
const encodedSignal = encodeURIComponent(signal);
@@ -21,8 +22,9 @@ export const getKeySuggestions = (
const encodedMetricName = encodeURIComponent(metricName);
const encodedFieldContext = encodeURIComponent(fieldContext);
const encodedFieldDataType = encodeURIComponent(fieldDataType);
const encodedSource = encodeURIComponent(signalSource);
return axios.get(
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}`,
`/fields/keys?signal=${encodedSignal}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&fieldContext=${encodedFieldContext}&fieldDataType=${encodedFieldDataType}&source=${encodedSource}`,
);
};

View File

@@ -8,13 +8,15 @@ import {
export const getValueSuggestions = (
props: QueryKeyValueRequestProps,
): Promise<AxiosResponse<QueryKeyValueSuggestionsResponseProps>> => {
const { signal, key, searchText } = props;
const { signal, key, searchText, signalSource, metricName } = props;
const encodedSignal = encodeURIComponent(signal);
const encodedKey = encodeURIComponent(key);
const encodedMetricName = encodeURIComponent(metricName || '');
const encodedSearchText = encodeURIComponent(searchText);
const encodedSource = encodeURIComponent(signalSource || '');
return axios.get(
`/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}`,
`/fields/values?signal=${encodedSignal}&name=${encodedKey}&searchText=${encodedSearchText}&metricName=${encodedMetricName}&source=${encodedSource}`,
);
};

View File

@@ -4,6 +4,6 @@ import { AllViewsProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder';
export const getAllViews = (
sourcepage: DataSource,
sourcepage: DataSource | 'meter',
): Promise<AxiosResponse<AllViewsProps>> =>
axios.get(`/explorer/views?sourcePage=${sourcepage}`);

View File

@@ -385,7 +385,7 @@ export function convertV5ResponseToLegacy(
data: {
resultType: 'scalar',
result: webTables,
warnings: v5Data?.data?.warnings || [],
warnings: v5Data?.data?.warning || [],
},
warning: v5Data?.warning || undefined,
},

View File

@@ -260,6 +260,7 @@ export function convertBuilderQueriesToV5(
spec = {
name: queryName,
signal: 'metrics' as const,
source: queryData.source || '',
...baseSpec,
aggregations: aggregations as MetricAggregation[],
// reduceTo: queryData.reduceTo,

View File

@@ -0,0 +1,19 @@
.loading-panel-data {
padding: 24px 0;
height: 240px;
display: flex;
justify-content: center;
align-items: flex-start;
.loading-panel-data-content {
display: flex;
align-items: flex-start;
flex-direction: column;
.loading-gif {
height: 72px;
margin-left: -24px;
}
}
}

View File

@@ -0,0 +1,19 @@
import './PanelDataLoading.styles.scss';
import { Typography } from 'antd';
export function PanelDataLoading(): JSX.Element {
return (
<div className="loading-panel-data">
<div className="loading-panel-data-content">
<img
className="loading-gif"
src="/Icons/loading-plane.gif"
alt="wait-icon"
/>
<Typography.Text>Fetching data...</Typography.Text>
</div>
</div>
);
}

View File

@@ -131,6 +131,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
signalSource={config?.signalSource || ''}
/>
))}

View File

@@ -18,11 +18,13 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
index,
version,
panelType,
signalSource = '',
}: {
query: IBuilderQuery;
index: number;
version: string;
panelType: PANEL_TYPES | null;
signalSource: string;
}): JSX.Element {
const { setAggregationOptions } = useQueryBuilderV2Context();
const {
@@ -208,6 +210,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
disabled={!queryAggregation.metricName}
query={query}
onChange={handleChangeGroupByKeys}
signalSource={signalSource}
/>
</div>
</div>
@@ -244,6 +247,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
disabled={!queryAggregation.metricName}
query={query}
onChange={handleChangeGroupByKeys}
signalSource={signalSource}
/>
</div>
</div>

View File

@@ -9,10 +9,12 @@ export const MetricsSelect = memo(function MetricsSelect({
query,
index,
version,
signalSource,
}: {
query: IBuilderQuery;
index: number;
version: string;
signalSource: 'meter' | '';
}): JSX.Element {
const { handleChangeAggregatorAttribute } = useQueryOperations({
index,
@@ -26,6 +28,7 @@ export const MetricsSelect = memo(function MetricsSelect({
onChange={handleChangeAggregatorAttribute}
query={query}
index={index}
signalSource={signalSource || ''}
/>
</div>
);

View File

@@ -127,6 +127,7 @@
box-sizing: border-box;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.6);
font-family: 'Space Mono', monospace !important;
color: var(--bg-vanilla-100) !important;
ul {
width: 100% !important;
@@ -162,7 +163,6 @@
overflow: hidden;
font-family: 'Space Mono', monospace !important;
color: var(--bg-vanilla-100) !important;
.cm-completionIcon {
display: none !important;
@@ -331,13 +331,14 @@
ul {
li {
color: var(--bg-ink-300) !important;
&:hover {
background: var(--bg-vanilla-300) !important;
}
&[aria-selected='true'] {
color: var(--bg-ink-500) !important;
background: var(--bg-vanilla-300) !important;
font-weight: 600 !important;
}
}
}

View File

@@ -271,16 +271,13 @@
ul {
li {
&:hover {
background-color: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
font-weight: 600;
}
color: var(--bg-ink-300) !important;
&:hover,
&[aria-selected='true'] {
background: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500) !important;
font-weight: 600;
font-weight: 600 !important;
}
}
}

View File

@@ -585,7 +585,7 @@
&:hover,
&[aria-selected='true'] {
background-color: var(--bg-vanilla-300) !important;
font-weight: 600;
font-weight: 600 !important;
}
}
}

View File

@@ -81,10 +81,12 @@ function QuerySearch({
queryData,
dataSource,
onRun,
signalSource,
}: {
onChange: (value: string) => void;
queryData: IBuilderQuery;
dataSource: DataSource;
signalSource?: string;
onRun?: (query: string) => void;
}): JSX.Element {
const isDarkMode = useIsDarkMode();
@@ -153,6 +155,7 @@ function QuerySearch({
// Reference to the editor view for programmatic autocompletion
const editorRef = useRef<EditorView | null>(null);
const lastKeyRef = useRef<string>('');
const lastFetchedKeyRef = useRef<string>('');
const lastValueRef = useRef<string>('');
const isMountedRef = useRef<boolean>(true);
@@ -210,10 +213,14 @@ function QuerySearch({
setKeySuggestions([]);
return;
}
lastFetchedKeyRef.current = searchText || '';
const response = await getKeySuggestions({
signal: dataSource,
searchText: searchText || '',
metricName: debouncedMetricName ?? undefined,
signalSource: signalSource as 'meter' | '',
});
if (response.data.data) {
@@ -241,6 +248,7 @@ function QuerySearch({
keySuggestions,
toggleSuggestions,
queryData.aggregateAttribute?.key,
signalSource,
],
);
@@ -374,6 +382,8 @@ function QuerySearch({
key,
searchText: sanitizedSearchText,
signal: dataSource,
signalSource: signalSource as 'meter' | '',
metricName: debouncedMetricName ?? undefined,
});
// Skip updates if component unmounted or key changed
@@ -461,8 +471,14 @@ function QuerySearch({
setIsFetchingCompleteValuesList(false);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[activeKey, dataSource, isFocused],
[
activeKey,
dataSource,
isLoadingSuggestions,
debouncedMetricName,
signalSource,
toggleSuggestions,
],
);
const debouncedFetchValueSuggestions = useMemo(
@@ -805,7 +821,7 @@ function QuerySearch({
option.label.toLowerCase().includes(searchText),
);
if (options.length === 0 && lastKeyRef.current !== searchText) {
if (options.length === 0 && lastFetchedKeyRef.current !== searchText) {
debouncedFetchKeySuggestions(searchText);
}
@@ -1436,6 +1452,7 @@ function QuerySearch({
QuerySearch.defaultProps = {
onRun: undefined,
signalSource: '',
};
export default QuerySearch;

View File

@@ -28,6 +28,7 @@ export const QueryV2 = memo(function QueryV2({
isListViewPanel = false,
version,
showOnlyWhereClause = false,
signalSource = '',
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
const { cloneQuery, panelType } = useQueryBuilder();
@@ -175,6 +176,7 @@ export const QueryV2 = memo(function QueryV2({
query={query}
index={index}
version={ENTITY_VERSION_V5}
signalSource={signalSource as 'meter' | ''}
/>
</div>
)}
@@ -186,6 +188,7 @@ export const QueryV2 = memo(function QueryV2({
onChange={handleSearchChange}
queryData={query}
dataSource={dataSource}
signalSource={signalSource}
/>
</div>
@@ -218,6 +221,7 @@ export const QueryV2 = memo(function QueryV2({
index={index}
key={`metrics-aggregate-section-${query.queryName}-${query.dataSource}`}
version="v4"
signalSource={signalSource as 'meter' | ''}
/>
)}

View File

@@ -17,6 +17,7 @@ import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
import { ChevronDown, ChevronRight } from 'lucide-react';
@@ -73,18 +74,59 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
searchText: searchText ?? '',
},
{
enabled: isOpen,
enabled: isOpen && source !== QuickFiltersSource.METER_EXPLORER,
keepPreviousData: true,
},
);
const {
data: keyValueSuggestions,
isLoading: isLoadingKeyValueSuggestions,
} = useGetQueryKeyValueSuggestions({
key: filter.attributeKey.key,
signal: filter.dataSource || DataSource.LOGS,
signalSource: 'meter',
options: {
enabled: isOpen && source === QuickFiltersSource.METER_EXPLORER,
keepPreviousData: true,
},
});
const attributeValues: string[] = useMemo(() => {
const dataType = filter.attributeKey.dataType || DataTypes.String;
if (source === QuickFiltersSource.METER_EXPLORER && keyValueSuggestions) {
// Process the response data
const responseData = keyValueSuggestions?.data as any;
const values = responseData.data?.values || {};
const stringValues = values.stringValues || [];
const numberValues = values.numberValues || [];
// Generate options from string values - explicitly handle empty strings
const stringOptions = stringValues
// Strict filtering for empty string - we'll handle it as a special case if needed
.filter(
(value: string | null | undefined): value is string =>
value !== null && value !== undefined && value !== '',
);
// Generate options from number values
const numberOptions = numberValues
.filter(
(value: number | null | undefined): value is number =>
value !== null && value !== undefined,
)
.map((value: number) => value.toString());
// Combine all options and make sure we don't have duplicate labels
return [...stringOptions, ...numberOptions];
}
const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
return (data?.payload?.[key] || []).filter(
(val) => val !== undefined && val !== null,
);
}, [data?.payload, filter.attributeKey.dataType]);
}, [data?.payload, filter.attributeKey.dataType, keyValueSuggestions, source]);
const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount);
@@ -478,12 +520,14 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
)}
</section>
</section>
{isOpen && isLoading && !attributeValues.length && (
<section className="loading">
<Skeleton paragraph={{ rows: 4 }} />
</section>
)}
{isOpen && !isLoading && (
{isOpen &&
(isLoading || isLoadingKeyValueSuggestions) &&
!attributeValues.length && (
<section className="loading">
<Skeleton paragraph={{ rows: 4 }} />
</section>
)}
{isOpen && !isLoading && !isLoadingKeyValueSuggestions && (
<>
{!isEmptyStateWithDocsEnabled && (
<section className="search">

View File

@@ -1,6 +1,8 @@
.quick-filters-container {
display: flex;
height: 100%;
position: relative;
.quick-filters-settings-container {
position: relative;
}
@@ -102,6 +104,37 @@
margin: 8px 12px;
}
}
.no-filters-container {
display: flex;
height: 100%;
gap: 8px;
align-items: center;
padding: 8px;
}
}
.perilin-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle, #fff 10%, transparent 0);
background-size: 12px 12px;
opacity: 1;
mask-image: radial-gradient(
circle at 50% 0,
rgba(11, 12, 14, 0.1) 0,
rgba(11, 12, 14, 0) 100%
);
-webkit-mask-image: radial-gradient(
circle at 50% 0,
rgba(11, 12, 14, 0.1) 0,
rgba(11, 12, 14, 0) 100%
);
}
.lightMode {

View File

@@ -15,7 +15,7 @@ import { LOCALSTORAGE } from 'constants/localStorage';
import { useApiMonitoringParams } from 'container/ApiMonitoring/queryParams';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep, isFunction, isNull } from 'lodash-es';
import { Settings2 as SettingsIcon } from 'lucide-react';
import { Frown, Settings2 as SettingsIcon } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useMemo, useState } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -236,6 +236,13 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
);
}
})}
{filterConfig.length === 0 && (
<div className="no-filters-container">
<Frown size={16} />
<Typography.Text>No filters found</Typography.Text>
</div>
)}
</section>
</>
</OverlayScrollbar>

View File

@@ -6,4 +6,5 @@ export const SIGNAL_DATA_SOURCE_MAP = {
[SignalType.TRACES]: DataSource.TRACES,
[SignalType.EXCEPTIONS]: DataSource.TRACES,
[SignalType.API_MONITORING]: DataSource.TRACES,
[SignalType.METER_EXPLORER]: DataSource.METRICS,
};

View File

@@ -54,6 +54,7 @@ const quickFiltersListURL = `${BASE_URL}/api/v1/orgs/me/filters/${SIGNAL}`;
const saveQuickFiltersURL = `${BASE_URL}/api/v1/orgs/me/filters`;
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v3/filter_suggestions`;
const quickFiltersAttributeValuesURL = `${BASE_URL}/api/v3/autocomplete/attribute_values`;
const fieldsValuesURL = `${BASE_URL}/api/v1/fields/values`;
const FILTER_OS_DESCRIPTION = 'os.description';
const FILTER_K8S_DEPLOYMENT_NAME = 'k8s.deployment.name';
@@ -77,7 +78,11 @@ const setupServer = (): void => {
putHandler(await req.json());
return res(ctx.status(200), ctx.json({}));
}),
rest.get(quickFiltersAttributeValuesURL, (_, res, ctx) =>
rest.get(quickFiltersAttributeValuesURL, (req, res, ctx) =>
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
),
rest.get(fieldsValuesURL, (req, res, ctx) =>
res(ctx.status(200), ctx.json(quickFiltersAttributeValuesResponse)),
),
);

View File

@@ -23,6 +23,7 @@ export enum SignalType {
LOGS = 'logs',
API_MONITORING = 'api_monitoring',
EXCEPTIONS = 'exceptions',
METER_EXPLORER = 'meter',
}
export interface IQuickFiltersConfig {
@@ -53,4 +54,5 @@ export enum QuickFiltersSource {
TRACES_EXPLORER = 'traces-explorer',
API_MONITORING = 'api-monitoring',
EXCEPTIONS = 'exceptions',
METER_EXPLORER = 'meter',
}

View File

@@ -23,6 +23,7 @@ import {
BoolOperators,
DataSource,
LogsAggregatorOperator,
MeterAggregateOperator,
MetricAggregateOperator,
NumberOperators,
QueryAdditionalFilter,
@@ -36,6 +37,7 @@ import { v4 as uuid } from 'uuid';
import {
logsAggregateOperatorOptions,
meterAggregateOperatorOptions,
metricAggregateOperatorOptions,
metricsGaugeAggregateOperatorOptions,
metricsGaugeSpaceAggregateOperatorOptions,
@@ -79,6 +81,7 @@ export const mapOfOperators = {
metrics: metricAggregateOperatorOptions,
logs: logsAggregateOperatorOptions,
traces: tracesAggregateOperatorOptions,
meter: meterAggregateOperatorOptions,
};
export const metricsOperatorsByType = {
@@ -193,6 +196,7 @@ export const initialQueryBuilderFormValues: IBuilderQuery = {
groupBy: [],
legend: '',
reduceTo: 'avg',
source: '',
};
const initialQueryBuilderFormLogsValues: IBuilderQuery = {
@@ -209,6 +213,39 @@ const initialQueryBuilderFormTracesValues: IBuilderQuery = {
dataSource: DataSource.TRACES,
};
export const initialQueryBuilderFormMeterValues: IBuilderQuery = {
dataSource: DataSource.METRICS,
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
aggregateOperator: MeterAggregateOperator.COUNT,
aggregateAttribute: initialAutocompleteData,
timeAggregation: MeterAggregateOperator.RATE,
spaceAggregation: MeterAggregateOperator.SUM,
filter: { expression: '' },
aggregations: [
{
metricName: '',
temporality: '',
timeAggregation: MeterAggregateOperator.COUNT,
spaceAggregation: MeterAggregateOperator.SUM,
reduceTo: 'avg',
},
],
functions: [],
filters: { items: [], op: 'AND' },
expression: createNewBuilderItemName({
existNames: [],
sourceNames: alphabet,
}),
disabled: false,
stepInterval: undefined,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
};
export const initialQueryBuilderFormValuesMap: Record<
DataSource,
IBuilderQuery
@@ -285,6 +322,19 @@ export const initialQueriesMap: Record<DataSource, Query> = {
traces: initialQueryTracesWithType,
};
export const initialQueryMeterWithType: Query = {
...initialQueryWithType,
builder: {
...initialQueryWithType.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.metrics,
source: 'meter',
},
],
},
};
export const operatorsByTypes: Record<LocalDataType, string[]> = {
string: Object.values(StringOperators),
number: Object.values(NumberOperators),

View File

@@ -125,6 +125,126 @@ export const metricAggregateOperatorOptions: SelectOption<string, string>[] = [
},
];
export const meterAggregateOperatorOptions: SelectOption<string, string>[] = [
{
value: MetricAggregateOperator.COUNT,
label: 'Count',
},
{
value: MetricAggregateOperator.COUNT_DISTINCT,
// eslint-disable-next-line sonarjs/no-duplicate-string
label: 'Count Distinct',
},
{
value: MetricAggregateOperator.SUM,
label: 'Sum',
},
{
value: MetricAggregateOperator.AVG,
label: 'Avg',
},
{
value: MetricAggregateOperator.MAX,
label: 'Max',
},
{
value: MetricAggregateOperator.MIN,
label: 'Min',
},
{
value: MetricAggregateOperator.P05,
label: 'P05',
},
{
value: MetricAggregateOperator.P10,
label: 'P10',
},
{
value: MetricAggregateOperator.P20,
label: 'P20',
},
{
value: MetricAggregateOperator.P25,
label: 'P25',
},
{
value: MetricAggregateOperator.P50,
label: 'P50',
},
{
value: MetricAggregateOperator.P75,
label: 'P75',
},
{
value: MetricAggregateOperator.P90,
label: 'P90',
},
{
value: MetricAggregateOperator.P95,
label: 'P95',
},
{
value: MetricAggregateOperator.P99,
label: 'P99',
},
{
value: MetricAggregateOperator.RATE,
label: 'Rate',
},
{
value: MetricAggregateOperator.SUM_RATE,
label: 'Sum_rate',
},
{
value: MetricAggregateOperator.AVG_RATE,
label: 'Avg_rate',
},
{
value: MetricAggregateOperator.MAX_RATE,
label: 'Max_rate',
},
{
value: MetricAggregateOperator.MIN_RATE,
label: 'Min_rate',
},
{
value: MetricAggregateOperator.RATE_SUM,
label: 'Rate_sum',
},
{
value: MetricAggregateOperator.RATE_AVG,
label: 'Rate_avg',
},
{
value: MetricAggregateOperator.RATE_MIN,
label: 'Rate_min',
},
{
value: MetricAggregateOperator.RATE_MAX,
label: 'Rate_max',
},
{
value: MetricAggregateOperator.HIST_QUANTILE_50,
label: 'Hist_quantile_50',
},
{
value: MetricAggregateOperator.HIST_QUANTILE_75,
label: 'Hist_quantile_75',
},
{
value: MetricAggregateOperator.HIST_QUANTILE_90,
label: 'Hist_quantile_90',
},
{
value: MetricAggregateOperator.HIST_QUANTILE_95,
label: 'Hist_quantile_95',
},
{
value: MetricAggregateOperator.HIST_QUANTILE_99,
label: 'Hist_quantile_99',
},
];
export const tracesAggregateOperatorOptions: SelectOption<string, string>[] = [
{
value: TracesAggregatorOperator.COUNT,

View File

@@ -77,6 +77,9 @@ const ROUTES = {
API_MONITORING: '/api-monitoring/explorer',
METRICS_EXPLORER_BASE: '/metrics-explorer',
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
METER_EXPLORER_BASE: '/meter-explorer',
METER_EXPLORER: '/meter-explorer',
METER_EXPLORER_VIEWS: '/meter-explorer/views',
HOME_PAGE: '/',
} as const;

View File

@@ -6,6 +6,7 @@ export const GlobalShortcuts = {
NavigateToAlerts: 'a+shift',
NavigateToExceptions: 'e+shift',
NavigateToMessagingQueues: 'm+shift',
ToggleSidebar: 'b+shift',
};
export const GlobalShortcutsName = {
@@ -16,6 +17,7 @@ export const GlobalShortcutsName = {
NavigateToAlerts: 'shift+a',
NavigateToExceptions: 'shift+e',
NavigateToMessagingQueues: 'shift+m',
ToggleSidebar: 'shift+b',
};
export const GlobalShortcutsDescription = {
@@ -26,4 +28,5 @@ export const GlobalShortcutsDescription = {
NavigateToAlerts: 'Navigate to alerts page',
NavigateToExceptions: 'Navigate to Exceptions page',
NavigateToMessagingQueues: 'Navigate to Messaging Queues page',
ToggleSidebar: 'Toggle sidebar visibility',
};

View File

@@ -0,0 +1,176 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import logEvent from 'api/common/logEvent';
import { GlobalShortcuts } from 'constants/shortcuts/globalShortcuts';
import { USER_PREFERENCES } from 'constants/userPreferences';
import {
KeyboardHotkeysProvider,
useKeyboardHotkeys,
} from 'hooks/hotkeys/useKeyboardHotkeys';
import { QueryClient, QueryClientProvider } from 'react-query';
// Mock dependencies
jest.mock('api/common/logEvent', () => jest.fn());
// Mock the AppContext
const mockUpdateUserPreferenceInContext = jest.fn();
const SHIFT_B_KEYBOARD_SHORTCUT = '{Shift>}b{/Shift}';
jest.mock('providers/App/App', () => ({
useAppContext: jest.fn(() => ({
userPreferences: [
{
name: USER_PREFERENCES.SIDENAV_PINNED,
value: false,
},
],
updateUserPreferenceInContext: mockUpdateUserPreferenceInContext,
})),
}));
function TestComponent({
mockHandleShortcut,
}: {
mockHandleShortcut: () => void;
}): JSX.Element {
const { registerShortcut } = useKeyboardHotkeys();
registerShortcut(GlobalShortcuts.ToggleSidebar, mockHandleShortcut);
return <div data-testid="test">Test</div>;
}
describe('Sidebar Toggle Shortcut', () => {
let queryClient: QueryClient;
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
});
jest.clearAllMocks();
});
afterEach(() => {
jest.clearAllMocks();
});
describe('Global Shortcuts Constants', () => {
it('should have the correct shortcut key combination', () => {
expect(GlobalShortcuts.ToggleSidebar).toBe('b+shift');
});
});
describe('Keyboard Shortcut Registration', () => {
it('should register the sidebar toggle shortcut correctly', async () => {
const user = userEvent.setup();
const mockHandleShortcut = jest.fn();
render(
<QueryClientProvider client={queryClient}>
<KeyboardHotkeysProvider>
<TestComponent mockHandleShortcut={mockHandleShortcut} />
</KeyboardHotkeysProvider>
</QueryClientProvider>,
);
// Trigger the shortcut
await user.keyboard(SHIFT_B_KEYBOARD_SHORTCUT);
expect(mockHandleShortcut).toHaveBeenCalled();
});
it('should not trigger shortcut in input fields', async () => {
const user = userEvent.setup();
const mockHandleShortcut = jest.fn();
function TestComponent(): JSX.Element {
const { registerShortcut } = useKeyboardHotkeys();
registerShortcut(GlobalShortcuts.ToggleSidebar, mockHandleShortcut);
return (
<div>
<input data-testid="input-field" />
<div data-testid="test">Test</div>
</div>
);
}
render(
<QueryClientProvider client={queryClient}>
<KeyboardHotkeysProvider>
<TestComponent />
</KeyboardHotkeysProvider>
</QueryClientProvider>,
);
// Focus on input field
const inputField = screen.getByTestId('input-field');
await user.click(inputField);
// Try to trigger shortcut while focused on input
await user.keyboard('{Shift>}b{/Shift}');
// Should not trigger the shortcut
expect(mockHandleShortcut).not.toHaveBeenCalled();
});
});
describe('Sidebar Toggle Functionality', () => {
it('should log the toggle event with correct parameters', async () => {
const user = userEvent.setup();
const mockHandleShortcut = jest.fn(() => {
logEvent('Global Shortcut: Sidebar Toggle', {
previousState: false,
newState: true,
});
});
render(
<QueryClientProvider client={queryClient}>
<KeyboardHotkeysProvider>
<TestComponent mockHandleShortcut={mockHandleShortcut} />
</KeyboardHotkeysProvider>
</QueryClientProvider>,
);
await user.keyboard(SHIFT_B_KEYBOARD_SHORTCUT);
expect(logEvent).toHaveBeenCalledWith('Global Shortcut: Sidebar Toggle', {
previousState: false,
newState: true,
});
});
it('should update user preference in context', async () => {
const user = userEvent.setup();
const mockHandleShortcut = jest.fn(() => {
const save = {
name: USER_PREFERENCES.SIDENAV_PINNED,
value: true,
};
mockUpdateUserPreferenceInContext(save);
});
render(
<QueryClientProvider client={queryClient}>
<KeyboardHotkeysProvider>
<TestComponent mockHandleShortcut={mockHandleShortcut} />
</KeyboardHotkeysProvider>
</QueryClientProvider>,
);
await user.keyboard(SHIFT_B_KEYBOARD_SHORTCUT);
expect(mockUpdateUserPreferenceInContext).toHaveBeenCalledWith({
name: USER_PREFERENCES.SIDENAV_PINNED,
value: true,
});
});
});
});

View File

@@ -10,8 +10,10 @@ import setLocalStorageApi from 'api/browser/localstorage/set';
import getChangelogByVersion from 'api/changelog/getChangelogByVersion';
import logEvent from 'api/common/logEvent';
import manageCreditCardApi from 'api/v1/portal/create';
import updateUserPreference from 'api/v1/user/preferences/name/update';
import getUserLatestVersion from 'api/v1/version/getLatestVersion';
import getUserVersion from 'api/v1/version/getVersion';
import { AxiosError } from 'axios';
import cx from 'classnames';
import ChangelogModal from 'components/ChangelogModal/ChangelogModal';
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
@@ -22,10 +24,12 @@ import { Events } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
import { GlobalShortcuts } from 'constants/shortcuts/globalShortcuts';
import { USER_PREFERENCES } from 'constants/userPreferences';
import SideNav from 'container/SideNav';
import TopNav from 'container/TopNav';
import dayjs from 'dayjs';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { useNotifications } from 'hooks/useNotifications';
@@ -68,8 +72,10 @@ import {
LicensePlatform,
LicenseState,
} from 'types/api/licensesV3/getActive';
import { UserPreference } from 'types/api/preferences/preference';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { showErrorNotification } from 'utils/error';
import { eventEmitter } from 'utils/getEventEmitter';
import {
getFormattedDate,
@@ -662,10 +668,85 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</div>
);
const sideNavPinned = userPreferences?.find(
const sideNavPinnedPreference = userPreferences?.find(
(preference) => preference.name === USER_PREFERENCES.SIDENAV_PINNED,
)?.value as boolean;
// Add loading state to prevent layout shift during initial load
const [isSidebarLoaded, setIsSidebarLoaded] = useState(false);
// Get sidebar state from localStorage as fallback until preferences are loaded
const getSidebarStateFromLocalStorage = useCallback((): boolean => {
try {
const storedValue = getLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED);
return storedValue === 'true';
} catch {
return false;
}
}, []);
// Set sidebar as loaded after user preferences are fetched
useEffect(() => {
if (userPreferences !== null) {
setIsSidebarLoaded(true);
}
}, [userPreferences]);
// Use localStorage value as fallback until preferences are loaded
const isSideNavPinned = isSidebarLoaded
? sideNavPinnedPreference
: getSidebarStateFromLocalStorage();
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
const { updateUserPreferenceInContext } = useAppContext();
const { mutate: updateUserPreferenceMutation } = useMutation(
updateUserPreference,
{
onError: (error) => {
showErrorNotification(notifications, error as AxiosError);
},
},
);
const handleToggleSidebar = useCallback((): void => {
const newState = !isSideNavPinned;
logEvent('Global Shortcut: Sidebar Toggle', {
previousState: isSideNavPinned,
newState,
});
// Save to localStorage immediately for instant feedback
setLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED, newState.toString());
// Update the context immediately
const save = {
name: USER_PREFERENCES.SIDENAV_PINNED,
value: newState,
};
updateUserPreferenceInContext(save as UserPreference);
// Make the API call in the background
updateUserPreferenceMutation({
name: USER_PREFERENCES.SIDENAV_PINNED,
value: newState,
});
}, [
isSideNavPinned,
updateUserPreferenceInContext,
updateUserPreferenceMutation,
]);
// Register the sidebar toggle shortcut
useEffect(() => {
registerShortcut(GlobalShortcuts.ToggleSidebar, handleToggleSidebar);
return (): void => {
deregisterShortcut(GlobalShortcuts.ToggleSidebar);
};
}, [registerShortcut, deregisterShortcut, handleToggleSidebar]);
const SHOW_TRIAL_EXPIRY_BANNER =
showTrialExpiryBanner && !showPaymentFailedWarning;
const SHOW_WORKSPACE_RESTRICTED_BANNER = showWorkspaceRestricted;
@@ -739,14 +820,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
className={cx(
'app-layout',
isDarkMode ? 'darkMode dark' : 'lightMode',
sideNavPinned ? 'side-nav-pinned' : '',
isSideNavPinned ? 'side-nav-pinned' : '',
SHOW_WORKSPACE_RESTRICTED_BANNER ? 'isWorkspaceRestricted' : '',
SHOW_TRIAL_EXPIRY_BANNER ? 'isTrialExpired' : '',
SHOW_PAYMENT_FAILED_BANNER ? 'isPaymentFailed' : '',
)}
>
{isToDisplayLayout && !renderFullScreen && (
<SideNav isPinned={sideNavPinned} />
<SideNav isPinned={isSideNavPinned} />
)}
<div
className={cx('app-content', {

View File

@@ -16,6 +16,7 @@ function ExplorerOptionWrapper({
sourcepage,
isOneChartPerQuery,
splitedQueries,
signalSource,
}: ExplorerOptionsWrapperProps): JSX.Element {
const [isExplorerOptionHidden, setIsExplorerOptionHidden] = useState(false);
@@ -32,6 +33,7 @@ function ExplorerOptionWrapper({
isLoading={isLoading}
onExport={onExport}
sourcepage={sourcepage}
signalSource={signalSource}
isExplorerOptionHidden={isExplorerOptionHidden}
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
isOneChartPerQuery={isOneChartPerQuery}

View File

@@ -1,12 +1,12 @@
.explorer-options-container {
position: fixed;
bottom: 24px;
bottom: 8px;
left: calc(50% + 240px);
transform: translate(calc(-50% - 120px), 0);
transition: left 0.2s linear;
display: flex;
gap: 16px;
gap: 8px;
background-color: transparent;
.multi-alert-button,
@@ -33,11 +33,12 @@
display: inline-flex;
align-items: center;
gap: 12px;
padding: 10px 10px;
border-radius: 50px;
border: 1px solid var(--bg-slate-400);
padding: 10px 12px;
background: rgba(22, 24, 29, 0.6);
border: 1px solid var(--bg-slate-500);
border-radius: 4px;
backdrop-filter: blur(20px);
box-sizing: border-box;
.action-icon {
display: flex;
@@ -64,9 +65,9 @@
.explorer-options {
padding: 10px 12px;
border: 1px solid var(--bg-slate-400);
border-radius: 50px;
background: rgba(22, 24, 29, 0.6);
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
backdrop-filter: blur(20px);
cursor: default;

View File

@@ -93,6 +93,7 @@ function ExplorerOptions({
onExport,
query,
sourcepage,
signalSource,
isExplorerOptionHidden = false,
setIsExplorerOptionHidden,
isOneChartPerQuery = false,
@@ -110,6 +111,7 @@ function ExplorerOptions({
const isLogsExplorer = sourcepage === DataSource.LOGS;
const isMetricsExplorer = sourcepage === DataSource.METRICS;
const isMeterExplorer = signalSource === 'meter';
const PRESERVED_VIEW_LOCAL_STORAGE_KEY = LOCALSTORAGE.LAST_USED_SAVED_VIEWS;
@@ -120,8 +122,11 @@ function ExplorerOptions({
if (isMetricsExplorer) {
return PreservedViewsTypes.METRICS;
}
if (isMeterExplorer) {
return PreservedViewsTypes.METER;
}
return PreservedViewsTypes.TRACES;
}, [isLogsExplorer, isMetricsExplorer]);
}, [isLogsExplorer, isMetricsExplorer, isMeterExplorer]);
const onModalToggle = useCallback((value: boolean) => {
setIsExport(value);
@@ -150,6 +155,10 @@ function ExplorerOptions({
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
panelType,
});
} else if (isMeterExplorer) {
logEvent('Meter Explorer: Save view clicked', {
panelType,
});
}
setIsSaveModalOpen(!isSaveModalOpen);
};
@@ -243,7 +252,7 @@ function ExplorerOptions({
error,
isRefetching,
refetch: refetchAllView,
} = useGetAllViews(sourcepage);
} = useGetAllViews(isMeterExplorer ? 'meter' : sourcepage);
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
@@ -316,7 +325,7 @@ function ExplorerOptions({
compositeQuery,
viewKey,
extraData: updatedExtraData,
sourcePage: sourcepage,
sourcePage: isMeterExplorer ? 'meter' : sourcepage,
viewName,
});
@@ -332,7 +341,7 @@ function ExplorerOptions({
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
viewKey,
extraData: updatedExtraData,
sourcePage: sourcepage,
sourcePage: isMeterExplorer ? 'meter' : sourcepage,
viewName,
},
{
@@ -459,6 +468,11 @@ function ExplorerOptions({
panelType,
viewName: option?.value,
});
} else if (isMeterExplorer) {
logEvent('Meter Explorer: Select view', {
panelType,
viewName: option?.value,
});
}
updatePreservedViewInLocalStorage(option);
@@ -505,6 +519,11 @@ function ExplorerOptions({
: defaultLogsSelectedColumns,
});
if (signalSource === 'meter') {
history.replace(ROUTES.METER_EXPLORER);
return;
}
history.replace(DATASOURCE_VS_ROUTES[sourcepage]);
};
@@ -549,7 +568,7 @@ function ExplorerOptions({
redirectWithQueryBuilderData,
refetchAllView,
saveViewAsync,
sourcePage: sourcepage,
sourcePage: isMeterExplorer ? 'meter' : sourcepage,
viewName: newViewName,
setNewViewName,
});
@@ -668,7 +687,7 @@ function ExplorerOptions({
return `Query ${query.builder.queryData[0].queryName}`;
};
const alertButton = useMemo(() => {
const CreateAlertButton = useMemo(() => {
if (isOneChartPerQuery) {
const selectLabel = (
<Button
@@ -721,7 +740,7 @@ function ExplorerOptions({
splitedQueries,
]);
const dashboardButton = useMemo(() => {
const AddToDashboardButton = useMemo(() => {
if (isOneChartPerQuery) {
const selectLabel = (
<Button
@@ -829,7 +848,7 @@ function ExplorerOptions({
style={{
background: extraData
? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)`
: 'transparent',
: 'initial',
}}
>
<div className="view-options">
@@ -884,10 +903,13 @@ function ExplorerOptions({
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
{alertButton}
{dashboardButton}
</div>
{signalSource !== 'meter' && (
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
{CreateAlertButton}
{AddToDashboardButton}
</div>
)}
<div className="actions">
{/* Hide the info icon for metrics explorer until we get the docs link */}
{!isMetricsExplorer && (
@@ -993,6 +1015,7 @@ export interface ExplorerOptionsProps {
query: Query | null;
disabled: boolean;
sourcepage: DataSource;
signalSource?: string;
isExplorerOptionHidden?: boolean;
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
isOneChartPerQuery?: boolean;
@@ -1005,6 +1028,7 @@ ExplorerOptions.defaultProps = {
setIsExplorerOptionHidden: undefined,
isOneChartPerQuery: false,
splitedQueries: [],
signalSource: '',
};
export default ExplorerOptions;

View File

@@ -2,4 +2,5 @@ export enum PreservedViewsTypes {
LOGS = 'logs',
TRACES = 'traces',
METRICS = 'metrics',
METER = 'meter',
}

View File

@@ -13,7 +13,7 @@ import { PreservedViewsTypes } from './constants';
export interface SaveNewViewHandlerProps {
viewName: string;
compositeQuery: ICompositeMetricQuery;
sourcePage: DataSource;
sourcePage: DataSource | 'meter';
extraData: SaveViewProps['extraData'];
panelType: PANEL_TYPES | null;
notifications: NotificationInstance;
@@ -32,7 +32,8 @@ export interface SaveNewViewHandlerProps {
export type PreservedViewType =
| PreservedViewsTypes.LOGS
| PreservedViewsTypes.TRACES
| PreservedViewsTypes.METRICS;
| PreservedViewsTypes.METRICS
| PreservedViewsTypes.METER;
export type PreservedViewsInLocalStorage = Partial<
Record<PreservedViewType, { key: string; value: string }>

View File

@@ -37,7 +37,7 @@ export const saveNewViewHandler = ({
{
viewName,
compositeQuery,
sourcePage,
sourcePage: sourcePage as DataSource,
extraData,
},
{

View File

@@ -28,6 +28,7 @@ import getTimeString from 'lib/getTimeString';
import history from 'lib/history';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { isEmpty } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -313,7 +314,7 @@ function ChartPreview({
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
?.active || false;
const isWarning = !!queryResponse.data?.warning?.message;
const isWarning = !isEmpty(queryResponse.data?.warning);
return (
<div className="alert-chart-container" ref={graphRef}>
<ChartContainer>

View File

@@ -240,8 +240,18 @@ function FormAlertRules({
const queryData = currentQuery.builder.queryData[index];
const updatedFunctions = updateFunctions(queryData);
queryData.functions = updatedFunctions;
handleSetQueryData(index, queryData);
// Only update if functions actually changed to avoid resetting aggregateAttribute
const currentFunctions = queryData.functions || [];
const functionsChanged = !isEqual(currentFunctions, updatedFunctions);
if (functionsChanged) {
const updatedQueryData = {
...queryData,
functions: updatedFunctions,
};
handleSetQueryData(index, updatedQueryData);
}
}
};

View File

@@ -301,7 +301,7 @@ function GridCardGraph({
widget={widget}
queryResponse={queryResponse}
errorMessage={errorMessage}
isWarning={!!queryResponse.data?.warning?.message}
isWarning={!isEmpty(queryResponse.data?.warning)}
version={version}
threshold={threshold}
headerMenuList={menuList}

View File

@@ -73,6 +73,8 @@ export default function TableRow({
{tableColumns.map((column) => {
if (!column.render) return <td>Empty</td>;
if (!column.key) return null;
const element: ColumnTypeRender<Record<string, unknown>> = column.render(
log[column.key as keyof Record<string, unknown>],
log,
@@ -97,6 +99,7 @@ export default function TableRow({
fontSize={fontSize}
columnKey={column.key as string}
onClick={handleShowLogDetails}
className={column.key as string}
>
{cloneElement(children, props)}
</TableCellStyled>

View File

@@ -136,7 +136,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
key={column.key}
fontSize={tableViewProps?.fontSize}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(isDragColumn && { className: 'dragHandler' })}
{...(isDragColumn && { className: `dragHandler ${column.key}` })}
columnKey={column.key as string}
>
{(column.title as string).replace(/^\w/, (c) => c.toUpperCase())}

View File

@@ -1,4 +1,5 @@
import { TelemetryFieldKey } from 'api/v5/v5';
import { isEmpty } from 'lodash-es';
import { IField } from 'types/api/logs/fields';
import {
IBuilderQuery,
@@ -8,11 +9,13 @@ import {
export const convertKeysToColumnFields = (
keys: TelemetryFieldKey[],
): IField[] =>
keys.map((item) => ({
dataType: item.fieldDataType ?? '',
name: item.name,
type: item.fieldContext ?? '',
}));
keys
.filter((item) => !isEmpty(item.name))
.map((item) => ({
dataType: item.fieldDataType ?? '',
name: item.name,
type: item.fieldContext ?? '',
}));
/**
* Determines if a query represents a trace-to-logs navigation
* by checking for the presence of a trace_id filter.

View File

@@ -0,0 +1,195 @@
.meter-explorer-container {
display: flex;
flex-direction: row;
.meter-explorer-quick-filters-section {
width: 280px;
border-right: 1px solid var(--bg-slate-500);
&.hidden {
display: none;
}
}
.meter-explorer-content-section {
width: 100%;
.explore-header {
display: flex;
align-items: center;
justify-content: space-between;
margin: 4px 0;
padding: 0 8px;
.explore-header-left-actions {
display: flex;
align-items: flex-start;
gap: 10px;
}
.explore-header-right-actions {
display: flex;
align-items: flex-end;
gap: 10px;
}
}
.query-section {
max-height: 450px;
overflow-y: auto;
.rc-virtual-list-holder {
height: 150px;
}
}
.explore-tabs {
margin: 15px 0;
.tab {
background-color: var(--bg-slate-500);
border-color: var(--bg-ink-200);
width: 180px;
padding: 16px 0;
display: flex;
justify-content: center;
align-items: center;
}
.tab:first-of-type {
border-top-left-radius: 2px;
}
.tab:last-of-type {
border-top-right-radius: 2px;
}
.selected-view {
background: var(--bg-ink-500);
}
}
.explore-content {
.ant-space {
margin-top: 10px;
margin-bottom: 20px;
}
.empty-meter-search {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.time-series-view-panel {
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
padding: 8px !important;
margin: 8px;
}
.time-series-container {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(100%, calc(50% - 8px)), 1fr)
);
gap: 16px;
width: 100%;
height: fit-content;
}
}
}
&.quick-filters-open {
.meter-explorer-content-section {
width: calc(100% - 280px);
}
}
padding-bottom: 80px;
}
.meter-time-series-container {
display: flex;
flex-direction: column;
gap: 10px;
.builder-units-filter {
padding: 0 8px;
margin-bottom: 0px !important;
.builder-units-filter-label {
margin-bottom: 0px !important;
font-size: 13px;
}
}
}
.lightMode {
.meter-explorer-container {
.explore-tabs {
.tab {
background-color: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-400);
}
.selected-view {
background: var(--bg-vanilla-500);
}
}
}
}
.dashboards-and-alerts-popover-container {
display: flex;
gap: 16px;
.dashboards-and-alerts-popover {
border-radius: 20px;
padding: 4px 8px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
gap: 4px;
&:hover {
opacity: 0.8;
}
}
.dashboards-popover {
border: 1px solid var(--bg-sienna-500);
.ant-typography {
color: var(--bg-sienna-500);
}
}
.alerts-popover {
border: 1px solid var(--bg-sakura-500);
.ant-typography {
color: var(--bg-sakura-500);
}
}
}
.no-data-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
gap: 16px;
.no-data-text {
color: var(--text-vanilla-500);
font-size: 14px;
line-height: 20px;
text-align: center;
}
}

View File

@@ -0,0 +1,182 @@
import './Explorer.styles.scss';
import * as Sentry from '@sentry/react';
import { Button, Tooltip } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
import { initialQueryMeterWithType, PANEL_TYPES } from 'constants/queryBuilder';
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { Filter } from 'lucide-react';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
import { v4 as uuid } from 'uuid';
import { MeterExplorerEventKeys, MeterExplorerEvents } from '../events';
import TimeSeries from './TimeSeries';
import { splitQueryIntoOneChartPerQuery } from './utils';
function Explorer(): JSX.Element {
const {
handleRunQuery,
stagedQuery,
updateAllQueriesOperators,
currentQuery,
} = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
const [showQuickFilters, setShowQuickFilters] = useState(true);
const defaultQuery = useMemo(
() =>
updateAllQueriesOperators(
initialQueryMeterWithType,
PANEL_TYPES.TIME_SERIES,
DataSource.METRICS,
'meter' as 'meter' | '',
),
[updateAllQueriesOperators],
);
const exportDefaultQuery = useMemo(
() =>
updateAllQueriesOperators(
currentQuery || initialQueryMeterWithType,
PANEL_TYPES.TIME_SERIES,
DataSource.METRICS,
'meter' as 'meter' | '',
),
[currentQuery, updateAllQueriesOperators],
);
useShareBuilderUrl({ defaultValue: defaultQuery });
const handleExport = useCallback(
(
dashboard: Dashboard | null,
_isNewDashboard?: boolean,
queryToExport?: Query,
): void => {
if (!dashboard) return;
const widgetId = uuid();
const dashboardEditView = generateExportToDashboardLink({
query: queryToExport || exportDefaultQuery,
panelType: PANEL_TYPES.TIME_SERIES,
dashboardId: dashboard.id,
widgetId,
});
safeNavigate(dashboardEditView);
},
[exportDefaultQuery, safeNavigate],
);
const splitedQueries = useMemo(
() =>
splitQueryIntoOneChartPerQuery(stagedQuery || initialQueryMeterWithType),
[stagedQuery],
);
useEffect(() => {
logEvent(MeterExplorerEvents.TabChanged, {
[MeterExplorerEventKeys.Tab]: 'explorer',
});
}, []);
const queryComponents = useMemo(
(): QueryBuilderProps['queryComponents'] => ({}),
[],
);
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div
className={cx('meter-explorer-container', {
'quick-filters-open': showQuickFilters,
})}
>
<div
className={cx('meter-explorer-quick-filters-section', {
hidden: !showQuickFilters,
})}
>
<QuickFilters
className="qf-meter-explorer"
source={QuickFiltersSource.METER_EXPLORER}
signal={SignalType.METER_EXPLORER}
showFilterCollapse
showQueryName={false}
handleFilterVisibilityChange={(): void => {
setShowQuickFilters(!showQuickFilters);
}}
/>
</div>
<div className="meter-explorer-content-section">
<div className="meter-explorer-explore-content">
<div className="explore-header">
<div className="explore-header-left-actions">
{!showQuickFilters && (
<Tooltip title="Show Quick Filters" placement="right" arrow={false}>
<Button
className="periscope-btn outline"
icon={<Filter size={16} />}
onClick={(): void => setShowQuickFilters(!showQuickFilters)}
/>
</Tooltip>
)}
</div>
<div className="explore-header-right-actions">
<DateTimeSelector showAutoRefresh />
<RightToolbarActions
onStageRunQuery={(): void => handleRunQuery(true, true)}
/>
</div>
</div>
<QueryBuilderV2
config={{
initialDataSource: DataSource.METRICS,
queryVariant: 'static',
signalSource: 'meter',
}}
panelType={PANEL_TYPES.TIME_SERIES}
queryComponents={queryComponents}
showFunctions={false}
version="v3"
/>
<div className="explore-content">
<TimeSeries />
</div>
</div>
<ExplorerOptionWrapper
disabled={!stagedQuery}
query={exportDefaultQuery}
sourcepage={DataSource.METRICS}
signalSource="meter"
onExport={handleExport}
isOneChartPerQuery={false}
splitedQueries={splitedQueries}
/>
</div>
</div>
</Sentry.ErrorBoundary>
);
}
export default Explorer;

View File

@@ -0,0 +1,13 @@
import { Typography } from 'antd';
import { ChartLine } from 'lucide-react';
export default function NoData(): JSX.Element {
return (
<div className="no-data-container">
<ChartLine size={48} />
<Typography.Text className="no-data-text">
No data found for the selected query
</Typography.Text>
</div>
);
}

View File

@@ -0,0 +1,43 @@
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QueryBuilder } from 'container/QueryBuilder';
import { ButtonWrapper } from 'container/TracesExplorer/QuerySection/styles';
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { DataSource } from 'types/common/queryBuilder';
import { MeterExplorerEventKeys, MeterExplorerEvents } from '../events';
function QuerySection(): JSX.Element {
const { handleRunQuery } = useQueryBuilder();
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.TIME_SERIES);
return (
<div className="query-section">
<QueryBuilder
panelType={panelTypes}
config={{ initialDataSource: DataSource.METRICS, queryVariant: 'static' }}
version="v4"
actions={
<ButtonWrapper>
<Button
onClick={(): void => {
handleRunQuery();
logEvent(MeterExplorerEvents.QueryBuilderQueryChanged, {
[MeterExplorerEventKeys.Tab]: 'explorer',
});
}}
type="primary"
>
Run Query
</Button>
</ButtonWrapper>
}
/>
</div>
);
}
export default QuerySection;

View File

@@ -0,0 +1,142 @@
import { isAxiosError } from 'axios';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { initialQueryMeterWithType, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useMemo, useState } from 'react';
import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import APIError from 'types/api/error';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
function TimeSeries(): JSX.Element {
const { stagedQuery, currentQuery } = useQueryBuilder();
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const [yAxisUnit, setYAxisUnit] = useState<string>('');
const isValidToConvertToMs = useMemo(() => {
const isValid: boolean[] = [];
currentQuery.builder.queryData.forEach(
({ aggregateAttribute, aggregateOperator }) => {
const isExistDurationNanoAttribute =
aggregateAttribute?.key === 'durationNano' ||
aggregateAttribute?.key === 'duration_nano';
const isCountOperator =
aggregateOperator === 'count' || aggregateOperator === 'count_distinct';
isValid.push(!isCountOperator && isExistDurationNanoAttribute);
},
);
return isValid.every(Boolean);
}, [currentQuery]);
const queryPayloads = useMemo(
() => [stagedQuery || initialQueryMeterWithType],
[stagedQuery],
);
const { showErrorModal } = useErrorModal();
const queries = useQueries(
queryPayloads.map((payload, index) => ({
queryKey: [
REACT_QUERY_KEY.GET_QUERY_RANGE,
payload,
ENTITY_VERSION_V5,
globalSelectedTime,
maxTime,
minTime,
index,
],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(
{
query: payload,
graphType: PANEL_TYPES.TIME_SERIES,
selectedTime: 'GLOBAL_TIME',
globalSelectedInterval: globalSelectedTime,
params: {
dataSource: DataSource.METRICS,
},
},
ENTITY_VERSION_V5,
),
enabled: !!payload,
retry: (failureCount: number, error: Error): boolean => {
let status: number | undefined;
if (error instanceof APIError) {
status = error.getHttpStatusCode();
} else if (isAxiosError(error)) {
status = error.response?.status;
}
if (status && status >= 400 && status < 500) {
return false;
}
return failureCount < 3;
},
onError: (error: APIError): void => {
showErrorModal(error);
},
})),
);
const data = useMemo(() => queries.map(({ data }) => data) ?? [], [queries]);
const responseData = useMemo(
() =>
data.map((datapoint) =>
isValidToConvertToMs ? convertDataValueToMs(datapoint) : datapoint,
),
[data, isValidToConvertToMs],
);
const onUnitChangeHandler = (value: string): void => {
setYAxisUnit(value);
};
return (
<div className="meter-time-series-container">
<BuilderUnitsFilter onChange={onUnitChangeHandler} yAxisUnit={yAxisUnit} />
<div className="time-series-container">
{responseData.map((datapoint, index) => (
<div
className="time-series-view-panel"
// eslint-disable-next-line react/no-array-index-key
key={index}
>
<TimeSeriesView
isFilterApplied={false}
isError={queries[index].isError}
isLoading={queries[index].isLoading}
data={datapoint}
dataSource={DataSource.METRICS}
yAxisUnit={yAxisUnit}
/>
</div>
))}
</div>
</div>
);
}
export default TimeSeries;

View File

@@ -0,0 +1,3 @@
import Explorer from './Explorer';
export default Explorer;

View File

@@ -0,0 +1,37 @@
import { RelatedMetric } from 'api/metricsExplorer/getRelatedMetrics';
import { UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
export enum ExplorerTabs {
TIME_SERIES = 'time-series',
RELATED_METRICS = 'related-metrics',
}
export interface TimeSeriesProps {
showOneChartPerQuery: boolean;
}
export interface RelatedMetricsProps {
metricNames: string[];
}
export interface RelatedMetricsCardProps {
metric: RelatedMetricWithQueryResult;
}
export interface UseGetRelatedMetricsGraphsProps {
selectedMetricName: string | null;
startMs: number;
endMs: number;
}
export interface UseGetRelatedMetricsGraphsReturn {
relatedMetrics: RelatedMetricWithQueryResult[];
isRelatedMetricsLoading: boolean;
isRelatedMetricsError: boolean;
}
export interface RelatedMetricWithQueryResult extends RelatedMetric {
queryResult: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>;
}

View File

@@ -0,0 +1,37 @@
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
export const splitQueryIntoOneChartPerQuery = (query: Query): Query[] => {
const queries: Query[] = [];
query.builder.queryData.forEach((currentQuery) => {
const newQuery = {
...query,
id: uuid(),
builder: {
...query.builder,
queryData: [currentQuery],
queryFormulas: [],
},
};
queries.push(newQuery);
});
query.builder.queryFormulas.forEach((currentFormula) => {
const newQuery = {
...query,
id: uuid(),
builder: {
...query.builder,
queryFormulas: [currentFormula],
queryData: query.builder.queryData.map((currentQuery) => ({
...currentQuery,
disabled: true,
})),
},
};
queries.push(newQuery);
});
return queries;
};

View File

@@ -0,0 +1,51 @@
/**
* This file contains all analytics events for the Meter Explorer.
*/
export enum MeterExplorerEvents {
TabChanged = 'Meter Explorer: Tab visited',
ModalOpened = 'Meter Explorer: Modal opened',
MeterClicked = 'Meter Explorer: Meter clicked',
FilterApplied = 'Meter Explorer: Filter applied',
TreemapViewChanged = 'Meter Explorer: Treemap view changed',
PageNumberChanged = 'Meter Explorer: Page number changed',
PageSizeChanged = 'Meter Explorer: Page size changed',
OrderByApplied = 'Meter Explorer: Order by applied',
MetricMetadataUpdated = 'Meter Explorer: Metric metadata updated',
OpenInExplorerClicked = 'Meter Explorer: Open in explorer clicked',
InspectViewChanged = 'Meter Explorer: Inspect view changed',
InspectQueryChanged = 'Meter Explorer: Inspect query changed',
InspectPointClicked = 'Meter Explorer: Inspect point clicked',
QueryBuilderQueryChanged = 'Meter Explorer: QueryBuilder query changed',
YAxisUnitApplied = 'Meter Explorer: Y axis unit applied',
AddToAlertClicked = 'Meter Explorer: Add to alert clicked',
AddToDashboardClicked = 'Meter Explorer: Add to dashboard clicked',
SaveViewClicked = 'Meter Explorer: Save view clicked',
SearchApplied = 'Meter Explorer: Search applied',
ViewEdited = 'Meter Explorer: View edited',
ViewDeleted = 'Meter Explorer: View deleted',
}
export enum MeterExplorerEventKeys {
Tab = 'tab',
Modal = 'modal',
View = 'view',
Interval = 'interval',
ViewType = 'viewType',
PageNumber = 'pageNumber',
PageSize = 'pageSize',
ColumnName = 'columnName',
Order = 'order',
AttributeKey = 'attributeKey',
AttributeValue = 'attributeValue',
MetricName = 'metricName',
InspectView = 'inspectView',
TimeAggregationOption = 'timeAggregationOption',
TimeAggregationInterval = 'timeAggregationInterval',
SpaceAggregationOption = 'spaceAggregationOption',
SpaceAggregationLabels = 'spaceAggregationLabels',
OneChartPerQueryEnabled = 'oneChartPerQueryEnabled',
YAxisUnit = 'yAxisUnit',
ViewName = 'viewName',
Filters = 'filters',
TimeRange = 'timeRange',
}

View File

@@ -13,6 +13,7 @@ import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { isEmpty } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom-v5-compat';
@@ -135,7 +136,7 @@ function Explorer(): JSX.Element {
/>
</div>
<div className="explore-header-right-actions">
{warning?.message && <WarningPopover warningData={warning} />}
{!isEmpty(warning) && <WarningPopover warningData={warning} />}
<DateTimeSelector showAutoRefresh />
<RightToolbarActions
onStageRunQuery={(): void => handleRunQuery(true, true)}
@@ -188,7 +189,7 @@ function Explorer(): JSX.Element {
query={exportDefaultQuery}
sourcepage={DataSource.METRICS}
onExport={handleExport}
isOneChartPerQuery={showOneChartPerQuery}
isOneChartPerQuery={false}
splitedQueries={splitedQueries}
/>
</Sentry.ErrorBoundary>

View File

@@ -1,5 +1,6 @@
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { SpaceAggregation, TimeAggregation } from 'api/v5/v5';
import { initialQueriesMap } from 'constants/queryBuilder';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -110,6 +111,15 @@ export function getMetricDetailsQuery(
isJSON: false,
dataType: DataTypes.String,
},
aggregations: [
{
metricName,
timeAggregation: timeAggregation as TimeAggregation,
spaceAggregation: spaceAggregation as SpaceAggregation,
reduceTo: 'avg',
temporality: '',
},
],
aggregateOperator,
timeAggregation,
spaceAggregation,

View File

@@ -1,6 +1,7 @@
import './MySettings.styles.scss';
import { Radio, RadioChangeEvent, Switch, Tag } from 'antd';
import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import updateUserPreference from 'api/v1/user/preferences/name/update';
import { AxiosError } from 'axios';
@@ -109,6 +110,9 @@ function MySettings(): JSX.Element {
// Optimistically update the UI
setSideNavPinned(checked);
// Save to localStorage immediately for instant feedback
setLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED, checked.toString());
// Update the context immediately
const save = {
name: USER_PREFERENCES.SIDENAV_PINNED,
@@ -130,6 +134,8 @@ function MySettings(): JSX.Element {
name: USER_PREFERENCES.SIDENAV_PINNED,
value: !checked,
} as UserPreference);
// Also revert localStorage
setLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED, (!checked).toString());
showErrorNotification(notifications, error as AxiosError);
},
},

View File

@@ -6,6 +6,7 @@ import { Card } from 'container/GridCardLayout/styles';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { isEmpty } from 'lodash-es';
import { memo } from 'react';
import { Warning } from 'types/api';
@@ -38,7 +39,7 @@ function WidgetGraph({
<div className="header">
<div className="header-left">
<PlotTag queryType={currentQuery.queryType} panelType={selectedGraph} />
{queryResponse.data?.warning?.message && (
{!isEmpty(queryResponse.data?.warning) && (
<WarningPopover warningData={queryResponse.data?.warning as Warning} />
)}
</div>

View File

@@ -6,6 +6,7 @@ import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKey
import useDebounce from 'hooks/useDebounce';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { has } from 'lodash-es';
import { AllTraceFilterKeyValue } from 'pages/TracesExplorer/Filter/filterUtils';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import { useCallback, useEffect, useMemo, useState } from 'react';
@@ -452,7 +453,9 @@ const useOptionsMenu = ({
() => ({
addColumn: {
isFetching: isSearchedAttributesFetchingV5,
value: preferences?.columns || defaultOptionsQuery.selectColumns,
value:
preferences?.columns.filter((item) => has(item, 'name')) ||
defaultOptionsQuery.selectColumns.filter((item) => has(item, 'name')),
options: optionsFromAttributeKeys || [],
onFocus: handleFocus,
onBlur: handleBlur,

View File

@@ -17,8 +17,9 @@ export type QueryBuilderConfig =
| {
queryVariant: 'static';
initialDataSource: DataSource;
signalSource?: string;
}
| { queryVariant: 'dropdown' };
| { queryVariant: 'dropdown'; signalSource?: string };
export type QueryBuilderProps = {
config?: QueryBuilderConfig;

View File

@@ -11,4 +11,5 @@ export type QueryProps = {
version: string;
showSpanScopeSelector?: boolean;
showOnlyWhereClause?: boolean;
signalSource?: string;
} & Pick<QueryBuilderProps, 'filterConfigs' | 'queryComponents'>;

View File

@@ -4,8 +4,9 @@ import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
export type AgregatorFilterProps = Pick<AutoCompleteProps, 'disabled'> & {
query: IBuilderQuery;
onChange: (value: BaseAutocompleteData) => void;
onChange: (value: BaseAutocompleteData, isEditMode?: boolean) => void;
defaultValue?: string;
onSelect?: (value: BaseAutocompleteData) => void;
index?: number;
signalSource?: 'meter' | '';
};

View File

@@ -38,6 +38,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
defaultValue,
onSelect,
index,
signalSource,
}: AgregatorFilterProps): JSX.Element {
const queryClient = useQueryClient();
const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]);
@@ -73,6 +74,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
searchText: debouncedValue,
aggregateOperator: queryAggregation.timeAggregation,
dataSource: query.dataSource,
source: signalSource || '',
}),
{
enabled:
@@ -135,7 +137,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
if (metricData) {
// Update the aggregateAttribute with the fetched type information
onChange(metricData);
onChange(metricData, true);
}
}
}, [
@@ -152,10 +154,17 @@ export const AggregatorFilter = memo(function AggregatorFilter({
setSearchText(text);
}, []);
const placeholder: string =
query.dataSource === DataSource.METRICS
? `Search metric name`
: 'Aggregate attribute';
const getPlaceholder = useCallback(() => {
if (signalSource === 'meter') {
return 'Meter name';
}
if (query.dataSource === DataSource.METRICS) {
return 'Metric name';
}
return 'Aggregate attribute';
}, [signalSource, query.dataSource]);
const getAttributesData = useCallback(
(): BaseAutocompleteData[] =>
@@ -214,11 +223,16 @@ export const AggregatorFilter = memo(function AggregatorFilter({
);
const handleBlur = useCallback(async () => {
if (searchText) {
if (searchText && searchText !== queryAggregation.metricName) {
const aggregateAttributes = await getResponseAttributes();
handleChangeCustomValue(searchText, aggregateAttributes);
}
}, [getResponseAttributes, handleChangeCustomValue, searchText]);
}, [
getResponseAttributes,
handleChangeCustomValue,
searchText,
queryAggregation?.metricName,
]);
const handleChange = useCallback(
(
@@ -284,7 +298,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
return (
<AutoComplete
getPopupContainer={popupContainer}
placeholder={placeholder}
placeholder={getPlaceholder()}
style={selectStyle}
filterOption={false}
onSearch={handleSearchText}

View File

@@ -30,8 +30,10 @@ function BuilderUnitsFilter({
};
return (
<Space>
<DefaultLabel>Y-axis unit</DefaultLabel>
<Space className="builder-units-filter">
<DefaultLabel className="builder-units-filter-label">
Y-axis unit
</DefaultLabel>
<Select
getPopupContainer={popupContainer}
style={selectStyles}

View File

@@ -5,4 +5,5 @@ export type GroupByFilterProps = {
query: IBuilderQuery;
onChange: (values: BaseAutocompleteData[]) => void;
disabled: boolean;
signalSource?: string;
};

View File

@@ -10,9 +10,17 @@ import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAut
// ** Helpers
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import { isEqual, uniqWith } from 'lodash-es';
import { memo, ReactNode, useCallback, useEffect, useState } from 'react';
import {
memo,
ReactNode,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { useQueryClient } from 'react-query';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -25,6 +33,7 @@ export const GroupByFilter = memo(function GroupByFilter({
query,
onChange,
disabled,
signalSource,
}: GroupByFilterProps): JSX.Element {
const queryClient = useQueryClient();
const [searchText, setSearchText] = useState<string>('');
@@ -38,10 +47,17 @@ export const GroupByFilter = memo(function GroupByFilter({
const debouncedValue = useDebounce(searchText, DEBOUNCE_DELAY);
const dataSource = useMemo(() => {
if (signalSource === 'meter') {
return 'meter' as DataSource;
}
return query.dataSource;
}, [signalSource, query.dataSource]);
const { isFetching } = useGetAggregateKeys(
{
aggregateAttribute: query.aggregateAttribute?.key || '',
dataSource: query.dataSource,
dataSource,
aggregateOperator: query.aggregateOperator || '',
searchText: debouncedValue,
},

View File

@@ -8,6 +8,7 @@ import {
Book,
Boxes,
BugIcon,
ChartArea,
Cloudy,
DraftingCompass,
FileKey2,
@@ -113,7 +114,7 @@ const menuItems: SidebarItem[] = [
key: ROUTES.METRICS_EXPLORER,
label: 'Metrics',
icon: <BarChart2 size={16} />,
isNew: true,
isNew: false,
itemKey: 'metrics',
},
{
@@ -230,7 +231,7 @@ export const defaultMoreMenuItems: SidebarItem[] = [
key: ROUTES.METRICS_EXPLORER,
label: 'Metrics',
icon: <BarChart2 size={16} />,
isNew: true,
isNew: false,
isEnabled: true,
itemKey: 'metrics',
},
@@ -264,6 +265,15 @@ export const defaultMoreMenuItems: SidebarItem[] = [
isEnabled: true,
itemKey: 'external-apis',
},
{
key: ROUTES.METER_EXPLORER,
label: 'Meter Explorer',
icon: <ChartArea size={16} />,
isNew: false,
isEnabled: false,
isBeta: true,
itemKey: 'meter-explorer',
},
{
key: ROUTES.MESSAGING_QUEUES_OVERVIEW,
label: 'Messaging Queues',

View File

@@ -1,7 +1,7 @@
.span-details-drawer {
display: flex;
flex-direction: column;
width: 330px;
width: 450px;
border-left: 1px solid var(--bg-slate-400);
overflow-y: auto;
@@ -176,6 +176,34 @@
justify-content: center;
width: 100%;
padding: 4px 8px;
margin-right: 8px;
gap: 4px;
.tab-label {
display: flex;
align-items: center;
}
.count-badge {
display: flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 6px;
border-radius: 10px;
background: rgba(171, 189, 255, 0.1);
color: var(--bg-vanilla-400);
font-variant-numeric: lining-nums tabular-nums slashed-zero;
font-feature-settings: 'dlig' on, 'salt' on;
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.065px;
text-transform: uppercase;
}
}
.attributes-tab-btn:hover,

View File

@@ -54,7 +54,10 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
icon={<Bookmark size="14" />}
className="attributes-tab-btn"
>
Attributes
<span className="tab-label">Attributes</span>
<span className="count-badge">
{Object.keys(span.tagMap || {}).length}
</span>
</Button>
),
key: 'attributes',
@@ -63,7 +66,8 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
{
label: (
<Button type="text" icon={<Anvil size="14" />} className="events-tab-btn">
Events
<span className="tab-label">Events</span>
<span className="count-badge">{span.event?.length || 0}</span>
</Button>
),
key: 'events',
@@ -82,7 +86,14 @@ function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element {
icon={<Link2 size="14" />}
className="linked-spans-tab-btn"
>
Links
<span className="tab-label">Links</span>
<span className="count-badge">
{
(
span.references?.filter((ref: any) => ref.refType !== 'CHILD_OF') || []
).length
}
</span>
</Button>
),
key: 'linked-spans',

View File

@@ -205,6 +205,7 @@ function TimeSeriesView({
return (
<div className="time-series-view">
{isError && error && <ErrorInPlace error={error as APIError} />}
<div
className="graph-container"
style={{ height: '100%', width: '100%' }}

View File

@@ -20,6 +20,7 @@ function TimeSeriesViewContainer({
dataSource = DataSource.TRACES,
isFilterApplied,
setWarning,
setIsLoadingQueries,
}: TimeSeriesViewProps): JSX.Element {
const { stagedQuery, currentQuery, panelType } = useQueryBuilder();
@@ -47,7 +48,7 @@ function TimeSeriesViewContainer({
return isValid.every(Boolean);
}, [currentQuery]);
const { data, isLoading, isError, error } = useGetQueryRange(
const { data, isLoading, isFetching, isError, error } = useGetQueryRange(
{
query: stagedQuery || initialQueriesMap[dataSource],
graphType: panelType || PANEL_TYPES.TIME_SERIES,
@@ -83,12 +84,20 @@ function TimeSeriesViewContainer({
[data, isValidToConvertToMs],
);
useEffect(() => {
if (isLoading || isFetching) {
setIsLoadingQueries(true);
} else {
setIsLoadingQueries(false);
}
}, [isLoading, isFetching, setIsLoadingQueries]);
return (
<TimeSeriesView
isFilterApplied={isFilterApplied}
isError={isError}
error={error as APIError}
isLoading={isLoading}
isLoading={isLoading || isFetching}
data={responseData}
yAxisUnit={isValidToConvertToMs ? 'ms' : 'short'}
dataSource={dataSource}
@@ -101,6 +110,7 @@ interface TimeSeriesViewProps {
dataSource?: DataSource;
isFilterApplied: boolean;
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
}
TimeSeriesViewContainer.defaultProps = {

View File

@@ -233,6 +233,9 @@ export const routesToSkip = [
ROUTES.ALL_ERROR,
ROUTES.UN_AUTHORIZED,
ROUTES.NOT_FOUND,
ROUTES.METER_EXPLORER,
ROUTES.METER_EXPLORER_BASE,
ROUTES.METER_EXPLORER_VIEWS,
ROUTES.SOMETHING_WENT_WRONG,
];

View File

@@ -372,7 +372,7 @@ function DateTimeSelection({
})),
},
};
return JSON.stringify(updatedCompositeQuery);
return encodeURIComponent(JSON.stringify(updatedCompositeQuery));
}, [currentQuery]);
const onSelectHandler = useCallback(

View File

@@ -49,9 +49,14 @@ import { getListColumns, transformDataWithDate } from './utils';
interface ListViewProps {
isFilterApplied: boolean;
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
}
function ListView({ isFilterApplied, setWarning }: ListViewProps): JSX.Element {
function ListView({
isFilterApplied,
setWarning,
setIsLoadingQueries,
}: ListViewProps): JSX.Element {
const {
stagedQuery,
panelType: panelTypeFromQueryBuilder,
@@ -162,6 +167,14 @@ function ListView({ isFilterApplied, setWarning }: ListViewProps): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data?.payload, data?.warning]);
useEffect(() => {
if (isLoading || isFetching) {
setIsLoadingQueries(true);
} else {
setIsLoadingQueries(false);
}
}, [isLoading, isFetching, setIsLoadingQueries]);
const dataLength =
data?.payload?.data?.newResult?.data?.result[0]?.list?.length;
const totalCount = useMemo(() => dataLength || 0, [dataLength]);

View File

@@ -16,8 +16,10 @@ import { GlobalReducer } from 'types/reducer/globalTime';
function TableView({
setWarning,
setIsLoadingQueries,
}: {
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
}): JSX.Element {
const { stagedQuery, panelType } = useQueryBuilder();
@@ -26,7 +28,7 @@ function TableView({
GlobalReducer
>((state) => state.globalTime);
const { data, isLoading, isError, error } = useGetQueryRange(
const { data, isLoading, isFetching, isError, error } = useGetQueryRange(
{
query: stagedQuery || initialQueriesMap.traces,
graphType: panelType || PANEL_TYPES.TABLE,
@@ -49,6 +51,14 @@ function TableView({
},
);
useEffect(() => {
if (isLoading || isFetching) {
setIsLoadingQueries(true);
} else {
setIsLoadingQueries(false);
}
}, [isLoading, isFetching, setIsLoadingQueries]);
const queryTableData = useMemo(
() =>
data?.payload?.data?.newResult?.data?.result ||

View File

@@ -40,11 +40,13 @@ import { ActionsContainer, Container } from './styles';
interface TracesViewProps {
isFilterApplied: boolean;
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
setIsLoadingQueries: Dispatch<SetStateAction<boolean>>;
}
function TracesView({
isFilterApplied,
setWarning,
setIsLoadingQueries,
}: TracesViewProps): JSX.Element {
const { stagedQuery, panelType } = useQueryBuilder();
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
@@ -117,6 +119,14 @@ function TracesView({
[responseData],
);
useEffect(() => {
if (isLoading || isFetching) {
setIsLoadingQueries(true);
} else {
setIsLoadingQueries(false);
}
}, [isLoading, isFetching, setIsLoadingQueries]);
useEffect(() => {
if (!isLoading && !isFetching && !isError && (tableData || []).length !== 0) {
logEvent('Traces Explorer: Data present', {

View File

@@ -212,7 +212,7 @@ export const useQueryOperations: UseQueryOperations = ({
);
const handleChangeAggregatorAttribute = useCallback(
(value: BaseAutocompleteData): void => {
(value: BaseAutocompleteData, isEditMode?: boolean): void => {
const newQuery: IBuilderQuery = {
...query,
aggregateAttribute: value,
@@ -226,30 +226,32 @@ export const useQueryOperations: UseQueryOperations = ({
handleMetricAggregateAtributeTypes(newQuery.aggregateAttribute);
}
if (newQuery.aggregateAttribute?.type === ATTRIBUTE_TYPES.SUM) {
newQuery.aggregateOperator = MetricAggregateOperator.RATE;
newQuery.timeAggregation = MetricAggregateOperator.RATE;
} else if (newQuery.aggregateAttribute?.type === ATTRIBUTE_TYPES.GAUGE) {
newQuery.aggregateOperator = MetricAggregateOperator.AVG;
newQuery.timeAggregation = MetricAggregateOperator.AVG;
} else {
newQuery.timeAggregation = '';
}
newQuery.spaceAggregation = '';
// Handled query with unknown metric to avoid 400 and 500 errors
// With metric value typed and not available then - time - 'avg', space - 'avg'
// If not typed - time - 'rate', space - 'sum', op - 'count'
if (isEmpty(newQuery.aggregateAttribute?.type)) {
if (!isEmpty(newQuery.aggregateAttribute?.key)) {
if (!isEditMode) {
if (newQuery.aggregateAttribute?.type === ATTRIBUTE_TYPES.SUM) {
newQuery.aggregateOperator = MetricAggregateOperator.RATE;
newQuery.timeAggregation = MetricAggregateOperator.RATE;
} else if (newQuery.aggregateAttribute?.type === ATTRIBUTE_TYPES.GAUGE) {
newQuery.aggregateOperator = MetricAggregateOperator.AVG;
newQuery.timeAggregation = MetricAggregateOperator.AVG;
newQuery.spaceAggregation = MetricAggregateOperator.AVG;
} else {
newQuery.aggregateOperator = MetricAggregateOperator.COUNT;
newQuery.timeAggregation = MetricAggregateOperator.RATE;
newQuery.spaceAggregation = MetricAggregateOperator.SUM;
newQuery.timeAggregation = '';
}
newQuery.spaceAggregation = '';
// Handled query with unknown metric to avoid 400 and 500 errors
// With metric value typed and not available then - time - 'avg', space - 'avg'
// If not typed - time - 'rate', space - 'sum', op - 'count'
if (isEmpty(newQuery.aggregateAttribute?.type)) {
if (!isEmpty(newQuery.aggregateAttribute?.key)) {
newQuery.aggregateOperator = MetricAggregateOperator.AVG;
newQuery.timeAggregation = MetricAggregateOperator.AVG;
newQuery.spaceAggregation = MetricAggregateOperator.AVG;
} else {
newQuery.aggregateOperator = MetricAggregateOperator.COUNT;
newQuery.timeAggregation = MetricAggregateOperator.RATE;
newQuery.spaceAggregation = MetricAggregateOperator.SUM;
}
}
}
}
@@ -262,61 +264,63 @@ export const useQueryOperations: UseQueryOperations = ({
handleMetricAggregateAtributeTypes(newQuery.aggregateAttribute);
}
if (newQuery.aggregateAttribute?.type === ATTRIBUTE_TYPES.SUM) {
newQuery.aggregations = [
{
timeAggregation: MetricAggregateOperator.RATE,
metricName: newQuery.aggregateAttribute?.key || '',
temporality: '',
spaceAggregation: '',
},
];
} else if (newQuery.aggregateAttribute?.type === ATTRIBUTE_TYPES.GAUGE) {
newQuery.aggregations = [
{
timeAggregation: MetricAggregateOperator.AVG,
metricName: newQuery.aggregateAttribute?.key || '',
temporality: '',
spaceAggregation: '',
},
];
} else {
newQuery.aggregations = [
{
timeAggregation: '',
metricName: newQuery.aggregateAttribute?.key || '',
temporality: '',
spaceAggregation: '',
},
];
}
newQuery.aggregateOperator = '';
newQuery.spaceAggregation = '';
// Handled query with unknown metric to avoid 400 and 500 errors
// With metric value typed and not available then - time - 'avg', space - 'avg'
// If not typed - time - 'rate', space - 'sum', op - 'count'
if (isEmpty(newQuery.aggregateAttribute?.type)) {
if (!isEmpty(newQuery.aggregateAttribute?.key)) {
if (!isEditMode) {
if (newQuery.aggregateAttribute?.type === ATTRIBUTE_TYPES.SUM) {
newQuery.aggregations = [
{
timeAggregation: MetricAggregateOperator.RATE,
metricName: newQuery.aggregateAttribute?.key || '',
temporality: '',
spaceAggregation: '',
},
];
} else if (newQuery.aggregateAttribute?.type === ATTRIBUTE_TYPES.GAUGE) {
newQuery.aggregations = [
{
timeAggregation: MetricAggregateOperator.AVG,
metricName: newQuery.aggregateAttribute?.key || '',
temporality: '',
spaceAggregation: MetricAggregateOperator.AVG,
spaceAggregation: '',
},
];
} else {
newQuery.aggregations = [
{
timeAggregation: MetricAggregateOperator.COUNT,
timeAggregation: '',
metricName: newQuery.aggregateAttribute?.key || '',
temporality: '',
spaceAggregation: MetricAggregateOperator.SUM,
spaceAggregation: '',
},
];
}
newQuery.aggregateOperator = '';
newQuery.spaceAggregation = '';
// Handled query with unknown metric to avoid 400 and 500 errors
// With metric value typed and not available then - time - 'avg', space - 'avg'
// If not typed - time - 'rate', space - 'sum', op - 'count'
if (isEmpty(newQuery.aggregateAttribute?.type)) {
if (!isEmpty(newQuery.aggregateAttribute?.key)) {
newQuery.aggregations = [
{
timeAggregation: MetricAggregateOperator.AVG,
metricName: newQuery.aggregateAttribute?.key || '',
temporality: '',
spaceAggregation: MetricAggregateOperator.AVG,
},
];
} else {
newQuery.aggregations = [
{
timeAggregation: MetricAggregateOperator.COUNT,
metricName: newQuery.aggregateAttribute?.key || '',
temporality: '',
spaceAggregation: MetricAggregateOperator.SUM,
},
];
}
}
}
}

View File

@@ -1,22 +1,44 @@
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
import { AxiosError, AxiosResponse } from 'axios';
import { useQuery, UseQueryResult } from 'react-query';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { ErrorResponse } from 'react-router-dom-v5-compat';
import { SuccessResponse } from 'types/api';
import { QueryKeyValueSuggestionsResponseProps } from 'types/api/querySuggestions/types';
export const useGetQueryKeyValueSuggestions = ({
key,
signal,
searchText,
signalSource,
metricName,
}: {
key: string;
signal: 'traces' | 'logs' | 'metrics';
searchText?: string;
signalSource?: 'meter' | '';
options?: UseQueryOptions<
SuccessResponse<QueryKeyValueSuggestionsResponseProps> | ErrorResponse
>;
metricName?: string;
}): UseQueryResult<
AxiosResponse<QueryKeyValueSuggestionsResponseProps>,
AxiosError
> =>
useQuery<AxiosResponse<QueryKeyValueSuggestionsResponseProps>, AxiosError>({
queryKey: ['queryKeyValueSuggestions', key, signal, searchText],
queryKey: [
'queryKeyValueSuggestions',
key,
signal,
searchText,
signalSource,
metricName,
],
queryFn: () =>
getValueSuggestions({ signal, key, searchText: searchText || '' }),
getValueSuggestions({
signal,
key,
searchText: searchText || '',
signalSource: signalSource as 'meter' | '',
metricName: metricName || '',
}),
});

View File

@@ -5,9 +5,9 @@ import { AllViewsProps } from 'types/api/saveViews/types';
import { DataSource } from 'types/common/queryBuilder';
export const useGetAllViews = (
sourcepage: DataSource,
sourcepage: DataSource | 'meter',
): UseQueryResult<AxiosResponse<AllViewsProps>, AxiosError> =>
useQuery<AxiosResponse<AllViewsProps>, AxiosError>({
queryKey: [{ sourcepage }],
queryFn: () => getAllViews(sourcepage),
queryFn: () => getAllViews(sourcepage as DataSource),
});

View File

@@ -17,9 +17,9 @@ const getChartData = ({
// eslint-disable-next-line sonarjs/cognitive-complexity
} => {
const uniqueTimeLabels = new Set<number>();
queryData.forEach((data) => {
data.queryData.forEach((query) => {
query.values.forEach((value) => {
queryData?.forEach((data) => {
data.queryData?.forEach((query) => {
query.values?.forEach((value) => {
uniqueTimeLabels.add(value[0]);
});
});
@@ -27,8 +27,8 @@ const getChartData = ({
const labels = Array.from(uniqueTimeLabels).sort((a, b) => a - b);
const response = queryData.map(
({ queryData, query: queryG, legend: legendG }) =>
const response =
queryData?.map(({ queryData, query: queryG, legend: legendG }) =>
queryData.map((e) => {
const { values = [], metric, legend, queryName } = e || {};
const labelNames = getLabelName(
@@ -61,7 +61,7 @@ const getChartData = ({
second: filledDataValues.map((e) => e.second || 0),
};
}),
);
) || [];
const modifiedData = response
.flat()

View File

@@ -490,6 +490,7 @@ export const defaultOutput = {
pageSize: 0,
queryName: 'A',
reduceTo: 'avg',
source: '',
spaceAggregation: 'sum',
stepInterval: 240,
timeAggregation: 'rate',

View File

@@ -28,7 +28,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { isEqual, isNull } from 'lodash-es';
import { isEmpty, isEqual, isNull } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -361,7 +361,7 @@ function LogsExplorer(): JSX.Element {
/>
}
warningElement={
warning?.message ? <WarningPopover warningData={warning} /> : <div />
!isEmpty(warning) ? <WarningPopover warningData={warning} /> : <div />
}
rightActions={
<RightToolbarActions

View File

@@ -6,7 +6,7 @@ import cx from 'classnames';
import { CardContainer } from 'container/GridCardLayout/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Widgets } from 'types/api/dashboard/getAll';
@@ -129,23 +129,22 @@ function MetricPage(): JSX.Element {
},
];
const [renderedGraphCount, setRenderedGraphCount] = useState(0);
const renderedGraphCountRef = useRef(0);
const hasLoggedRef = useRef(false);
const checkIfDataExists = (isDataAvailable: boolean): void => {
const checkIfDataExists = useCallback((isDataAvailable: boolean): void => {
if (isDataAvailable) {
const newCount = renderedGraphCount + 1;
setRenderedGraphCount(newCount);
renderedGraphCountRef.current += 1;
// Only log when first graph has rendered and we haven't logged yet
if (newCount === 1 && !hasLoggedRef.current) {
if (renderedGraphCountRef.current === 1 && !hasLoggedRef.current) {
logEvent('MQ Kafka: Metric view', {
graphRendered: true,
});
hasLoggedRef.current = true;
}
}
};
}, []);
return (
<div className="metric-page">

View File

@@ -0,0 +1,16 @@
.meter-explorer-page {
.ant-tabs-nav {
margin-bottom: 0;
}
.ant-tabs-tab {
padding: 8px 16px;
margin: 0 8px 0 0 !important;
.tab-item {
display: flex;
gap: 8px;
align-items: center;
}
}
}

View File

@@ -0,0 +1,22 @@
import './MeterExplorer.styles.scss';
import RouteTab from 'components/RouteTab';
import { TabRoutes } from 'components/RouteTab/types';
import history from 'lib/history';
import { useLocation } from 'react-use';
import { Explorer, Views } from './constants';
function MeterExplorerPage(): JSX.Element {
const { pathname } = useLocation();
const routes: TabRoutes[] = [Explorer, Views];
return (
<div className="meter-explorer-page">
<RouteTab routes={routes} activeKey={pathname} history={history} />
</div>
);
}
export default MeterExplorerPage;

View File

@@ -0,0 +1,32 @@
import { TabRoutes } from 'components/RouteTab/types';
import ROUTES from 'constants/routes';
import ExplorerPage from 'container/MeterExplorer/Explorer';
import { Compass, TowerControl } from 'lucide-react';
import SaveView from 'pages/SaveView';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
export const Explorer: TabRoutes = {
Component: (): JSX.Element => (
<PreferenceContextProvider>
<ExplorerPage />
</PreferenceContextProvider>
),
name: (
<div className="tab-item">
<Compass size={16} /> Explorer
</div>
),
route: ROUTES.METER_EXPLORER,
key: ROUTES.METER_EXPLORER,
};
export const Views: TabRoutes = {
Component: SaveView,
name: (
<div className="tab-item">
<TowerControl size={16} /> Views
</div>
),
route: ROUTES.METER_EXPLORER_VIEWS,
key: ROUTES.METER_EXPLORER_VIEWS,
};

View File

@@ -0,0 +1,3 @@
import MeterExplorerPage from './MeterExplorerPage';
export default MeterExplorerPage;

View File

@@ -6,6 +6,7 @@ export const SOURCEPAGE_VS_ROUTES: {
logs: ROUTES.LOGS_EXPLORER,
traces: ROUTES.TRACES_EXPLORER,
metrics: ROUTES.METRICS_EXPLORER_EXPLORER,
meter: ROUTES.METER_EXPLORER,
} as const;
export const ROUTES_VS_SOURCEPAGE: {
@@ -14,4 +15,5 @@ export const ROUTES_VS_SOURCEPAGE: {
[ROUTES.LOGS_SAVE_VIEWS]: 'logs',
[ROUTES.TRACES_SAVE_VIEWS]: 'traces',
[ROUTES.METRICS_EXPLORER_VIEWS]: 'metrics',
[ROUTES.METER_EXPLORER_VIEWS]: 'meter',
} as const;

View File

@@ -17,6 +17,10 @@ import {
} from 'components/ExplorerCard/utils';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getRandomColor } from 'container/ExplorerOptions/utils';
import {
MeterExplorerEventKeys,
MeterExplorerEvents,
} from 'container/MeterExplorer/events';
import {
MetricsExplorerEventKeys,
MetricsExplorerEvents,
@@ -163,6 +167,10 @@ function SaveView(): JSX.Element {
logEvent(MetricsExplorerEvents.TabChanged, {
[MetricsExplorerEventKeys.Tab]: 'views',
});
} else if (sourcepage === 'meter') {
logEvent(MeterExplorerEvents.TabChanged, {
[MeterExplorerEventKeys.Tab]: 'views',
});
}
logEventCalledRef.current = true;
}

View File

@@ -69,6 +69,7 @@ function TracesExplorer(): JSX.Element {
// Get panel type from URL
const panelTypesFromUrl = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
const [isLoadingQueries, setIsLoadingQueries] = useState<boolean>(false);
const [selectedView, setSelectedView] = useState<ExplorerViews>(() =>
getExplorerViewFromUrl(searchParams, panelTypesFromUrl),
@@ -318,11 +319,12 @@ function TracesExplorer(): JSX.Element {
/>
}
warningElement={
warning?.message ? <WarningPopover warningData={warning} /> : <div />
!isEmpty(warning) ? <WarningPopover warningData={warning} /> : <div />
}
rightActions={
<RightToolbarActions
onStageRunQuery={(): void => handleRunQuery(true, true)}
isLoadingQueries={isLoadingQueries}
/>
}
/>
@@ -344,13 +346,21 @@ function TracesExplorer(): JSX.Element {
{selectedView === ExplorerViews.LIST && (
<div className="trace-explorer-list-view">
<ListView isFilterApplied={isFilterApplied} setWarning={setWarning} />
<ListView
isFilterApplied={isFilterApplied}
setWarning={setWarning}
setIsLoadingQueries={setIsLoadingQueries}
/>
</div>
)}
{selectedView === ExplorerViews.TRACE && (
<div className="trace-explorer-traces-view">
<TracesView isFilterApplied={isFilterApplied} setWarning={setWarning} />
<TracesView
isFilterApplied={isFilterApplied}
setWarning={setWarning}
setIsLoadingQueries={setIsLoadingQueries}
/>
</div>
)}
@@ -360,13 +370,17 @@ function TracesExplorer(): JSX.Element {
dataSource={DataSource.TRACES}
isFilterApplied={isFilterApplied}
setWarning={setWarning}
setIsLoadingQueries={setIsLoadingQueries}
/>
</div>
)}
{selectedView === ExplorerViews.TABLE && (
<div className="trace-explorer-table-view">
<TableView setWarning={setWarning} />
<TableView
setWarning={setWarning}
setIsLoadingQueries={setIsLoadingQueries}
/>
</div>
)}
</div>

View File

@@ -241,12 +241,26 @@ export function QueryBuilderProvider({
);
const updateAllQueriesOperators = useCallback(
(query: Query, panelType: PANEL_TYPES, dataSource: DataSource): Query => {
(
query: Query,
panelType: PANEL_TYPES,
dataSource: DataSource,
signalSource?: 'meter' | '',
): Query => {
const queryData = query.builder.queryData?.map((item) =>
getElementWithActualOperator(item, dataSource, panelType),
);
return { ...query, builder: { ...query.builder, queryData } };
return {
...query,
builder: {
...query.builder,
queryData: queryData.map((item) => ({
...item,
source: signalSource,
})),
},
};
},
[getElementWithActualOperator],
@@ -854,6 +868,7 @@ export function QueryBuilderProvider({
const handleRunQuery = useCallback(
(shallUpdateStepInterval?: boolean, newQBQuery?: boolean) => {
let currentQueryData = currentQuery;
if (newQBQuery) {
currentQueryData = {
...currentQueryData,

View File

@@ -1,6 +1,7 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable no-empty */
import { TelemetryFieldKey } from 'api/v5/v5';
import { has } from 'lodash-es';
import { useEffect, useState } from 'react';
import { DataSource } from 'types/common/queryBuilder';
@@ -8,6 +9,14 @@ import logsLoaderConfig from '../configs/logsLoaderConfig';
import tracesLoaderConfig from '../configs/tracesLoaderConfig';
import { FormattingOptions, Preferences } from '../types';
const migrateColumns = (columns: any): any =>
columns.map((column: any) => {
if (has(column, 'key') && !has(column, 'name')) {
return { ...column, name: column.key };
}
return column;
});
// Generic preferences loader that works with any config
async function preferencesLoader<T>(config: {
priority: readonly string[];
@@ -26,11 +35,16 @@ async function preferencesLoader<T>(config: {
const validColumnsResult = results.find(
({ result }) => result.columns?.length,
);
const validFormattingResult = results.find(({ result }) => result.formatting);
const migratedColumns = validColumnsResult?.result.columns
? migrateColumns(validColumnsResult?.result.columns)
: undefined;
// Combine valid results or fallback to default
const finalResult = {
columns: validColumnsResult?.result.columns || config.default().columns,
columns: migratedColumns || config.default().columns,
formatting:
validFormattingResult?.result.formatting || config.default().formatting,
};

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