Compare commits

..

14 Commits

Author SHA1 Message Date
Karan Balani
3a92f33ca5 chore: update integration test flakiness 2026-06-10 18:28:15 +05:30
Karan Balani
2af4f81349 chore: fix openapi specs, lint issues and fix tests 2026-06-10 17:57:31 +05:30
Karan Balani
bfc89d79ff chore: minor changes to naming 2026-06-10 17:57:31 +05:30
Karan Balani
f9e8ce6f91 chore: remove unused parameter sqlstore in migration 2026-06-10 17:57:31 +05:30
Karan Balani
e0d87e216b refactor: sso config to support type and sso enabled in spec 2026-06-10 17:57:31 +05:30
Nikhil Mantri
27603e09d0 feat(infra-monitoring): v2 statefulsets integration tests (#11440)
* chore: updated logic and use centralized function in the module

* chore: filter metric groups

* chore: filter metric groups

* chore: formula correction

* chore: added step flooring note

* chore: comment correction

* chore: comment correction

* chore: removed function

* chore: renamed variables

* chore: added happy test

* chore: added test 2 for accuracy and test 3 for missing metrics check

* chore: added filter test 4

* chore: added 5th test for filterByStatus

* chore: added group by tests

* chore: pagination test added

* chore: added validation tests

* chore: added auth test

* chore: added all tests

* chore: fix for surfacing meta for pods custom group by

* chore: added nodes integration test suite

* chore: namespaces integration tests

* ci: register inframonitoring suite + ruff format 01_hosts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: added integration tests for clusters

* chore: formatting changed

* chore: formatting changed

* chore: formatting changed

* chore: added volumes integration tests

* chore: added deployments

* chore: added tests

* chore: added order by host.name test

* refactor(infra-monitoring): address review comments on hosts integration tests

- inline _post helper at call sites
- combine filter operator tests into parametrized test_hosts_filter
- combine bad attr/grammar tests into parametrized test_hosts_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_hosts_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align pods integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_pods_filter
- combine bad attr/grammar tests into parametrized test_pods_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_pods_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align nodes integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_nodes_filter
- combine bad attr/grammar tests into parametrized test_nodes_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_nodes_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align namespaces integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_namespaces_filter
- combine bad attr/grammar tests into parametrized test_namespaces_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_namespaces_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align clusters integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_clusters_filter
- combine bad attr/grammar tests into parametrized test_clusters_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_clusters_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align volumes integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_volumes_filter
- combine bad attr/grammar tests into parametrized test_volumes_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_volumes_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align deployments integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_deployments_filter
- combine bad attr/grammar tests into parametrized test_deployments_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_deployments_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align statefulsets integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_statefulsets_filter
- combine bad attr/grammar tests into parametrized test_statefulsets_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_statefulsets_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine hosts groupby tests into parametrized test

- merge test_hosts_groupby_hostname + test_hosts_groupby_os_type into
  test_hosts_groupby parametrized on (dataset, group key, expected counts/values)
- preserves all assertions incl hostName populated-vs-empty branch coverage

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine hosts orderby tests into parametrized test

- merge total_invariant_across_orderby + orderby_correctness + orderby_by_host_name
  into test_hosts_orderby parametrized on (column, record_field) x direction
- each case asserts both the total/len invariant and sortedness; sortedness now
  covered for all metric columns, not just cpu
- single dataset (hosts_orderby.jsonl) + CONTAINS 'order-' guard on all cases

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine hosts pagination tests

- fold offset-beyond-total case into the test_hosts_pagination page walk
  (offset K+5 expects 0 records via max(0, min(limit, K-offset)); total
  invariant covers the beyond-total page's total == K)
- single seed instead of two

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): merge hosts happy_path into test_hosts_accuracy

- happy_path and value_accuracy datasets were structurally identical
  (same 4 metrics, same sample counts, 2 hosts); one test now asserts
  shape/contract + exact metric values in a single seed/request
- drop unused hosts_happy_path.jsonl

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine pods integration tests logically

- merge happy_path into test_pods_accuracy (datasets structurally identical);
  drop unused pods_happy_path.jsonl
- merge groupby namespace/deployment into parametrized test_pods_groupby
- merge orderby invariant + correctness + by-pod-name into test_pods_orderby
  ((column, record_field) x direction; sortedness now covered for all columns)
- fold offset-beyond-total into test_pods_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine nodes integration tests logically

- merge happy_path into test_nodes_accuracy; drop unused nodes_happy_path.jsonl
- merge orderby invariant + correctness + by-node-name into test_nodes_orderby
  ((column, record_field) x direction; sortedness now covered for all columns)
- fold offset-beyond-total into test_nodes_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine namespaces integration tests logically

- merge happy_path into test_namespaces_accuracy; drop unused namespaces_happy_path.jsonl
- merge orderby invariant + correctness + by-namespace-name into test_namespaces_orderby
- fold offset-beyond-total into test_namespaces_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine clusters integration tests logically

- merge happy_path into test_clusters_accuracy; drop unused clusters_happy_path.jsonl
- merge orderby invariant + correctness + by-cluster-name into test_clusters_orderby
- fold offset-beyond-total into test_clusters_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine volumes integration tests logically

- merge happy_path into test_volumes_accuracy; drop unused volumes_happy_path.jsonl
- merge orderby invariant + correctness + by-pvc-name into test_volumes_orderby
- fold offset-beyond-total into test_volumes_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine deployments integration tests logically

- merge happy_path into test_deployments_accuracy; drop unused deployments_happy_path.jsonl
- merge orderby invariant + correctness + by-deployment-name into test_deployments_orderby
- fold offset-beyond-total into test_deployments_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine statefulsets integration tests logically

- merge happy_path into test_statefulsets_accuracy; drop unused statefulsets_happy_path.jsonl
- merge orderby invariant + correctness + by-statefulset-name into test_statefulsets_orderby
- fold offset-beyond-total into test_statefulsets_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in hosts filter tests

- regenerate hosts_filter_dataset.jsonl so every host mirrors the acc-h1
  sample pattern from hosts_value_accuracy.jsonl (CI-proven expected values)
- test_hosts_filter now asserts cpu/memory/wait/load15/diskUsage per filtered
  record against FILTER_DATASET_EXPECTED (1e-9), proving filters don't
  distort aggregation

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): scope filter expected values inside test_hosts_filter

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in pods filter tests

- regenerate pods_filter_dataset.jsonl so every pod mirrors the acc-p1
  sample pattern from pods_value_accuracy.jsonl (CI-proven expected values)
- test_pods_filter now asserts the 6 CPU/memory fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in nodes filter tests

- regenerate nodes_filter_dataset.jsonl so every node mirrors the acc-n1
  sample pattern from nodes_value_accuracy.jsonl (CI-proven expected values)
- test_nodes_filter now asserts the 4 CPU/memory fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in namespaces filter tests

- regenerate namespaces_filter_dataset.jsonl so every namespace mirrors the
  acc-ns-1 sample pattern (2 pods) from namespaces_value_accuracy.jsonl
- test_namespaces_filter now asserts namespaceCPU/namespaceMemory per record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in clusters filter tests

- regenerate clusters_filter_dataset.jsonl so every cluster mirrors the
  acc-cluster-1 sample pattern (2 nodes) from clusters_value_accuracy.jsonl
- test_clusters_filter now asserts the 4 CPU/memory fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in volumes filter tests

- regenerate volumes_filter_dataset.jsonl so every PVC mirrors the acc-pvc-1
  sample pattern from volumes_value_accuracy.jsonl (CI-proven expected values)
- test_volumes_filter now asserts all 6 volume fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in deployments filter tests

- regenerate deployments_filter_dataset.jsonl so every deployment mirrors the
  acc-dep-1 sample pattern (2 pods) from deployments_value_accuracy.jsonl
- test_deployments_filter now asserts the 6 CPU/memory fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in statefulsets filter tests

- regenerate statefulsets_filter_dataset.jsonl so every statefulset mirrors
  the acc-ss-1 sample pattern (2 pods) from statefulsets_value_accuracy.jsonl
- test_statefulsets_filter now asserts the 6 CPU/memory fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align nodes groupby + pod-phase tests with structure

- merge pod_phase_counts_list_mode + _no_pods_on_node into parametrized
  test_nodes_pod_phase_counts ((dataset, node, filter, expected) cases)
- rename groupby_cluster -> test_nodes_groupby, parametrize over
  k8s.node.name + k8s.cluster.name; adds the node-name-in-groupBy branch
  (nodeName populated, condition derived) that hosts/pods both cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align namespaces groupby with structure

- rename groupby_cluster -> test_namespaces_groupby, parametrize over
  k8s.namespace.name + k8s.cluster.name; adds the namespace-name-in-groupBy
  branch (namespaceName populated) that hosts/pods/nodes all cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align clusters groupby with structure

- rename groupby_cloud_provider -> test_clusters_groupby, parametrize over
  k8s.cluster.name + cloud.provider; adds the cluster-name-in-groupBy branch
  (clusterName populated) that hosts/pods/nodes/namespaces all cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align volumes groupby with structure

- rename groupby_namespace -> test_volumes_groupby, parametrize over
  k8s.persistentvolumeclaim.name + k8s.namespace.name; adds the
  pvc-name-in-groupBy branch (persistentVolumeClaimName populated) that the
  other endpoints all cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align deployments groupby with structure

- rename groupby_namespace -> test_deployments_groupby, parametrize over
  k8s.deployment.name + k8s.namespace.name; adds the deployment-name-in-groupBy
  branch (deploymentName populated) that the other endpoints all cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align statefulsets groupby with structure

- rename groupby_namespace -> test_statefulsets_groupby, parametrize over
  k8s.statefulset.name + k8s.namespace.name; adds the statefulset-name-in-groupBy
  branch (statefulSetName populated) that the other endpoints all cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 10:58:22 +00:00
Gaurav Tewari
7b2882abde feat: show recent queries to user (#11523)
* feat: initial commit

* refactor: recent query

* refactor: move all components into one

* refactor: queries

* refactor: self review comments

* refactor: css

* refactor: comments

* refactor: more comments

* feat: add time feature

* refactor : more changes

* chore: remove comments

* refactor: more code

* chore: remove extra commentes

* refactor: store in local storage

* fix: update types

* refactor: more changes

* refactor: import issue

* fix: lint

* chore: update styling

* fix: update query

* feat: use source as well in query key

* refactor: review changes

* refactor: self review changes

* refactor: more changes

* fix: more refactor

* refactor: make code much better

* fix: minor refactor

* refactor: review changes

* refactor: getRecentHook and make logic simple

* chore: drop useSaveRecentQuery from gridcard

* refactor: run save logic on handleRunQuery

* chore: remove space

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-06-10 10:58:19 +00:00
Nikhil Mantri
30f1c2d92d feat(infra-monitoring): v2 deployments integration tests (#11437)
* chore: updated logic and use centralized function in the module

* chore: filter metric groups

* chore: filter metric groups

* chore: formula correction

* chore: added step flooring note

* chore: comment correction

* chore: comment correction

* chore: removed function

* chore: renamed variables

* chore: added happy test

* chore: added test 2 for accuracy and test 3 for missing metrics check

* chore: added filter test 4

* chore: added 5th test for filterByStatus

* chore: added group by tests

* chore: pagination test added

* chore: added validation tests

* chore: added auth test

* chore: added all tests

* chore: fix for surfacing meta for pods custom group by

* chore: added nodes integration test suite

* chore: namespaces integration tests

* ci: register inframonitoring suite + ruff format 01_hosts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: added integration tests for clusters

* chore: formatting changed

* chore: formatting changed

* chore: formatting changed

* chore: added volumes integration tests

* chore: added deployments

* chore: added order by host.name test

* refactor(infra-monitoring): address review comments on hosts integration tests

- inline _post helper at call sites
- combine filter operator tests into parametrized test_hosts_filter
- combine bad attr/grammar tests into parametrized test_hosts_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_hosts_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align pods integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_pods_filter
- combine bad attr/grammar tests into parametrized test_pods_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_pods_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align nodes integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_nodes_filter
- combine bad attr/grammar tests into parametrized test_nodes_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_nodes_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align namespaces integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_namespaces_filter
- combine bad attr/grammar tests into parametrized test_namespaces_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_namespaces_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align clusters integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_clusters_filter
- combine bad attr/grammar tests into parametrized test_clusters_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_clusters_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align volumes integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_volumes_filter
- combine bad attr/grammar tests into parametrized test_volumes_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_volumes_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align deployments integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_deployments_filter
- combine bad attr/grammar tests into parametrized test_deployments_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_deployments_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine hosts groupby tests into parametrized test

- merge test_hosts_groupby_hostname + test_hosts_groupby_os_type into
  test_hosts_groupby parametrized on (dataset, group key, expected counts/values)
- preserves all assertions incl hostName populated-vs-empty branch coverage

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine hosts orderby tests into parametrized test

- merge total_invariant_across_orderby + orderby_correctness + orderby_by_host_name
  into test_hosts_orderby parametrized on (column, record_field) x direction
- each case asserts both the total/len invariant and sortedness; sortedness now
  covered for all metric columns, not just cpu
- single dataset (hosts_orderby.jsonl) + CONTAINS 'order-' guard on all cases

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine hosts pagination tests

- fold offset-beyond-total case into the test_hosts_pagination page walk
  (offset K+5 expects 0 records via max(0, min(limit, K-offset)); total
  invariant covers the beyond-total page's total == K)
- single seed instead of two

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): merge hosts happy_path into test_hosts_accuracy

- happy_path and value_accuracy datasets were structurally identical
  (same 4 metrics, same sample counts, 2 hosts); one test now asserts
  shape/contract + exact metric values in a single seed/request
- drop unused hosts_happy_path.jsonl

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine pods integration tests logically

- merge happy_path into test_pods_accuracy (datasets structurally identical);
  drop unused pods_happy_path.jsonl
- merge groupby namespace/deployment into parametrized test_pods_groupby
- merge orderby invariant + correctness + by-pod-name into test_pods_orderby
  ((column, record_field) x direction; sortedness now covered for all columns)
- fold offset-beyond-total into test_pods_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine nodes integration tests logically

- merge happy_path into test_nodes_accuracy; drop unused nodes_happy_path.jsonl
- merge orderby invariant + correctness + by-node-name into test_nodes_orderby
  ((column, record_field) x direction; sortedness now covered for all columns)
- fold offset-beyond-total into test_nodes_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine namespaces integration tests logically

- merge happy_path into test_namespaces_accuracy; drop unused namespaces_happy_path.jsonl
- merge orderby invariant + correctness + by-namespace-name into test_namespaces_orderby
- fold offset-beyond-total into test_namespaces_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine clusters integration tests logically

- merge happy_path into test_clusters_accuracy; drop unused clusters_happy_path.jsonl
- merge orderby invariant + correctness + by-cluster-name into test_clusters_orderby
- fold offset-beyond-total into test_clusters_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine volumes integration tests logically

- merge happy_path into test_volumes_accuracy; drop unused volumes_happy_path.jsonl
- merge orderby invariant + correctness + by-pvc-name into test_volumes_orderby
- fold offset-beyond-total into test_volumes_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine deployments integration tests logically

- merge happy_path into test_deployments_accuracy; drop unused deployments_happy_path.jsonl
- merge orderby invariant + correctness + by-deployment-name into test_deployments_orderby
- fold offset-beyond-total into test_deployments_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in hosts filter tests

- regenerate hosts_filter_dataset.jsonl so every host mirrors the acc-h1
  sample pattern from hosts_value_accuracy.jsonl (CI-proven expected values)
- test_hosts_filter now asserts cpu/memory/wait/load15/diskUsage per filtered
  record against FILTER_DATASET_EXPECTED (1e-9), proving filters don't
  distort aggregation

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): scope filter expected values inside test_hosts_filter

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in pods filter tests

- regenerate pods_filter_dataset.jsonl so every pod mirrors the acc-p1
  sample pattern from pods_value_accuracy.jsonl (CI-proven expected values)
- test_pods_filter now asserts the 6 CPU/memory fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in nodes filter tests

- regenerate nodes_filter_dataset.jsonl so every node mirrors the acc-n1
  sample pattern from nodes_value_accuracy.jsonl (CI-proven expected values)
- test_nodes_filter now asserts the 4 CPU/memory fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in namespaces filter tests

- regenerate namespaces_filter_dataset.jsonl so every namespace mirrors the
  acc-ns-1 sample pattern (2 pods) from namespaces_value_accuracy.jsonl
- test_namespaces_filter now asserts namespaceCPU/namespaceMemory per record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in clusters filter tests

- regenerate clusters_filter_dataset.jsonl so every cluster mirrors the
  acc-cluster-1 sample pattern (2 nodes) from clusters_value_accuracy.jsonl
- test_clusters_filter now asserts the 4 CPU/memory fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in volumes filter tests

- regenerate volumes_filter_dataset.jsonl so every PVC mirrors the acc-pvc-1
  sample pattern from volumes_value_accuracy.jsonl (CI-proven expected values)
- test_volumes_filter now asserts all 6 volume fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in deployments filter tests

- regenerate deployments_filter_dataset.jsonl so every deployment mirrors the
  acc-dep-1 sample pattern (2 pods) from deployments_value_accuracy.jsonl
- test_deployments_filter now asserts the 6 CPU/memory fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align nodes groupby + pod-phase tests with structure

- merge pod_phase_counts_list_mode + _no_pods_on_node into parametrized
  test_nodes_pod_phase_counts ((dataset, node, filter, expected) cases)
- rename groupby_cluster -> test_nodes_groupby, parametrize over
  k8s.node.name + k8s.cluster.name; adds the node-name-in-groupBy branch
  (nodeName populated, condition derived) that hosts/pods both cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align namespaces groupby with structure

- rename groupby_cluster -> test_namespaces_groupby, parametrize over
  k8s.namespace.name + k8s.cluster.name; adds the namespace-name-in-groupBy
  branch (namespaceName populated) that hosts/pods/nodes all cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align clusters groupby with structure

- rename groupby_cloud_provider -> test_clusters_groupby, parametrize over
  k8s.cluster.name + cloud.provider; adds the cluster-name-in-groupBy branch
  (clusterName populated) that hosts/pods/nodes/namespaces all cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align volumes groupby with structure

- rename groupby_namespace -> test_volumes_groupby, parametrize over
  k8s.persistentvolumeclaim.name + k8s.namespace.name; adds the
  pvc-name-in-groupBy branch (persistentVolumeClaimName populated) that the
  other endpoints all cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align deployments groupby with structure

- rename groupby_namespace -> test_deployments_groupby, parametrize over
  k8s.deployment.name + k8s.namespace.name; adds the deployment-name-in-groupBy
  branch (deploymentName populated) that the other endpoints all cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 10:35:23 +00:00
Nikhil Mantri
446dd4589f feat(infra-monitoring): v2 volumes integration tests (#11431)
Some checks failed
build-staging / staging (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore: updated logic and use centralized function in the module

* chore: filter metric groups

* chore: filter metric groups

* chore: formula correction

* chore: added step flooring note

* chore: comment correction

* chore: comment correction

* chore: removed function

* chore: renamed variables

* chore: added happy test

* chore: added test 2 for accuracy and test 3 for missing metrics check

* chore: added filter test 4

* chore: added 5th test for filterByStatus

* chore: added group by tests

* chore: pagination test added

* chore: added validation tests

* chore: added auth test

* chore: added all tests

* chore: fix for surfacing meta for pods custom group by

* chore: added nodes integration test suite

* chore: namespaces integration tests

* ci: register inframonitoring suite + ruff format 01_hosts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: added integration tests for clusters

* chore: formatting changed

* chore: formatting changed

* chore: formatting changed

* chore: added volumes integration tests

* chore: added order by host.name test

* refactor(infra-monitoring): address review comments on hosts integration tests

- inline _post helper at call sites
- combine filter operator tests into parametrized test_hosts_filter
- combine bad attr/grammar tests into parametrized test_hosts_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_hosts_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align pods integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_pods_filter
- combine bad attr/grammar tests into parametrized test_pods_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_pods_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align nodes integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_nodes_filter
- combine bad attr/grammar tests into parametrized test_nodes_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_nodes_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align namespaces integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_namespaces_filter
- combine bad attr/grammar tests into parametrized test_namespaces_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_namespaces_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align clusters integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_clusters_filter
- combine bad attr/grammar tests into parametrized test_clusters_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_clusters_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align volumes integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_volumes_filter
- combine bad attr/grammar tests into parametrized test_volumes_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_volumes_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine hosts groupby tests into parametrized test

- merge test_hosts_groupby_hostname + test_hosts_groupby_os_type into
  test_hosts_groupby parametrized on (dataset, group key, expected counts/values)
- preserves all assertions incl hostName populated-vs-empty branch coverage

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine hosts orderby tests into parametrized test

- merge total_invariant_across_orderby + orderby_correctness + orderby_by_host_name
  into test_hosts_orderby parametrized on (column, record_field) x direction
- each case asserts both the total/len invariant and sortedness; sortedness now
  covered for all metric columns, not just cpu
- single dataset (hosts_orderby.jsonl) + CONTAINS 'order-' guard on all cases

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine hosts pagination tests

- fold offset-beyond-total case into the test_hosts_pagination page walk
  (offset K+5 expects 0 records via max(0, min(limit, K-offset)); total
  invariant covers the beyond-total page's total == K)
- single seed instead of two

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): merge hosts happy_path into test_hosts_accuracy

- happy_path and value_accuracy datasets were structurally identical
  (same 4 metrics, same sample counts, 2 hosts); one test now asserts
  shape/contract + exact metric values in a single seed/request
- drop unused hosts_happy_path.jsonl

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine pods integration tests logically

- merge happy_path into test_pods_accuracy (datasets structurally identical);
  drop unused pods_happy_path.jsonl
- merge groupby namespace/deployment into parametrized test_pods_groupby
- merge orderby invariant + correctness + by-pod-name into test_pods_orderby
  ((column, record_field) x direction; sortedness now covered for all columns)
- fold offset-beyond-total into test_pods_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine nodes integration tests logically

- merge happy_path into test_nodes_accuracy; drop unused nodes_happy_path.jsonl
- merge orderby invariant + correctness + by-node-name into test_nodes_orderby
  ((column, record_field) x direction; sortedness now covered for all columns)
- fold offset-beyond-total into test_nodes_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine namespaces integration tests logically

- merge happy_path into test_namespaces_accuracy; drop unused namespaces_happy_path.jsonl
- merge orderby invariant + correctness + by-namespace-name into test_namespaces_orderby
- fold offset-beyond-total into test_namespaces_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine clusters integration tests logically

- merge happy_path into test_clusters_accuracy; drop unused clusters_happy_path.jsonl
- merge orderby invariant + correctness + by-cluster-name into test_clusters_orderby
- fold offset-beyond-total into test_clusters_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine volumes integration tests logically

- merge happy_path into test_volumes_accuracy; drop unused volumes_happy_path.jsonl
- merge orderby invariant + correctness + by-pvc-name into test_volumes_orderby
- fold offset-beyond-total into test_volumes_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in hosts filter tests

- regenerate hosts_filter_dataset.jsonl so every host mirrors the acc-h1
  sample pattern from hosts_value_accuracy.jsonl (CI-proven expected values)
- test_hosts_filter now asserts cpu/memory/wait/load15/diskUsage per filtered
  record against FILTER_DATASET_EXPECTED (1e-9), proving filters don't
  distort aggregation

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): scope filter expected values inside test_hosts_filter

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in pods filter tests

- regenerate pods_filter_dataset.jsonl so every pod mirrors the acc-p1
  sample pattern from pods_value_accuracy.jsonl (CI-proven expected values)
- test_pods_filter now asserts the 6 CPU/memory fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in nodes filter tests

- regenerate nodes_filter_dataset.jsonl so every node mirrors the acc-n1
  sample pattern from nodes_value_accuracy.jsonl (CI-proven expected values)
- test_nodes_filter now asserts the 4 CPU/memory fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in namespaces filter tests

- regenerate namespaces_filter_dataset.jsonl so every namespace mirrors the
  acc-ns-1 sample pattern (2 pods) from namespaces_value_accuracy.jsonl
- test_namespaces_filter now asserts namespaceCPU/namespaceMemory per record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in clusters filter tests

- regenerate clusters_filter_dataset.jsonl so every cluster mirrors the
  acc-cluster-1 sample pattern (2 nodes) from clusters_value_accuracy.jsonl
- test_clusters_filter now asserts the 4 CPU/memory fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in volumes filter tests

- regenerate volumes_filter_dataset.jsonl so every PVC mirrors the acc-pvc-1
  sample pattern from volumes_value_accuracy.jsonl (CI-proven expected values)
- test_volumes_filter now asserts all 6 volume fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align nodes groupby + pod-phase tests with structure

- merge pod_phase_counts_list_mode + _no_pods_on_node into parametrized
  test_nodes_pod_phase_counts ((dataset, node, filter, expected) cases)
- rename groupby_cluster -> test_nodes_groupby, parametrize over
  k8s.node.name + k8s.cluster.name; adds the node-name-in-groupBy branch
  (nodeName populated, condition derived) that hosts/pods both cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align namespaces groupby with structure

- rename groupby_cluster -> test_namespaces_groupby, parametrize over
  k8s.namespace.name + k8s.cluster.name; adds the namespace-name-in-groupBy
  branch (namespaceName populated) that hosts/pods/nodes all cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align clusters groupby with structure

- rename groupby_cloud_provider -> test_clusters_groupby, parametrize over
  k8s.cluster.name + cloud.provider; adds the cluster-name-in-groupBy branch
  (clusterName populated) that hosts/pods/nodes/namespaces all cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align volumes groupby with structure

- rename groupby_namespace -> test_volumes_groupby, parametrize over
  k8s.persistentvolumeclaim.name + k8s.namespace.name; adds the
  pvc-name-in-groupBy branch (persistentVolumeClaimName populated) that the
  other endpoints all cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 09:30:31 +00:00
primus-bot[bot]
c0c9039428 chore(release): bump to v0.128.0 (#11633)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-06-10 06:59:07 +00:00
Nikhil Soni
a014e9c0cb chore(trace-detail): waterfall v3 and v2 cleanup (#11609)
* chore: cleanup waterfall v3 api

We've moved to v4 as part of memory allocation optimisation

* chore: remove select all limit from waterfall request

It was added for testing only and fine tuning the limit value

* chore: update openapi specs

* chore: remove waterfall v2 since we've moved to v4 now
2026-06-10 05:15:36 +00:00
Vinicius Lourenço
b898269ddc chore(api): deprecate any hand-written api call (#11632) 2026-06-10 01:44:03 +00:00
swapnil-signoz
8fdad21a2e chore: bumping integration agent version to v0.0.11 (#11621)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-06-09 22:40:30 +00:00
Nikhil Mantri
2e0d25479a feat(infra-monitoring): v2 clusters integration tests (#11430)
* chore: updated logic and use centralized function in the module

* chore: filter metric groups

* chore: filter metric groups

* chore: formula correction

* chore: added step flooring note

* chore: comment correction

* chore: comment correction

* chore: removed function

* chore: renamed variables

* chore: added happy test

* chore: added test 2 for accuracy and test 3 for missing metrics check

* chore: added filter test 4

* chore: added 5th test for filterByStatus

* chore: added group by tests

* chore: pagination test added

* chore: added validation tests

* chore: added auth test

* chore: added all tests

* chore: fix for surfacing meta for pods custom group by

* chore: added nodes integration test suite

* chore: namespaces integration tests

* ci: register inframonitoring suite + ruff format 01_hosts

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: added integration tests for clusters

* chore: formatting changed

* chore: formatting changed

* chore: formatting changed

* chore: added order by host.name test

* refactor(infra-monitoring): address review comments on hosts integration tests

- inline _post helper at call sites
- combine filter operator tests into parametrized test_hosts_filter
- combine bad attr/grammar tests into parametrized test_hosts_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_hosts_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align pods integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_pods_filter
- combine bad attr/grammar tests into parametrized test_pods_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_pods_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align nodes integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_nodes_filter
- combine bad attr/grammar tests into parametrized test_nodes_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_nodes_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align namespaces integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_namespaces_filter
- combine bad attr/grammar tests into parametrized test_namespaces_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_namespaces_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align clusters integration tests with review feedback

- inline _post helper at call sites
- combine filter operator tests into parametrized test_clusters_filter
- combine bad attr/grammar tests into parametrized test_clusters_filter_invalid
- convert orderby total-invariant nested loop to stacked parametrize
- drop redundant test_clusters_auth (auth covered globally)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine hosts groupby tests into parametrized test

- merge test_hosts_groupby_hostname + test_hosts_groupby_os_type into
  test_hosts_groupby parametrized on (dataset, group key, expected counts/values)
- preserves all assertions incl hostName populated-vs-empty branch coverage

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine hosts orderby tests into parametrized test

- merge total_invariant_across_orderby + orderby_correctness + orderby_by_host_name
  into test_hosts_orderby parametrized on (column, record_field) x direction
- each case asserts both the total/len invariant and sortedness; sortedness now
  covered for all metric columns, not just cpu
- single dataset (hosts_orderby.jsonl) + CONTAINS 'order-' guard on all cases

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine hosts pagination tests

- fold offset-beyond-total case into the test_hosts_pagination page walk
  (offset K+5 expects 0 records via max(0, min(limit, K-offset)); total
  invariant covers the beyond-total page's total == K)
- single seed instead of two

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): merge hosts happy_path into test_hosts_accuracy

- happy_path and value_accuracy datasets were structurally identical
  (same 4 metrics, same sample counts, 2 hosts); one test now asserts
  shape/contract + exact metric values in a single seed/request
- drop unused hosts_happy_path.jsonl

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine pods integration tests logically

- merge happy_path into test_pods_accuracy (datasets structurally identical);
  drop unused pods_happy_path.jsonl
- merge groupby namespace/deployment into parametrized test_pods_groupby
- merge orderby invariant + correctness + by-pod-name into test_pods_orderby
  ((column, record_field) x direction; sortedness now covered for all columns)
- fold offset-beyond-total into test_pods_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine nodes integration tests logically

- merge happy_path into test_nodes_accuracy; drop unused nodes_happy_path.jsonl
- merge orderby invariant + correctness + by-node-name into test_nodes_orderby
  ((column, record_field) x direction; sortedness now covered for all columns)
- fold offset-beyond-total into test_nodes_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine namespaces integration tests logically

- merge happy_path into test_namespaces_accuracy; drop unused namespaces_happy_path.jsonl
- merge orderby invariant + correctness + by-namespace-name into test_namespaces_orderby
- fold offset-beyond-total into test_namespaces_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): combine clusters integration tests logically

- merge happy_path into test_clusters_accuracy; drop unused clusters_happy_path.jsonl
- merge orderby invariant + correctness + by-cluster-name into test_clusters_orderby
- fold offset-beyond-total into test_clusters_pagination page walk

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in hosts filter tests

- regenerate hosts_filter_dataset.jsonl so every host mirrors the acc-h1
  sample pattern from hosts_value_accuracy.jsonl (CI-proven expected values)
- test_hosts_filter now asserts cpu/memory/wait/load15/diskUsage per filtered
  record against FILTER_DATASET_EXPECTED (1e-9), proving filters don't
  distort aggregation

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): scope filter expected values inside test_hosts_filter

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in pods filter tests

- regenerate pods_filter_dataset.jsonl so every pod mirrors the acc-p1
  sample pattern from pods_value_accuracy.jsonl (CI-proven expected values)
- test_pods_filter now asserts the 6 CPU/memory fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in nodes filter tests

- regenerate nodes_filter_dataset.jsonl so every node mirrors the acc-n1
  sample pattern from nodes_value_accuracy.jsonl (CI-proven expected values)
- test_nodes_filter now asserts the 4 CPU/memory fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in namespaces filter tests

- regenerate namespaces_filter_dataset.jsonl so every namespace mirrors the
  acc-ns-1 sample pattern (2 pods) from namespaces_value_accuracy.jsonl
- test_namespaces_filter now asserts namespaceCPU/namespaceMemory per record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(infra-monitoring): assert metric value accuracy in clusters filter tests

- regenerate clusters_filter_dataset.jsonl so every cluster mirrors the
  acc-cluster-1 sample pattern (2 nodes) from clusters_value_accuracy.jsonl
- test_clusters_filter now asserts the 4 CPU/memory fields per filtered record

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align nodes groupby + pod-phase tests with structure

- merge pod_phase_counts_list_mode + _no_pods_on_node into parametrized
  test_nodes_pod_phase_counts ((dataset, node, filter, expected) cases)
- rename groupby_cluster -> test_nodes_groupby, parametrize over
  k8s.node.name + k8s.cluster.name; adds the node-name-in-groupBy branch
  (nodeName populated, condition derived) that hosts/pods both cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align namespaces groupby with structure

- rename groupby_cluster -> test_namespaces_groupby, parametrize over
  k8s.namespace.name + k8s.cluster.name; adds the namespace-name-in-groupBy
  branch (namespaceName populated) that hosts/pods/nodes all cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(infra-monitoring): align clusters groupby with structure

- rename groupby_cloud_provider -> test_clusters_groupby, parametrize over
  k8s.cluster.name + cloud.provider; adds the cluster-name-in-groupBy branch
  (clusterName populated) that hosts/pods/nodes/namespaces all cover

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 18:51:23 +00:00
200 changed files with 8453 additions and 7199 deletions

View File

@@ -190,7 +190,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.127.1
image: signoz/signoz:v0.128.0
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.127.1
image: signoz/signoz:v0.128.0
ports:
- "8080:8080" # signoz port
volumes:

View File

@@ -181,7 +181,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.127.1}
image: signoz/signoz:${VERSION:-v0.128.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -109,7 +109,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.127.1}
image: signoz/signoz:${VERSION:-v0.128.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -470,25 +470,6 @@ components:
role:
type: string
type: object
AuthtypesAuthDomainConfig:
oneOf:
- $ref: '#/components/schemas/AuthtypesSamlConfig'
- $ref: '#/components/schemas/AuthtypesGoogleConfig'
- $ref: '#/components/schemas/AuthtypesOIDCConfig'
properties:
googleAuthConfig:
$ref: '#/components/schemas/AuthtypesGoogleConfig'
oidcConfig:
$ref: '#/components/schemas/AuthtypesOIDCConfig'
roleMapping:
$ref: '#/components/schemas/AuthtypesRoleMapping'
samlConfig:
$ref: '#/components/schemas/AuthtypesSamlConfig'
ssoEnabled:
type: boolean
ssoType:
$ref: '#/components/schemas/AuthtypesAuthNProvider'
type: object
AuthtypesAuthNProvider:
enum:
- google_auth
@@ -515,6 +496,48 @@ components:
nullable: true
type: array
type: object
AuthtypesAuthProviderEnvelope:
discriminator:
mapping:
google_auth: '#/components/schemas/AuthtypesAuthProviderGoogle'
oidc: '#/components/schemas/AuthtypesAuthProviderOIDC'
saml: '#/components/schemas/AuthtypesAuthProviderSAML'
propertyName: type
oneOf:
- $ref: '#/components/schemas/AuthtypesAuthProviderSAML'
- $ref: '#/components/schemas/AuthtypesAuthProviderOIDC'
- $ref: '#/components/schemas/AuthtypesAuthProviderGoogle'
type: object
AuthtypesAuthProviderGoogle:
properties:
config:
$ref: '#/components/schemas/AuthtypesGoogleConfig'
type:
$ref: '#/components/schemas/AuthtypesAuthNProvider'
required:
- type
- config
type: object
AuthtypesAuthProviderOIDC:
properties:
config:
$ref: '#/components/schemas/AuthtypesOIDCConfig'
type:
$ref: '#/components/schemas/AuthtypesAuthNProvider'
required:
- type
- config
type: object
AuthtypesAuthProviderSAML:
properties:
config:
$ref: '#/components/schemas/AuthtypesSAMLConfig'
type:
$ref: '#/components/schemas/AuthtypesAuthNProvider'
required:
- type
- config
type: object
AuthtypesCallbackAuthNSupport:
properties:
provider:
@@ -526,8 +549,6 @@ components:
properties:
authNProviderInfo:
$ref: '#/components/schemas/AuthtypesAuthNProviderInfo'
config:
$ref: '#/components/schemas/AuthtypesAuthDomainConfig'
createdAt:
format: date-time
type: string
@@ -537,6 +558,12 @@ components:
type: string
orgId:
type: string
provider:
$ref: '#/components/schemas/AuthtypesAuthProviderEnvelope'
roleMapping:
$ref: '#/components/schemas/AuthtypesRoleMapping'
ssoEnabled:
type: boolean
updatedAt:
format: date-time
type: string
@@ -634,10 +661,14 @@ components:
type: object
AuthtypesPostableAuthDomain:
properties:
config:
$ref: '#/components/schemas/AuthtypesAuthDomainConfig'
name:
type: string
provider:
$ref: '#/components/schemas/AuthtypesAuthProviderEnvelope'
roleMapping:
$ref: '#/components/schemas/AuthtypesRoleMapping'
ssoEnabled:
type: boolean
type: object
AuthtypesPostableEmailPasswordSession:
properties:
@@ -710,7 +741,7 @@ components:
useRoleAttribute:
type: boolean
type: object
AuthtypesSamlConfig:
AuthtypesSAMLConfig:
properties:
attributeMapping:
$ref: '#/components/schemas/AuthtypesAttributeMapping'
@@ -745,8 +776,12 @@ components:
type: object
AuthtypesUpdatableAuthDomain:
properties:
config:
$ref: '#/components/schemas/AuthtypesAuthDomainConfig'
provider:
$ref: '#/components/schemas/AuthtypesAuthProviderEnvelope'
roleMapping:
$ref: '#/components/schemas/AuthtypesRoleMapping'
ssoEnabled:
type: boolean
type: object
AuthtypesUserRole:
properties:
@@ -6726,11 +6761,6 @@ components:
type: object
SpantypesGettableWaterfallTrace:
properties:
aggregations:
items:
$ref: '#/components/schemas/SpantypesSpanAggregationResult'
nullable: true
type: array
endTimestampMillis:
minimum: 0
type: integer
@@ -6818,14 +6848,6 @@ components:
type: object
SpantypesPostableWaterfall:
properties:
aggregations:
items:
$ref: '#/components/schemas/SpantypesSpanAggregation'
nullable: true
type: array
limit:
minimum: 0
type: integer
selectedSpanId:
type: string
uncollapsedSpans:
@@ -20681,76 +20703,6 @@ paths:
summary: Get flamegraph view for a trace
tags:
- tracedetail
/api/v3/traces/{traceID}/waterfall:
post:
deprecated: false
description: Returns the waterfall view of spans for a given trace ID with tree
structure, metadata, and windowed pagination
operationId: GetWaterfall
parameters:
- in: path
name: traceID
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SpantypesPostableWaterfall'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/SpantypesGettableWaterfallTrace'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get waterfall view for a trace
tags:
- tracedetail
/api/v4/traces/{traceID}/waterfall:
post:
deprecated: false

View File

@@ -53,7 +53,7 @@ func New(store authtypes.AuthNStore, licensing licensing.Licensing, providerSett
}
func (a *AuthN) LoginURL(ctx context.Context, siteURL *url.URL, authDomain *authtypes.AuthDomain) (string, error) {
if authDomain.AuthDomainConfig().AuthNProvider != authtypes.AuthNProviderOIDC {
if authDomain.AuthDomainConfig().Provider.Type != authtypes.AuthNProviderOIDC {
return "", errors.Newf(errors.TypeInternal, authtypes.ErrCodeAuthDomainMismatch, "domain type is not oidc")
}
@@ -106,14 +106,14 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
return nil, err
}
if claims == nil && authDomain.AuthDomainConfig().OIDC.GetUserInfo {
if claims == nil && authDomain.AuthDomainConfig().Oidc().GetUserInfo {
claims, err = a.claimsFromUserInfo(ctx, oidcProvider, token)
if err != nil {
return nil, err
}
}
emailClaim, ok := claims[authDomain.AuthDomainConfig().OIDC.ClaimMapping.Email].(string)
emailClaim, ok := claims[authDomain.AuthDomainConfig().Oidc().ClaimMapping.Email].(string)
if !ok {
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "oidc: missing email in claims")
}
@@ -123,7 +123,7 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "oidc: failed to parse email").WithAdditional(err.Error())
}
if !authDomain.AuthDomainConfig().OIDC.InsecureSkipEmailVerified {
if !authDomain.AuthDomainConfig().Oidc().InsecureSkipEmailVerified {
emailVerifiedClaim, ok := claims["email_verified"].(bool)
if !ok {
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "oidc: missing email_verified in claims")
@@ -135,14 +135,14 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
}
name := ""
if nameClaim := authDomain.AuthDomainConfig().OIDC.ClaimMapping.Name; nameClaim != "" {
if nameClaim := authDomain.AuthDomainConfig().Oidc().ClaimMapping.Name; nameClaim != "" {
if n, ok := claims[nameClaim].(string); ok {
name = n
}
}
var groups []string
if groupsClaim := authDomain.AuthDomainConfig().OIDC.ClaimMapping.Groups; groupsClaim != "" {
if groupsClaim := authDomain.AuthDomainConfig().Oidc().ClaimMapping.Groups; groupsClaim != "" {
if claimValue, exists := claims[groupsClaim]; exists {
switch g := claimValue.(type) {
case []any:
@@ -161,7 +161,7 @@ func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtype
}
role := ""
if roleClaim := authDomain.AuthDomainConfig().OIDC.ClaimMapping.Role; roleClaim != "" {
if roleClaim := authDomain.AuthDomainConfig().Oidc().ClaimMapping.Role; roleClaim != "" {
if r, ok := claims[roleClaim].(string); ok {
role = r
}
@@ -177,11 +177,11 @@ func (a *AuthN) ProviderInfo(ctx context.Context, authDomain *authtypes.AuthDoma
}
func (a *AuthN) oidcProviderAndoauth2Config(ctx context.Context, siteURL *url.URL, authDomain *authtypes.AuthDomain) (*oidc.Provider, *oauth2.Config, error) {
if authDomain.AuthDomainConfig().OIDC.IssuerAlias != "" {
ctx = oidc.InsecureIssuerURLContext(ctx, authDomain.AuthDomainConfig().OIDC.IssuerAlias)
if authDomain.AuthDomainConfig().Oidc().IssuerAlias != "" {
ctx = oidc.InsecureIssuerURLContext(ctx, authDomain.AuthDomainConfig().Oidc().IssuerAlias)
}
oidcProvider, err := oidc.NewProvider(ctx, authDomain.AuthDomainConfig().OIDC.Issuer)
oidcProvider, err := oidc.NewProvider(ctx, authDomain.AuthDomainConfig().Oidc().Issuer)
if err != nil {
return nil, nil, err
}
@@ -194,8 +194,8 @@ func (a *AuthN) oidcProviderAndoauth2Config(ctx context.Context, siteURL *url.UR
}
return oidcProvider, &oauth2.Config{
ClientID: authDomain.AuthDomainConfig().OIDC.ClientID,
ClientSecret: authDomain.AuthDomainConfig().OIDC.ClientSecret,
ClientID: authDomain.AuthDomainConfig().Oidc().ClientID,
ClientSecret: authDomain.AuthDomainConfig().Oidc().ClientSecret,
Endpoint: oidcProvider.Endpoint(),
Scopes: scopes,
RedirectURL: (&url.URL{
@@ -212,7 +212,7 @@ func (a *AuthN) claimsFromIDToken(ctx context.Context, authDomain *authtypes.Aut
return nil, errors.New(errors.TypeNotFound, errors.CodeNotFound, "oidc: no id_token in token response")
}
verifier := provider.Verifier(&oidc.Config{ClientID: authDomain.AuthDomainConfig().OIDC.ClientID})
verifier := provider.Verifier(&oidc.Config{ClientID: authDomain.AuthDomainConfig().Oidc().ClientID})
idToken, err := verifier.Verify(ctx, rawIDToken)
if err != nil {
return nil, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "oidc: failed to verify token").WithAdditional(err.Error())

View File

@@ -40,7 +40,7 @@ func New(ctx context.Context, store authtypes.AuthNStore, licensing licensing.Li
}
func (a *AuthN) LoginURL(ctx context.Context, siteURL *url.URL, authDomain *authtypes.AuthDomain) (string, error) {
if authDomain.AuthDomainConfig().AuthNProvider != authtypes.AuthNProviderSAML {
if authDomain.AuthDomainConfig().Provider.Type != authtypes.AuthNProviderSAML {
return "", errors.Newf(errors.TypeInternal, authtypes.ErrCodeAuthDomainMismatch, "saml: domain type is not saml")
}
@@ -101,19 +101,19 @@ func (a *AuthN) HandleCallback(ctx context.Context, formValues url.Values) (*aut
}
name := ""
if nameAttribute := authDomain.AuthDomainConfig().SAML.AttributeMapping.Name; nameAttribute != "" {
if nameAttribute := authDomain.AuthDomainConfig().Saml().AttributeMapping.Name; nameAttribute != "" {
if val := assertionInfo.Values.Get(nameAttribute); val != "" {
name = val
}
}
var groups []string
if groupAttribute := authDomain.AuthDomainConfig().SAML.AttributeMapping.Groups; groupAttribute != "" {
if groupAttribute := authDomain.AuthDomainConfig().Saml().AttributeMapping.Groups; groupAttribute != "" {
groups = assertionInfo.Values.GetAll(groupAttribute)
}
role := ""
if roleAttribute := authDomain.AuthDomainConfig().SAML.AttributeMapping.Role; roleAttribute != "" {
if roleAttribute := authDomain.AuthDomainConfig().Saml().AttributeMapping.Role; roleAttribute != "" {
if val := assertionInfo.Values.Get(roleAttribute); val != "" {
role = val
}
@@ -142,11 +142,11 @@ func (a *AuthN) serviceProvider(siteURL *url.URL, authDomain *authtypes.AuthDoma
// The ServiceProviderIssuer is the client id in case of keycloak. Since we set it to the host here, we need to set the client id == host in keycloak.
// For AWSSSO, this is the value of Application SAML audience.
return &saml2.SAMLServiceProvider{
IdentityProviderSSOURL: authDomain.AuthDomainConfig().SAML.SamlIdp,
IdentityProviderIssuer: authDomain.AuthDomainConfig().SAML.SamlEntity,
IdentityProviderSSOURL: authDomain.AuthDomainConfig().Saml().SamlIdp,
IdentityProviderIssuer: authDomain.AuthDomainConfig().Saml().SamlEntity,
ServiceProviderIssuer: siteURL.Host,
AssertionConsumerServiceURL: acsURL.String(),
SignAuthnRequests: !authDomain.AuthDomainConfig().SAML.InsecureSkipAuthNRequestsSigned,
SignAuthnRequests: !authDomain.AuthDomainConfig().Saml().InsecureSkipAuthNRequestsSigned,
AllowMissingAttributes: true,
IDPCertificateStore: certStore,
SPKeyStore: dsig.RandomKeyStoreForTest(),
@@ -159,15 +159,15 @@ func (a *AuthN) getCertificateStore(authDomain *authtypes.AuthDomain) (dsig.X509
}
var certBytes []byte
if strings.Contains(authDomain.AuthDomainConfig().SAML.SamlCert, "-----BEGIN CERTIFICATE-----") {
block, _ := pem.Decode([]byte(authDomain.AuthDomainConfig().SAML.SamlCert))
if strings.Contains(authDomain.AuthDomainConfig().Saml().SamlCert, "-----BEGIN CERTIFICATE-----") {
block, _ := pem.Decode([]byte(authDomain.AuthDomainConfig().Saml().SamlCert))
if block == nil {
return certStore, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "no valid pem cert found")
}
certBytes = block.Bytes
} else {
certData, err := base64.StdEncoding.DecodeString(authDomain.AuthDomainConfig().SAML.SamlCert)
certData, err := base64.StdEncoding.DecodeString(authDomain.AuthDomainConfig().Saml().SamlCert)
if err != nil {
return certStore, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to read certificate: %s", err.Error())
}

View File

@@ -5,6 +5,13 @@ import convertObjectIntoParams from 'lib/query/convertObjectIntoParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/alerts/getTriggered';
/**
* @deprecated Use the generated `useGetAlerts` hook (or `getAlerts` fetcher) from
* `api/generated/services/alerts` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const getTriggered = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createEmail';
/**
* @deprecated Use the generated `useCreateChannel` hook (or `createChannel` fetcher) from
* `api/generated/services/channels` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const create = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createMsTeams';
/**
* @deprecated Use the generated `useCreateChannel` hook (or `createChannel` fetcher) from
* `api/generated/services/channels` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const create = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createOpsgenie';
/**
* @deprecated Use the generated `useCreateChannel` hook (or `createChannel` fetcher) from
* `api/generated/services/channels` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const create = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createPager';
/**
* @deprecated Use the generated `useCreateChannel` hook (or `createChannel` fetcher) from
* `api/generated/services/channels` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const create = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createSlack';
/**
* @deprecated Use the generated `useCreateChannel` hook (or `createChannel` fetcher) from
* `api/generated/services/channels` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const create = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/createWebhook';
/**
* @deprecated Use the generated `useCreateChannel` hook (or `createChannel` fetcher) from
* `api/generated/services/channels` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const create = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/delete';
/**
* @deprecated Use the generated `useDeleteChannelByID` hook (or `deleteChannelByID` fetcher) from
* `api/generated/services/channels` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const deleteChannel = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/editEmail';
/**
* @deprecated Use the generated `useUpdateChannelByID` hook (or `updateChannelByID` fetcher) from
* `api/generated/services/channels` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const editEmail = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/editMsTeams';
/**
* @deprecated Use the generated `useUpdateChannelByID` hook (or `updateChannelByID` fetcher) from
* `api/generated/services/channels` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const editMsTeams = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorResponse, ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/editOpsgenie';
/**
* @deprecated Use the generated `useUpdateChannelByID` hook (or `updateChannelByID` fetcher) from
* `api/generated/services/channels` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const editOpsgenie = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps> | ErrorResponse> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/editPager';
/**
* @deprecated Use the generated `useUpdateChannelByID` hook (or `updateChannelByID` fetcher) from
* `api/generated/services/channels` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const editPager = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/editSlack';
/**
* @deprecated Use the generated `useUpdateChannelByID` hook (or `updateChannelByID` fetcher) from
* `api/generated/services/channels` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const editSlack = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/editWebhook';
/**
* @deprecated Use the generated `useUpdateChannelByID` hook (or `updateChannelByID` fetcher) from
* `api/generated/services/channels` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const editWebhook = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -5,6 +5,13 @@ import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/channels/get';
import { Channels } from 'types/api/channels/getAll';
/**
* @deprecated Use the generated `useGetChannelByID` hook (or `getChannelByID` fetcher) from
* `api/generated/services/channels` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const get = async (props: Props): Promise<SuccessResponseV2<Channels>> => {
try {
const response = await axios.get<PayloadProps>(`/channels/${props.id}`);

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Channels, PayloadProps } from 'types/api/channels/getAll';
/**
* @deprecated Use the generated `useListChannels` hook (or `listChannels` fetcher) from
* `api/generated/services/channels` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const getAll = async (): Promise<SuccessResponseV2<Channels[]>> => {
try {
const response = await axios.get<PayloadProps>('/channels');

View File

@@ -5,6 +5,13 @@ import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constan
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { CreatePublicDashboardProps } from 'types/api/dashboard/public/create';
/**
* @deprecated Use the generated `useCreatePublicDashboard` hook (or `createPublicDashboard` fetcher) from
* `api/generated/services/dashboard` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const createPublicDashboard = async (
props: CreatePublicDashboardProps,
): Promise<SuccessResponseV2<CreatePublicDashboardProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { GetPublicDashboardDataProps, PayloadProps,PublicDashboardDataProps } from 'types/api/dashboard/public/get';
/**
* @deprecated Use the generated `useGetPublicDashboardData` hook (or `getPublicDashboardData` fetcher) from
* `api/generated/services/dashboard` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const getPublicDashboardData = async (props: GetPublicDashboardDataProps): Promise<SuccessResponseV2<PublicDashboardDataProps>> => {
try {
const response = await axios.get<PayloadProps>(`/public/dashboards/${props.id}`);

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { GetPublicDashboardMetaProps, PayloadProps,PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta';
/**
* @deprecated Use the generated `useGetPublicDashboard` hook (or `getPublicDashboard` fetcher) from
* `api/generated/services/dashboard` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const getPublicDashboardMeta = async (props: GetPublicDashboardMetaProps): Promise<SuccessResponseV2<PublicDashboardMetaProps>> => {
try {
const response = await axios.get<PayloadProps>(`/dashboards/${props.id}/public`);

View File

@@ -6,6 +6,13 @@ import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { GetPublicDashboardWidgetDataProps } from 'types/api/dashboard/public/getWidgetData';
/**
* @deprecated Use the generated `useGetPublicDashboardWidgetQueryRange` hook (or `getPublicDashboardWidgetQueryRange` fetcher) from
* `api/generated/services/dashboard` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const getPublicDashboardWidgetData = async (props: GetPublicDashboardWidgetDataProps): Promise<SuccessResponseV2<MetricRangePayloadV5>> => {
try {
const response = await axios.get(`/public/dashboards/${props.id}/widgets/${props.index}/query_range`, {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps,RevokePublicDashboardAccessProps } from 'types/api/dashboard/public/delete';
/**
* @deprecated Use the generated `useDeletePublicDashboard` hook (or `deletePublicDashboard` fetcher) from
* `api/generated/services/dashboard` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const revokePublicDashboardAccess = async (
props: RevokePublicDashboardAccessProps,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -5,6 +5,13 @@ import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constan
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { UpdatePublicDashboardProps } from 'types/api/dashboard/public/update';
/**
* @deprecated Use the generated `useUpdatePublicDashboard` hook (or `updatePublicDashboard` fetcher) from
* `api/generated/services/dashboard` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const updatePublicDashboard = async (
props: UpdatePublicDashboardProps,
): Promise<SuccessResponseV2<UpdatePublicDashboardProps>> => {

View File

@@ -9,6 +9,13 @@ interface ISubstituteVars {
compositeQuery: ICompositeMetricQuery;
}
/**
* @deprecated Use the generated `useReplaceVariables` hook (or `replaceVariables` fetcher) from
* `api/generated/services/querier` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
export const getSubstituteVars = async (
props?: Partial<QueryRangePayloadV5>,
signal?: AbortSignal,

View File

@@ -8,6 +8,12 @@ import { FieldKeyResponse } from 'types/api/dynamicVariables/getFieldKeys';
* Get field keys for a given signal type
* @param signal Type of signal (traces, logs, metrics)
* @param name Optional search text
*
* @deprecated Use the generated `useGetFieldsKeys` hook (or `getFieldsKeys` fetcher) from
* `api/generated/services/fields` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
export const getFieldKeys = async (
signal?: 'traces' | 'logs' | 'metrics',

View File

@@ -11,6 +11,12 @@ import { FieldValueResponse } from 'types/api/dynamicVariables/getFieldValues';
* @param name Name of the attribute for which values are being fetched
* @param value Optional search text
* @param existingQuery Optional existing query - across all present dynamic variables
*
* @deprecated Use the generated `useGetFieldsValues` hook (or `getFieldsValues` fetcher) from
* `api/generated/services/fields` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
export const getFieldValues = async (
signal?: 'traces' | 'logs' | 'metrics',

View File

@@ -8095,10 +8095,6 @@ export interface SpantypesWaterfallSpanDTO {
}
export interface SpantypesGettableWaterfallTraceDTO {
/**
* @type array,null
*/
aggregations?: SpantypesSpanAggregationResultDTO[] | null;
/**
* @type integer
* @minimum 0
@@ -8218,15 +8214,6 @@ export interface SpantypesPostableTraceAggregationsDTO {
}
export interface SpantypesPostableWaterfallDTO {
/**
* @type array,null
*/
aggregations?: SpantypesSpanAggregationDTO[] | null;
/**
* @type integer
* @minimum 0
*/
limit?: number;
/**
* @type string
*/
@@ -10521,17 +10508,6 @@ export type GetFlamegraph200 = {
status: string;
};
export type GetWaterfallPathParameters = {
traceID: string;
};
export type GetWaterfall200 = {
data: SpantypesGettableWaterfallTraceDTO;
/**
* @type string
*/
status: string;
};
export type GetWaterfallV4PathParameters = {
traceID: string;
};

View File

@@ -16,8 +16,6 @@ import type {
GetFlamegraphPathParameters,
GetTraceAggregations200,
GetTraceAggregationsPathParameters,
GetWaterfall200,
GetWaterfallPathParameters,
GetWaterfallV4200,
GetWaterfallV4PathParameters,
RenderErrorResponseDTO,
@@ -228,105 +226,6 @@ export const useGetFlamegraph = <
> => {
return useMutation(getGetFlamegraphMutationOptions(options));
};
/**
* Returns the waterfall view of spans for a given trace ID with tree structure, metadata, and windowed pagination
* @summary Get waterfall view for a trace
*/
export const getWaterfall = (
{ traceID }: GetWaterfallPathParameters,
spantypesPostableWaterfallDTO?: BodyType<SpantypesPostableWaterfallDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetWaterfall200>({
url: `/api/v3/traces/${traceID}/waterfall`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: spantypesPostableWaterfallDTO,
signal,
});
};
export const getGetWaterfallMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof getWaterfall>>,
TError,
{
pathParams: GetWaterfallPathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof getWaterfall>>,
TError,
{
pathParams: GetWaterfallPathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
},
TContext
> => {
const mutationKey = ['getWaterfall'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof getWaterfall>>,
{
pathParams: GetWaterfallPathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return getWaterfall(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type GetWaterfallMutationResult = NonNullable<
Awaited<ReturnType<typeof getWaterfall>>
>;
export type GetWaterfallMutationBody =
| BodyType<SpantypesPostableWaterfallDTO>
| undefined;
export type GetWaterfallMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get waterfall view for a trace
*/
export const useGetWaterfall = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof getWaterfall>>,
TError,
{
pathParams: GetWaterfallPathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof getWaterfall>>,
TError,
{
pathParams: GetWaterfallPathParameters;
data?: BodyType<SpantypesPostableWaterfallDTO>;
},
TContext
> => {
return useMutation(getGetWaterfallMutationOptions(options));
};
/**
* Returns the waterfall view of spans including all spans if total spans are under a limit, a max count otherwise. Aggregations are dropped compared to v3
* @summary Get waterfall view for a trace

View File

@@ -5,6 +5,13 @@ import {
QueryKeySuggestionsResponseProps,
} from 'types/api/querySuggestions/types';
/**
* @deprecated Use the generated `useGetFieldsKeys` hook (or `getFieldsKeys` fetcher) from
* `api/generated/services/fields` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
export const getKeySuggestions = (
props: QueryKeyRequestProps,
): Promise<AxiosResponse<QueryKeySuggestionsResponseProps>> => {

View File

@@ -5,6 +5,13 @@ import {
QueryKeyValueSuggestionsResponseProps,
} from 'types/api/querySuggestions/types';
/**
* @deprecated Use the generated `useGetFieldsValues` hook (or `getFieldsValues` fetcher) from
* `api/generated/services/fields` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
export const getValueSuggestions = (
props: QueryKeyValueRequestProps,
): Promise<AxiosResponse<QueryKeyValueSuggestionsResponseProps>> => {

View File

@@ -15,6 +15,13 @@ export interface CreateRoutingPolicyResponse {
message: string;
}
/**
* @deprecated Use the generated `useCreateRoutePolicy` hook (or `createRoutePolicy` fetcher) from
* `api/generated/services/routepolicies` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const createRoutingPolicy = async (
props: CreateRoutingPolicyBody,
): Promise<

View File

@@ -8,6 +8,13 @@ export interface DeleteRoutingPolicyResponse {
message: string;
}
/**
* @deprecated Use the generated `useDeleteRoutePolicyByID` hook (or `deleteRoutePolicyByID` fetcher) from
* `api/generated/services/routepolicies` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const deleteRoutingPolicy = async (
routingPolicyId: string,
): Promise<

View File

@@ -20,6 +20,13 @@ export interface GetRoutingPoliciesResponse {
data?: ApiRoutingPolicy[];
}
/**
* @deprecated Use the generated `useGetAllRoutePolicies` hook (or `getAllRoutePolicies` fetcher) from
* `api/generated/services/routepolicies` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
export const getRoutingPolicies = async (
signal?: AbortSignal,
headers?: Record<string, string>,

View File

@@ -15,6 +15,13 @@ export interface UpdateRoutingPolicyResponse {
message: string;
}
/**
* @deprecated Use the generated `useUpdateRoutePolicy` hook (or `updateRoutePolicy` fetcher) from
* `api/generated/services/routepolicies` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const updateRoutingPolicy = async (
id: string,
props: UpdateRoutingPolicyBody,

View File

@@ -27,7 +27,6 @@ const getTraceV4 = async (
{
selectedSpanId: props.selectedSpanId,
uncollapsedSpans,
limit: 10000,
},
);

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/user/resetPassword';
/**
* @deprecated Use the generated `useResetPassword` hook (or `resetPassword` fetcher) from
* `api/generated/services/users` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const resetPassword = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { UsersProps } from 'types/api/user/inviteUsers';
/**
* @deprecated Use the generated `useCreateBulkInvite` hook (or `createBulkInvite` fetcher) from
* `api/generated/services/users` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const inviteUsers = async (
users: UsersProps,
): Promise<SuccessResponseV2<null>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/user/setInvite';
/**
* @deprecated Use the generated `useCreateInvite` hook (or `createInvite` fetcher) from
* `api/generated/services/users` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const sendInvite = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {

View File

@@ -5,6 +5,13 @@ import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps } from 'types/api/preferences/list';
import { OrgPreference } from 'types/api/preferences/preference';
/**
* @deprecated Use the generated `useListOrgPreferences` hook (or `listOrgPreferences` fetcher) from
* `api/generated/services/preferences` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const listPreference = async (): Promise<
SuccessResponseV2<OrgPreference[]>
> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Props } from 'types/api/preferences/update';
/**
* @deprecated Use the generated `useUpdateOrgPreference` hook (or `updateOrgPreference` fetcher) from
* `api/generated/services/preferences` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const update = async (props: Props): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.put(`/org/preferences/${props.name}`, {

View File

@@ -5,6 +5,13 @@ import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps } from 'types/api/preferences/list';
import { UserPreference } from 'types/api/preferences/preference';
/**
* @deprecated Use the generated `useListUserPreferences` hook (or `listUserPreferences` fetcher) from
* `api/generated/services/preferences` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const list = async (): Promise<SuccessResponseV2<UserPreference[]>> => {
try {
const response = await axios.get<PayloadProps>(`/user/preferences`);

View File

@@ -5,6 +5,13 @@ import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/preferences/get';
import { UserPreference } from 'types/api/preferences/preference';
/**
* @deprecated Use the generated `useGetUserPreference` hook (or `getUserPreference` fetcher) from
* `api/generated/services/preferences` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const get = async (
props: Props,
): Promise<SuccessResponseV2<UserPreference>> => {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Props } from 'types/api/preferences/update';
/**
* @deprecated Use the generated `useUpdateUserPreference` hook (or `updateUserPreference` fetcher) from
* `api/generated/services/preferences` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const update = async (props: Props): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.put(`/user/preferences/${props.name}`, {

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, RawSuccessResponse, SuccessResponseV2 } from 'types/api';
import { Props, SessionsContext } from 'types/api/v2/sessions/context/get';
/**
* @deprecated Use the generated `useGetSessionContext` hook (or `getSessionContext` fetcher) from
* `api/generated/services/sessions` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const get = async (
props: Props,
): Promise<SuccessResponseV2<SessionsContext>> => {

View File

@@ -3,6 +3,13 @@ import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, RawSuccessResponse, SuccessResponseV2 } from 'types/api';
/**
* @deprecated Use the generated `useDeleteSession` hook (or `deleteSession` fetcher) from
* `api/generated/services/sessions` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const deleteSession = async (): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.delete<RawSuccessResponse<null>>('/sessions');

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, RawSuccessResponse, SuccessResponseV2 } from 'types/api';
import { Props, Token } from 'types/api/v2/sessions/email_password/post';
/**
* @deprecated Use the generated `useCreateSessionByEmailPassword` hook (or `createSessionByEmailPassword` fetcher) from
* `api/generated/services/sessions` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const post = async (props: Props): Promise<SuccessResponseV2<Token>> => {
try {
const response = await axios.post<RawSuccessResponse<Token>>(

View File

@@ -4,6 +4,13 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, RawSuccessResponse, SuccessResponseV2 } from 'types/api';
import { Props, Token } from 'types/api/v2/sessions/rotate/post';
/**
* @deprecated Use the generated `useRotateSession` hook (or `rotateSession` fetcher) from
* `api/generated/services/sessions` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const post = async (props: Props): Promise<SuccessResponseV2<Token>> => {
try {
const response = await axios.post<RawSuccessResponse<Token>>(

View File

@@ -8,6 +8,13 @@ import {
QueryRangePayloadV5,
} from 'types/api/v5/queryRange';
/**
* @deprecated Use the generated `useQueryRangeV5` hook (or `queryRangeV5` fetcher) from
* `api/generated/services/querier` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
export const getQueryRangeV5 = async (
props: QueryRangePayloadV5,
version: string,

View File

@@ -1,5 +1,11 @@
// TODO: Improve the styling of the query aggregation container and its components. - @YounixM , @H4ad
$dropdown-base-height: 250px;
$recents-header-height: 30px;
$recent-row-height: 36px;
// how many recents are rendered, this caps how tall the dropdown can grow to fit them.
$max-recents-shown: 5;
.code-mirror-where-clause {
width: 100%;
display: flex;
@@ -117,7 +123,23 @@
width: 100% !important;
max-width: 100% !important;
font-family: 'Space Mono', monospace !important;
min-height: 200px !important;
max-height: $dropdown-base-height !important;
overflow-y: auto !important;
// Recents render at the top of the dropdown ahead of regular suggestions.
// At the base max-height, having recents in view would crowd out the
// suggestion list below. This loop grows the dropdown by one row's worth
// of height for every recent present (up to $max-recents-shown), plus the
// section header. `:has(> li:nth-of-type(N) .cm-completionIcon-recent)`
// matches when the Nth child of <ul> is a recent — i.e. there are at
// least N recents visible.
@for $i from 1 through $max-recents-shown {
&:has(> li:nth-of-type(#{$i}) .cm-completionIcon-recent) {
max-height: $dropdown-base-height +
$recents-header-height +
($i * $recent-row-height) !important;
}
}
&::-webkit-scrollbar {
width: 0.3rem;
@@ -133,6 +155,19 @@
background: transparent;
}
completion-section {
display: block;
padding: 10px 12px 6px;
font-size: 10px !important;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--l3-foreground, var(--l2-foreground));
opacity: 0.7;
border-bottom: 0;
background-color: transparent;
}
li {
width: 100% !important;
max-width: 100% !important;
@@ -159,11 +194,78 @@
display: none !important;
}
.cm-completionDetail {
margin-left: auto;
font-style: normal;
font-size: var(--periscope-font-size-small, 11px);
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
opacity: 0.55;
}
&[aria-selected='true'] {
background: var(--l3-background) !important;
font-weight: 600 !important;
}
}
li:has(.cm-completionIcon-recent) {
&:hover .cm-recent-delete,
&[aria-selected='true'] .cm-recent-delete {
opacity: 1;
}
}
li .cm-completionLabel {
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cm-recent-delete {
margin-left: 8px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
padding: 0;
border: 0;
border-radius: 4px;
background: transparent;
color: var(--l2-foreground);
font-size: 18px;
line-height: 1;
cursor: pointer;
opacity: 0.5;
transition:
opacity 0.12s ease,
background-color 0.12s ease;
&:hover {
opacity: 1;
background: color-mix(in srgb, var(--l2-foreground) 18%, transparent);
}
}
}
&::after {
content: '↓↑ to navigate · ↵ to apply · esc to dismiss';
display: block;
padding: 8px 12px;
border-top: 1px solid var(--l1-border);
font-family: 'Space Mono', monospace;
font-size: 11px;
font-weight: 500;
letter-spacing: 0.02em;
color: var(--l2-foreground);
opacity: 0.6;
background: color-mix(in srgb, var(--l1-background) 50%, transparent);
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
}

View File

@@ -46,8 +46,15 @@ import {
import { validateQuery } from 'utils/queryValidationUtils';
import { unquote } from 'utils/stringUtils';
import { queryExamples } from './constants';
import { combineInitialAndUserExpression } from './utils';
import { getRecentQueries } from 'lib/recentQueries/getRecentQueries';
import type { SignalType } from 'types/api/v5/queryRange';
import { queryExamples, SUGGESTIONS_SECTION } from './constants';
import {
combineInitialAndUserExpression,
getRecentOptions,
renderRecentDeleteButton,
} from './utils';
import './QuerySearch.styles.scss';
@@ -1250,6 +1257,41 @@ function QuerySearch({
};
}
const signal = dataSource as SignalType;
function combinedSuggestions(
context: CompletionContext,
): CompletionResult | null {
const fullDoc = context.state.doc.toString();
const recentOptions = getRecentOptions(
getRecentQueries(signal, signalSource ?? ''),
fullDoc,
);
const result = autoSuggestions(context);
const suggestionOptions = (result?.options || []).map((opt) => ({
...opt,
section: SUGGESTIONS_SECTION,
}));
if (recentOptions.length === 0 && suggestionOptions.length === 0) {
return result;
}
if (!result) {
return {
from: 0,
to: fullDoc.length,
options: recentOptions,
};
}
return {
...result,
options: [...recentOptions, ...suggestionOptions],
};
}
// Effect to handle focus state and trigger suggestions
useEffect(() => {
const clearTimeout = toggleSuggestions(10);
@@ -1398,11 +1440,12 @@ function QuerySearch({
})}
extensions={[
autocompletion({
override: [autoSuggestions],
override: [combinedSuggestions],
defaultKeymap: true,
closeOnBlur: true,
activateOnTyping: true,
maxRenderedOptions: 50,
addToOptions: [{ render: renderRecentDeleteButton, position: 100 }],
}),
javascript({ jsx: false, typescript: false }),
EditorView.lineWrapping,

View File

@@ -1,3 +1,14 @@
export const RECENTS_SECTION = { name: 'Recent searches', rank: 1 } as const;
export const SUGGESTIONS_SECTION = { name: 'Suggestions', rank: 2 } as const;
// Custom CodeMirror Completion.type for recent-query entries. Used to discriminate
// recents from regular autocomplete completions in renderers and event handlers.
export const RECENT_COMPLETION_TYPE = 'recent';
// Max number of recents rendered in the autocomplete dropdown.
// Do change $max-recents-shown: in QuerySearch.styles.scss if you change this.
export const RECENTS_DISPLAY_CAP = 5;
export const queryExamples = [
{
label: 'Basic Query',

View File

@@ -1,3 +1,19 @@
import { closeCompletion, startCompletion } from '@codemirror/autocomplete';
import type { Completion } from '@codemirror/autocomplete';
import type { EditorView } from '@uiw/react-codemirror';
import dayjs from 'dayjs';
import { normalizeFilterExpression } from 'lib/recentQueries/normalize';
import * as recentQueriesStore from 'lib/recentQueries/recentQueriesStore';
import type { RecentQueryEntry } from 'lib/recentQueries/types';
import type { SignalType } from 'types/api/v5/queryRange';
import 'utils/timeUtils';
import {
RECENT_COMPLETION_TYPE,
RECENTS_DISPLAY_CAP,
RECENTS_SECTION,
} from './constants';
export function combineInitialAndUserExpression(
initial: string,
user: string,
@@ -38,3 +54,106 @@ export function getUserExpressionFromCombined(
}
return c;
}
// Filters and projects a list of recent-query entries into CodeMirror completions.
// Entries are supplied by the caller (typically via the useRecents hook) so this
// function stays pure and React doesn't have to re-subscribe inside CodeMirror's
// autocomplete callback.
export function getRecentOptions(
entries: RecentQueryEntry[],
fullDoc: string,
): Completion[] {
const normalizedDoc = normalizeFilterExpression(fullDoc);
const matches = entries
.filter((e) => {
const normalizedRecent = normalizeFilterExpression(e.filter.expression);
if (normalizedRecent === normalizedDoc) {
return false;
}
if (normalizedDoc === '') {
return true;
}
return normalizedRecent.includes(normalizedDoc);
})
.slice(0, RECENTS_DISPLAY_CAP);
return matches.map((entry, index) => ({
label: entry.filter.expression,
type: RECENT_COMPLETION_TYPE,
// CodeMirror sorts within a section by boost desc, then label asc. The store
// returns entries newest-first, so we mirror that by giving the newest entry
// the highest boost — otherwise CM falls back to alphabetical order and the
// "most recently used" expectation breaks. Stays within the recents section
// because section.rank keeps recents above suggestions regardless of boost.
boost: matches.length - index,
section: RECENTS_SECTION,
detail: dayjs(entry.lastUsedAt).fromNow(),
recentId: entry.id,
recentSignal: entry.signal,
recentSource: entry.source,
apply: (view: EditorView): void => {
view.dispatch({
changes: {
from: 0,
to: view.state.doc.length,
insert: entry.filter.expression,
},
selection: { anchor: entry.filter.expression.length },
});
closeCompletion(view);
},
}));
}
export function renderRecentDeleteButton(
completion: Completion,
_state: unknown,
view: EditorView | null,
): Node | null {
if (completion.type !== RECENT_COMPLETION_TYPE) {
return null;
}
const c = completion as Completion & {
recentId?: string;
recentSignal?: SignalType;
recentSource?: string;
};
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'cm-recent-delete';
btn.setAttribute('aria-label', 'Remove from recent searches');
btn.title = 'Remove from recent searches';
btn.textContent = '×';
queueMicrotask(() => {
if (btn.parentElement) {
btn.parentElement.title = completion.label;
}
});
const stop = (e: Event): void => {
e.preventDefault();
e.stopPropagation();
};
// CodeMirror's autocomplete closes the popup on pointerdown / mousedown outside
// the editor. The delete button lives inside the popup, so we must stop those
// events early — otherwise clicking × would dismiss the dropdown before the
// click handler fires and the entry wouldn't actually get removed.
btn.addEventListener('pointerdown', stop);
btn.addEventListener('mousedown', stop);
btn.addEventListener('click', (e) => {
stop(e);
if (!c.recentId || !c.recentSignal) {
return;
}
recentQueriesStore.remove(c.recentId, c.recentSignal, c.recentSource ?? '');
if (view) {
view.focus();
startCompletion(view);
}
});
return btn;
}

View File

@@ -0,0 +1,5 @@
export const STORAGE_KEY_PREFIX = 'qb_recent_v1';
export const STORAGE_VERSION = 1;
// Maximum entries kept per (signal, source) bucket. Larger than the UI's
// RECENTS_DISPLAY_CAP so deleting a visible entry surfaces an older one.
export const MAX_ENTRIES = 10;

View File

@@ -0,0 +1,15 @@
import type { SignalType } from 'types/api/v5/queryRange';
import * as store from './recentQueriesStore';
import type { RecentQueryEntry } from './types';
// Synchronous, non-subscribing read of the recent-queries bucket for a given
// (signal, source). Read-on-demand by design — subscribing here would
// reconfigure CodeMirror on every store change and close any open completion
// popup. Pair with saveQuery() for the write path.
export function getRecentQueries(
signal: SignalType,
source = '',
): RecentQueryEntry[] {
return store.list(signal, source);
}

View File

@@ -0,0 +1,100 @@
import { normalizeFilterExpression } from './normalize';
describe('normalizeFilterExpression', () => {
it('returns empty string for empty input', () => {
expect(normalizeFilterExpression('')).toBe('');
});
it('returns empty string for whitespace-only input', () => {
expect(normalizeFilterExpression(' \t ')).toBe('');
});
it('strips whitespace around operators', () => {
expect(normalizeFilterExpression(' a = 1 ')).toBe('a=1');
expect(normalizeFilterExpression('a = 1')).toBe('a=1');
expect(normalizeFilterExpression('a=1')).toBe('a=1');
});
it('lowercases AND / OR / NOT outside quotes', () => {
expect(normalizeFilterExpression('A AND B OR NOT C')).toBe('AandBornotC');
});
it('lowercases IN / LIKE / ILIKE / CONTAINS', () => {
expect(normalizeFilterExpression('host IN [1, 2] AND name LIKE "foo"')).toBe(
'hostin[1,2]andnamelike"foo"',
);
});
it('lowercases REGEXP', () => {
expect(normalizeFilterExpression('path REGEXP "foo"')).toBe(
'pathregexp"foo"',
);
expect(normalizeFilterExpression('path REGEXP "foo"')).toBe(
normalizeFilterExpression('path regexp "foo"'),
);
});
it('lowercases HAS / HASANY / HASALL / HASTOKEN function names', () => {
expect(normalizeFilterExpression('HAS(tags, "x")')).toBe(
normalizeFilterExpression('has(tags, "x")'),
);
expect(normalizeFilterExpression('HASANY(tags, ["a","b"])')).toBe(
normalizeFilterExpression('hasAny(tags, ["a","b"])'),
);
expect(normalizeFilterExpression('HASALL(tags, ["a","b"])')).toBe(
normalizeFilterExpression('hasAll(tags, ["a","b"])'),
);
expect(normalizeFilterExpression('HASTOKEN(msg, "err")')).toBe(
normalizeFilterExpression('hasToken(msg, "err")'),
);
});
it('lowercases TRUE / FALSE boolean literals', () => {
expect(normalizeFilterExpression('active = TRUE')).toBe(
normalizeFilterExpression('active = true'),
);
expect(normalizeFilterExpression('active = FALSE')).toBe(
normalizeFilterExpression('active = false'),
);
});
it('preserves whitespace and casing inside single-quoted strings', () => {
expect(normalizeFilterExpression("a = 'X Y'")).toBe("a='X Y'");
});
it('preserves whitespace and casing inside double-quoted strings', () => {
expect(normalizeFilterExpression('a = "X Y"')).toBe('a="X Y"');
});
it('does not lowercase keyword-looking substrings inside quotes', () => {
expect(normalizeFilterExpression("msg = 'AND ERROR'")).toBe(
"msg='AND ERROR'",
);
});
it('handles escaped quotes inside strings', () => {
expect(normalizeFilterExpression("msg = 'a\\'b' AND x = 1")).toBe(
"msg='a\\'b'andx=1",
);
});
it('treats two formattings of the same expression as identical', () => {
const a = normalizeFilterExpression(
'service.name = "frontend" AND severity = error',
);
const b = normalizeFilterExpression(
'service.name="frontend" and severity=error',
);
expect(a).toBe(b);
});
it('preserves unquoted value casing (treats them as identifiers)', () => {
expect(normalizeFilterExpression('status = OK')).toBe('status=OK');
});
it('handles mixed quotes in one expression', () => {
expect(normalizeFilterExpression(`a = 'X' AND b = "Y"`)).toBe(
`a='X'andb="Y"`,
);
});
});

View File

@@ -0,0 +1,56 @@
import {
OPERATORS,
QUERY_BUILDER_FUNCTIONS,
TRACE_OPERATOR_OPERATORS,
} from 'constants/antlrQueryConstants';
// Reserved keywords sourced from the ANTLR grammar constants so this list stays
// in sync with the parser. `\b` prevents partial matches inside identifiers
// (e.g. `OR` inside `originator`). `TRUE`/`FALSE` are BOOL literals, included
// so case variants of boolean values also dedup.
const WORD_KEYWORDS = [
...Object.keys(OPERATORS).filter((k) => /^[A-Z]+$/.test(k)),
...Object.keys(TRACE_OPERATOR_OPERATORS).filter((k) => /^[A-Z]+$/.test(k)),
...Object.values(QUERY_BUILDER_FUNCTIONS),
'TRUE',
'FALSE',
];
const KEYWORDS_RE = new RegExp(`\\b(${WORD_KEYWORDS.join('|')})\\b`, 'gi');
// Matches single- or double-quoted string literals, supporting escaped quotes
// (e.g. `'it\'s'` or `"a \" b"`). We preserve quoted spans verbatim during
// normalisation so user-meaningful whitespace and casing inside string values
// stays intact: `name = "Foo Bar"` must not collapse to `name="foobar"`.
const QUOTED_RE = /'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"/g;
// Lowercases reserved keywords and strips ALL whitespace from the unquoted regions
// of the input. Keywords are normalised so casing variants dedup; whitespace is
// dropped so formatting variants (`a=1` vs `a = 1`) dedup too.
function processOutsideQuotes(s: string): string {
return s.replace(KEYWORDS_RE, (m) => m.toLowerCase()).replace(/\s+/g, '');
}
// Produces a canonical form of a filter expression suitable for dedup-key derivation.
// Walks the input alternating between unquoted regions (where we normalise keywords
// and whitespace) and quoted regions (which we copy verbatim).
export function normalizeFilterExpression(input: string): string {
if (!input) {
return '';
}
let result = '';
let lastIndex = 0;
QUOTED_RE.lastIndex = 0;
let match = QUOTED_RE.exec(input);
while (match !== null) {
result += processOutsideQuotes(input.slice(lastIndex, match.index));
result += match[0];
lastIndex = QUOTED_RE.lastIndex;
match = QUOTED_RE.exec(input);
}
result += processOutsideQuotes(input.slice(lastIndex));
return result.trim();
}

View File

@@ -0,0 +1,247 @@
import { MAX_ENTRIES } from './constants';
import * as store from './recentQueriesStore';
import type { RecentQueryInput } from './recentQueriesStore';
import type { RecentQueryEntry } from './types';
const baseInput = (
overrides: Partial<RecentQueryInput> = {},
): RecentQueryInput => ({
signal: 'logs',
filter: { expression: "service.name = 'frontend'" },
...overrides,
});
function saveOrThrow(input: RecentQueryInput): RecentQueryEntry {
const saved = store.save(input);
if (!saved) {
throw new Error('expected save to return an entry');
}
return saved;
}
describe('recentQueries store', () => {
beforeEach(() => {
store.useRecentQueriesStore.setState({ buckets: {} });
localStorage.clear();
});
describe('save + list', () => {
it('saves an entry and lists it', () => {
store.save(baseInput());
const entries = store.list('logs');
expect(entries).toHaveLength(1);
expect(entries[0].filter.expression).toBe("service.name = 'frontend'");
expect(entries[0].id).toBeTruthy();
expect(entries[0].lastUsedAt).toBeGreaterThan(0);
});
it('does not save when the filter expression is empty', () => {
const result = store.save(baseInput({ filter: { expression: '' } }));
expect(result).toBeNull();
expect(store.list('logs')).toHaveLength(0);
});
it('does not save when the filter expression is whitespace only', () => {
const result = store.save(baseInput({ filter: { expression: ' ' } }));
expect(result).toBeNull();
expect(store.list('logs')).toHaveLength(0);
});
});
describe('LRU ordering', () => {
it('places the most recently saved entry at the front', () => {
store.save(baseInput({ filter: { expression: "severity_text = 'ERROR'" } }));
store.save(baseInput({ filter: { expression: 'http.status_code >= 500' } }));
store.save(baseInput({ filter: { expression: 'attempt = 1' } }));
const entries = store.list('logs');
expect(entries.map((e) => e.filter.expression)).toStrictEqual([
'attempt = 1',
'http.status_code >= 500',
"severity_text = 'ERROR'",
]);
});
it('re-saving an existing filter bumps it to the front', () => {
store.save(baseInput({ filter: { expression: "severity_text = 'ERROR'" } }));
store.save(baseInput({ filter: { expression: 'http.status_code >= 500' } }));
store.save(baseInput({ filter: { expression: "severity_text = 'ERROR'" } }));
const entries = store.list('logs');
expect(entries).toHaveLength(2);
expect(entries.map((e) => e.filter.expression)).toStrictEqual([
"severity_text = 'ERROR'",
'http.status_code >= 500',
]);
});
});
describe('dedup', () => {
it('treats formatting variations of the same filter as one entry', () => {
store.save(
baseInput({
filter: { expression: "severity_text = 'ERROR' AND attempt = 1" },
}),
);
store.save(
baseInput({
filter: { expression: "severity_text='ERROR' and attempt=1" },
}),
);
expect(store.list('logs')).toHaveLength(1);
});
});
describe('signal partitioning', () => {
it('saves to the right bucket per signal', () => {
store.save(
baseInput({
signal: 'logs',
filter: { expression: "severity_text = 'ERROR'" },
}),
);
store.save(
baseInput({
signal: 'traces',
filter: { expression: "service.name = 'orders-api'" },
}),
);
store.save(
baseInput({
signal: 'metrics',
filter: { expression: 'cpu_usage > 80' },
}),
);
expect(store.list('logs')).toHaveLength(1);
expect(store.list('traces')).toHaveLength(1);
expect(store.list('metrics')).toHaveLength(1);
expect(store.list('logs')[0].filter.expression).toBe(
"severity_text = 'ERROR'",
);
expect(store.list('traces')[0].filter.expression).toBe(
"service.name = 'orders-api'",
);
expect(store.list('metrics')[0].filter.expression).toBe('cpu_usage > 80');
});
it('does not leak between signals on dedup', () => {
store.save(
baseInput({
signal: 'logs',
filter: { expression: "service.name = 'frontend'" },
}),
);
store.save(
baseInput({
signal: 'traces',
filter: { expression: "service.name = 'frontend'" },
}),
);
expect(store.list('logs')).toHaveLength(1);
expect(store.list('traces')).toHaveLength(1);
});
});
describe('LRU cap', () => {
it('caps the bucket at MAX_ENTRIES and evicts the oldest', () => {
const total = MAX_ENTRIES + 1;
for (let i = 0; i < total; i += 1) {
store.save(baseInput({ filter: { expression: `attempt = ${i}` } }));
}
const entries = store.list('logs');
expect(entries).toHaveLength(MAX_ENTRIES);
expect(entries[0].filter.expression).toBe(`attempt = ${total - 1}`);
expect(entries.some((e) => e.filter.expression === 'attempt = 0')).toBe(
false,
);
});
});
describe('remove', () => {
it('removes an entry by id', () => {
store.save(baseInput({ filter: { expression: "severity_text = 'ERROR'" } }));
const saved = saveOrThrow(
baseInput({ filter: { expression: 'http.status_code >= 500' } }),
);
store.remove(saved.id, 'logs');
const entries = store.list('logs');
expect(entries).toHaveLength(1);
expect(entries[0].filter.expression).toBe("severity_text = 'ERROR'");
});
it('is a no-op when the id does not exist', () => {
store.save(baseInput({ filter: { expression: "severity_text = 'ERROR'" } }));
store.remove('does-not-exist', 'logs');
expect(store.list('logs')).toHaveLength(1);
});
it('does not touch other signals', () => {
const logsEntry = saveOrThrow(
baseInput({
signal: 'logs',
filter: { expression: "service.name = 'frontend'" },
}),
);
store.save(
baseInput({
signal: 'traces',
filter: { expression: "service.name = 'frontend'" },
}),
);
store.remove(logsEntry.id, 'logs');
expect(store.list('logs')).toHaveLength(0);
expect(store.list('traces')).toHaveLength(1);
});
});
describe('persistence', () => {
it('reads back the same entries after the in-memory state is reset', () => {
store.save(baseInput({ filter: { expression: "severity_text = 'ERROR'" } }));
store.save(baseInput({ filter: { expression: 'http.status_code >= 500' } }));
store.useRecentQueriesStore.setState({ buckets: {} });
const entries = store.list('logs');
expect(entries.map((e) => e.filter.expression)).toStrictEqual([
'http.status_code >= 500',
"severity_text = 'ERROR'",
]);
});
});
describe('reactive subscription via zustand', () => {
it('notifies zustand subscribers on save', () => {
const cb = jest.fn();
const unsubscribe = store.useRecentQueriesStore.subscribe(cb);
store.save(baseInput({ filter: { expression: "severity_text = 'ERROR'" } }));
expect(cb).toHaveBeenCalledTimes(1);
unsubscribe();
});
it('notifies zustand subscribers on remove', () => {
const saved = saveOrThrow(
baseInput({ filter: { expression: "severity_text = 'ERROR'" } }),
);
const cb = jest.fn();
const unsubscribe = store.useRecentQueriesStore.subscribe(cb);
store.remove(saved.id, 'logs');
expect(cb).toHaveBeenCalledTimes(1);
unsubscribe();
});
it('stops notifying after unsubscribe', () => {
const cb = jest.fn();
const unsubscribe = store.useRecentQueriesStore.subscribe(cb);
unsubscribe();
store.save(baseInput({ filter: { expression: "severity_text = 'ERROR'" } }));
expect(cb).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,144 @@
import get from 'api/browser/localstorage/get';
import set from 'api/browser/localstorage/set';
import type { SignalType } from 'types/api/v5/queryRange';
import { create } from 'zustand';
import { MAX_ENTRIES, STORAGE_VERSION } from './constants';
import { normalizeFilterExpression } from './normalize';
import type { RecentQueriesStoreShape, RecentQueryEntry } from './types';
import { bucketKey, makeId, normalizeSource, storageKeyFor } from './utils';
// Mirrors parsed localStorage so equal raw strings return the same array ref —
// preserves Object.is for zustand selector bail-out.
const persistedBucketCache = new Map<
string,
{ raw: string; parsed: RecentQueryEntry[] }
>();
function loadBucketFromStorage(
signal: SignalType,
source: string,
): RecentQueryEntry[] | null {
const key = bucketKey(signal, source);
try {
const raw = get(storageKeyFor(signal, source));
if (!raw) {
persistedBucketCache.delete(key);
return null;
}
const cached = persistedBucketCache.get(key);
if (cached && cached.raw === raw) {
return cached.parsed;
}
const parsedShape = JSON.parse(raw) as RecentQueriesStoreShape;
if (
parsedShape?.version !== STORAGE_VERSION ||
!Array.isArray(parsedShape.entries)
) {
persistedBucketCache.delete(key);
return null;
}
persistedBucketCache.set(key, { raw, parsed: parsedShape.entries });
return parsedShape.entries;
} catch {
persistedBucketCache.delete(key);
return null;
}
}
function saveBucketToStorage(
signal: SignalType,
source: string,
entries: RecentQueryEntry[],
): void {
try {
const raw = JSON.stringify({ version: STORAGE_VERSION, entries });
if (set(storageKeyFor(signal, source), raw)) {
persistedBucketCache.set(bucketKey(signal, source), {
raw,
parsed: entries,
});
}
} catch {
// Ignore storage errors (e.g. quota exceeded, JSON.stringify failure).
}
}
export type RecentQueryInput = Omit<
RecentQueryEntry,
'id' | 'lastUsedAt' | 'source'
> & {
source?: string;
};
type RecentQueriesState = {
buckets: Record<string, RecentQueryEntry[]>;
save: (entry: RecentQueryInput) => RecentQueryEntry | null;
remove: (id: string, signal: SignalType, source?: string) => void;
};
export const useRecentQueriesStore = create<RecentQueriesState>()(
(set, get) => ({
buckets: {},
save: (entry): RecentQueryEntry | null => {
const normalized = normalizeFilterExpression(entry.filter.expression);
if (!normalized) {
return null;
}
const source = normalizeSource(entry.source);
const key = bucketKey(entry.signal, source);
const current =
get().buckets[key] ?? loadBucketFromStorage(entry.signal, source) ?? [];
const filtered = current.filter(
(e) => normalizeFilterExpression(e.filter.expression) !== normalized,
);
const newEntry: RecentQueryEntry = {
...entry,
source,
id: makeId(entry.signal, source, normalized),
lastUsedAt: Date.now(),
};
const next = [newEntry, ...filtered].slice(0, MAX_ENTRIES);
set({ buckets: { ...get().buckets, [key]: next } });
saveBucketToStorage(entry.signal, source, next);
return newEntry;
},
remove: (id, signal, source = ''): void => {
const key = bucketKey(signal, source);
const current = get().buckets[key] ?? loadBucketFromStorage(signal, source);
if (!current) {
return;
}
const next = current.filter((e) => e.id !== id);
if (next.length === current.length) {
return;
}
set({ buckets: { ...get().buckets, [key]: next } });
saveBucketToStorage(signal, source, next);
},
}),
);
// Plain-function wrappers for non-React callers — same pattern as useColumnStore.ts.
export function save(entry: RecentQueryInput): RecentQueryEntry | null {
return useRecentQueriesStore.getState().save(entry);
}
export function remove(id: string, signal: SignalType, source = ''): void {
useRecentQueriesStore.getState().remove(id, signal, source);
}
// Synchronous bucket read with localStorage fallback for non-React callers.
export function list(signal: SignalType, source = ''): RecentQueryEntry[] {
const key = bucketKey(signal, source);
const state = useRecentQueriesStore.getState();
if (state.buckets[key]) {
return state.buckets[key];
}
return loadBucketFromStorage(signal, source) ?? [];
}

View File

@@ -0,0 +1,113 @@
import { DataSource } from 'types/common/queryBuilder';
import type { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { validateQuery } from 'utils/queryValidationUtils';
import * as store from './recentQueriesStore';
import { saveRecentQuery } from './saveRecentQuery';
jest.mock('utils/queryValidationUtils', () => ({
validateQuery: jest.fn(),
}));
const mockedValidateQuery = validateQuery as jest.MockedFunction<
typeof validateQuery
>;
const buildComposite = (
overrides: Partial<IBuilderQuery>[] = [{}],
): { builder: { queryData: IBuilderQuery[] } } => ({
builder: {
queryData: overrides.map((o, i) => ({
queryName: `Q${i}`,
dataSource: DataSource.LOGS,
aggregateOperator: 'count',
aggregateAttribute: undefined as never,
functions: [],
filter: { expression: 'service.name = "frontend"' },
groupBy: [],
expression: `Q${i}`,
disabled: false,
having: [],
limit: null,
stepInterval: null,
orderBy: [],
legend: '',
...o,
})) as IBuilderQuery[],
},
});
describe('saveRecentQuery', () => {
beforeEach(() => {
store.useRecentQueriesStore.setState({ buckets: {} });
localStorage.clear();
mockedValidateQuery.mockReturnValue({
isValid: true,
message: '',
errors: [],
});
});
it('saves the composite query when validation passes', () => {
saveRecentQuery(buildComposite());
const entries = store.list('logs');
expect(entries).toHaveLength(1);
expect(entries[0].filter.expression).toBe('service.name = "frontend"');
});
it('does not save when validateQuery rejects the expression', () => {
mockedValidateQuery.mockReturnValue({
isValid: false,
message: 'bad',
errors: [],
});
saveRecentQuery(buildComposite());
expect(store.list('logs')).toHaveLength(0);
});
it('does not save a builder query with an empty filter expression', () => {
saveRecentQuery(buildComposite([{ filter: { expression: '' } }]));
expect(store.list('logs')).toHaveLength(0);
});
it('saves each builder query in the composite separately', () => {
saveRecentQuery(
buildComposite([
{
dataSource: DataSource.LOGS,
filter: { expression: "service.name = 'frontend'" },
},
{
dataSource: DataSource.TRACES,
filter: { expression: "service.name = 'orders-api'" },
},
]),
);
expect(store.list('logs')).toHaveLength(1);
expect(store.list('traces')).toHaveLength(1);
});
it('skips builder queries whose dataSource is not a supported signal', () => {
saveRecentQuery(
buildComposite([{ dataSource: 'unknown' as IBuilderQuery['dataSource'] }]),
);
expect(store.list('logs')).toHaveLength(0);
expect(store.list('traces')).toHaveLength(0);
expect(store.list('metrics')).toHaveLength(0);
});
it('is a no-op when the composite is null, undefined, or empty', () => {
saveRecentQuery(null);
saveRecentQuery(undefined);
saveRecentQuery({ builder: { queryData: [] } });
saveRecentQuery({});
expect(store.list('logs')).toHaveLength(0);
});
});

View File

@@ -0,0 +1,52 @@
import type { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import type { SignalType } from 'types/api/v5/queryRange';
import { validateQuery } from 'utils/queryValidationUtils';
import * as store from './recentQueriesStore';
function toSignal(dataSource: IBuilderQuery['dataSource']): SignalType | null {
if (
dataSource === 'logs' ||
dataSource === 'traces' ||
dataSource === 'metrics'
) {
return dataSource;
}
return null;
}
type CompositeWithBuilder = {
builder?: { queryData?: IBuilderQuery[] };
};
// Persists each builder query in the composite as a recent entry. Call this
// only from explicit user-driven Run triggers — reacting to stagedQuery or any
// other derived state pollutes recents with navigation/refresh/go-to traffic.
export function saveRecentQuery(
query: CompositeWithBuilder | null | undefined,
): void {
const queryData = query?.builder?.queryData;
if (!Array.isArray(queryData) || queryData.length === 0) {
return;
}
queryData.forEach((q) => {
const expression = q.filter?.expression?.trim();
if (!expression) {
return;
}
const validation = validateQuery(expression);
if (!validation.isValid) {
return;
}
const signal = toSignal(q.dataSource);
if (!signal) {
return;
}
store.save({
signal,
source: q.source ?? '',
filter: q.filter ?? { expression: '' },
});
});
}

View File

@@ -0,0 +1,14 @@
import type { Filter, SignalType } from 'types/api/v5/queryRange';
export interface RecentQueryEntry {
id: string;
signal: SignalType;
source: string;
filter: Filter;
lastUsedAt: number;
}
export interface RecentQueriesStoreShape {
version: 1;
entries: RecentQueryEntry[];
}

View File

@@ -0,0 +1,24 @@
import type { SignalType } from 'types/api/v5/queryRange';
import { STORAGE_KEY_PREFIX } from './constants';
export function normalizeSource(source: string | undefined): string {
return source ?? '';
}
export function bucketKey(signal: SignalType, source: string): string {
return `${signal}:${source}`;
}
export function storageKeyFor(signal: SignalType, source: string): string {
return `${STORAGE_KEY_PREFIX}:${bucketKey(signal, source)}`;
}
// Same (signal, source, normalized filter) ⇒ same id ⇒ upsert.
export function makeId(
signal: SignalType,
source: string,
normalizedFilter: string,
): string {
return `${signal}|${source}|${normalizedFilter}`;
}

View File

@@ -1,30 +0,0 @@
import { useMemo } from 'react';
import type { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import type { BuilderQuery } from 'types/api/v5/queryRange';
/**
* Builds a record keyed by builder-query name to that query's groupBy keys
* in the V1 `BaseAutocompleteData` shape — the shape `TimeSeries` and the
* tooltip plugin consume. Conversion from v5 `GroupByKey` lives at this one
* call site that needs the V1 shape; the rest of V2 panel code stays on
* v5 types.
*/
export function useGroupByPerQuery(
builderQueries: BuilderQuery[],
): Record<string, BaseAutocompleteData[]> {
return useMemo(() => {
const result: Record<string, BaseAutocompleteData[]> = {};
builderQueries.forEach((q) => {
if (!q.name) {
return;
}
result[q.name] = (q.groupBy ?? []).map((g) => ({
key: g.name,
dataType: g.fieldDataType as BaseAutocompleteData['dataType'],
type: (g.fieldContext as BaseAutocompleteData['type']) ?? '',
id: '',
}));
});
return result;
}, [builderQueries]);
}

View File

@@ -1,29 +0,0 @@
import { useMemo } from 'react';
import type { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
import type { QueryRangeRequestV5 } from 'types/api/v5/queryRange';
import { getTimeRangeFromQueryRangeRequest } from 'utils/getTimeRange';
interface TimeScale {
minTimeScale: number | undefined;
maxTimeScale: number | undefined;
}
/**
* Derives the X-axis time-scale clamps from a query-range response. Reads
* `start`/`end` off `data.params` (the request that produced this payload)
* so each panel pins to the window it actually fetched — important during
* drag-zoom transitions when the time picker has moved but new data hasn't
* arrived yet. Falls back to the global time picker via the helper when
* `data` is absent.
*/
export function useTimeScale(
data: MetricQueryRangeSuccessResponse | undefined,
): TimeScale {
return useMemo(() => {
// `data.params` is typed `unknown` on this branch; PR 11562 narrows it
// to `QueryRangeRequestV5`. Drop this cast when that lands.
const params = data?.params as QueryRangeRequestV5 | undefined;
const { startTime, endTime } = getTimeRangeFromQueryRangeRequest(params);
return { minTimeScale: startTime, maxTimeScale: endTime };
}, [data]);
}

View File

@@ -1,34 +0,0 @@
import { definition as barChart } from './kinds/BarChartPanel/definition';
import { definition as histogram } from './kinds/HistogramPanel/definition';
import { definition as pieChart } from './kinds/PieChartPanel/definition';
import { definition as timeSeries } from './kinds/TimeSeriesPanel/definition';
import type {
PanelRegistry,
RenderablePanelDefinition,
} from './types/panelDefinition';
import type { PanelKind } from './types/panelKind';
// Pure assembly: each kind owns its own PanelDefinition (see
// `kinds/<Kind>/definition.ts`). Registering a new panel = add its folder and a
// single entry below — no other central file needs editing.
export const PANELS: PanelRegistry = {
[timeSeries.kind]: timeSeries,
[barChart.kind]: barChart,
[histogram.kind]: histogram,
[pieChart.kind]: pieChart,
};
export function getPanelDefinition(
kind: string | undefined,
): RenderablePanelDefinition | undefined {
if (!kind) {
return undefined;
}
// The registry is correlated by kind, so a string lookup yields a union over
// every kind's exactly-typed definition. The renderer cannot be validated
// against that union at the JSX boundary, so widen to the kind-agnostic
// surface here — the single, intentional cast for the whole panel system.
return PANELS[kind as PanelKind] as unknown as
| RenderablePanelDefinition
| undefined;
}

View File

@@ -1,152 +0,0 @@
import { useCallback, useMemo, useRef } from 'react';
import type { DashboardtypesBarChartPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
import TooltipFooter from 'container/DashboardContainer/visualization/panels/components/TooltipFooter';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { IRenderTooltipFooterArgs } from 'lib/uPlotV2/components/types';
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
import { useTimezone } from 'providers/Timezone';
import { useGroupByPerQuery } from '../../hooks/useGroupByPerQuery';
import { useTimeScale } from '../../hooks/useTimeScale';
import PanelStyles from '../../panel.module.scss';
import { PanelRendererProps } from '../../types/rendererProps';
import {
resolveDecimalPrecision,
resolveLegendPosition,
} from '../../utils/chartAppearanceMappings';
import { getBuilderQueries } from '../../utils/getBuilderQueries';
import { buildBarChartConfig } from './buildConfig';
import { ChartClickData } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
function BarPanelRenderer({
panelId,
panel,
data,
onClick,
onDragSelect,
dashboardPreference,
panelMode,
}: PanelRendererProps<'signoz/BarChartPanel'>): JSX.Element {
const graphRef = useRef<HTMLDivElement>(null);
const containerDimensions = useResizeObserver(graphRef);
const isDarkMode = useIsDarkMode();
const { timezone } = useTimezone();
// The registry guarantees this Renderer only runs when
// `panel.spec.plugin.kind === 'signoz/BarChartPanel'`, so the cast is a
// documented boundary narrowing. Memoized so the `?? {}` fallback doesn't
// produce a fresh object on each render.
const spec = useMemo<DashboardtypesBarChartPanelSpecDTO>(
() => (panel?.spec?.plugin?.spec ?? {}) as DashboardtypesBarChartPanelSpecDTO,
[panel?.spec?.plugin?.spec],
);
const builderQueries = useMemo(
() => getBuilderQueries(panel?.spec?.queries),
[panel?.spec?.queries],
);
const { minTimeScale, maxTimeScale } = useTimeScale(data);
const groupByPerQuery = useGroupByPerQuery(builderQueries);
const config = useMemo(
() =>
buildBarChartConfig({
panelId,
spec,
builderQueries,
apiResponse: data,
isDarkMode,
timezone,
panelMode,
minTimeScale,
maxTimeScale,
onDragSelect,
}),
[
panelId,
spec,
builderQueries,
data,
isDarkMode,
timezone,
panelMode,
minTimeScale,
maxTimeScale,
onDragSelect,
// `config` gets mutated by TooltipPlugin (config.setCursor for cursor sync).
// Rebuild it on syncMode changes so the new chart instance starts from a
// clean config — otherwise switching to "No Sync" would inherit stale sync
// settings from the previous mode.
dashboardPreference?.syncMode,
],
);
const chartData = useMemo(
() => (data?.payload ? prepareChartData(data.payload) : []),
[data?.payload],
);
const decimalPrecision = useMemo(
() => resolveDecimalPrecision(spec.formatting?.decimalPrecision),
[spec.formatting?.decimalPrecision],
);
const legendPosition = useMemo(() => {
return resolveLegendPosition(spec.legend?.position);
}, [spec.legend?.position]);
const renderTooltipFooter = useCallback(
({ isPinned, dismiss }: IRenderTooltipFooterArgs) => (
<TooltipFooter id={panelId} isPinned={isPinned} dismiss={dismiss} />
),
[panelId],
);
// The uPlot key prop is the only way to force a full teardown and re-mount
// of the chart. Including syncMode/syncFilterMode in the key ensures changes
// to these preferences trigger a fresh chart instance, preventing stale
// sync wiring from being inherited.
const key = `${dashboardPreference?.syncMode}-${dashboardPreference?.syncFilterMode}`;
const handleChartClick = useCallback(
(args: ChartClickData) => {
onClick?.(args);
},
[onClick],
);
return (
<div
ref={graphRef}
data-testid="bar-panel-renderer"
className={PanelStyles.panelContainer}
>
{containerDimensions.width > 0 && containerDimensions.height > 0 && (
<BarChart
key={key}
config={config}
data={chartData}
legendConfig={{ position: legendPosition }}
groupByPerQuery={groupByPerQuery}
canPinTooltip
timezone={timezone}
yAxisUnit={spec.formatting?.unit}
decimalPrecision={decimalPrecision}
width={containerDimensions.width}
height={containerDimensions.height}
syncMode={dashboardPreference?.syncMode}
syncFilterMode={dashboardPreference?.syncFilterMode}
isStackedBarChart={spec.visualization?.stackedBarChart ?? false}
renderTooltipFooter={renderTooltipFooter}
onClick={handleChartClick}
/>
)}
</div>
);
}
export default BarPanelRenderer;

View File

@@ -1,140 +0,0 @@
import type { DashboardtypesBarChartPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { getInitialStackedBands } from 'container/DashboardContainer/visualization/charts/utils/stackSeriesUtils';
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import { buildBaseConfig } from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/baseConfigBuilder';
import { resolveSeriesLabel } from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/resolveSeriesLabel';
import getLabelName from 'lib/getLabelName';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import { DrawStyle } from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
import type { BuilderQuery } from 'types/api/v5/queryRange';
export interface BuildBarChartConfigArgs {
panelId: string;
spec: DashboardtypesBarChartPanelSpecDTO;
/**
* Flat list of builder queries on this panel (see `getBuilderQueries`).
* Powers per-query legend resolution; empty for non-builder panels.
*/
builderQueries: BuilderQuery[];
apiResponse: MetricQueryRangeSuccessResponse | undefined;
isDarkMode: boolean;
timezone: Timezone;
panelMode: PanelMode;
onDragSelect?: (start: number, end: number) => void;
onClick?: OnClickPluginOpts['onClick'];
minTimeScale?: number;
maxTimeScale?: number;
}
/**
* Builds a fully-wired `UPlotConfigBuilder` for a Bar chart panel.
*
* Delegates the panel-agnostic scaffolding (scales, thresholds, axes,
* drag-to-zoom, click plugin) to the shared `buildBaseConfig`, then layers
* in the Bar-specific concerns: optional stacking via uPlot bands, plus
* one bar series per result row.
*/
export function buildBarChartConfig({
panelId,
spec,
builderQueries,
apiResponse,
isDarkMode,
timezone,
panelMode,
onDragSelect,
onClick,
minTimeScale,
maxTimeScale,
}: BuildBarChartConfigArgs): UPlotConfigBuilder {
const builder = buildBaseConfig({
panelId,
panelType: PANEL_TYPES.BAR,
isDarkMode,
timezone,
panelMode,
isLogScale: spec.axes?.isLogScale,
softMin: spec.axes?.softMin ?? undefined,
softMax: spec.axes?.softMax ?? undefined,
formatting: spec.formatting,
thresholds: spec.thresholds,
apiResponse,
minTimeScale,
maxTimeScale,
onDragSelect,
onClick,
});
addSeriesFromResponse({
builder,
spec,
builderQueries,
apiResponse,
isDarkMode,
});
return builder;
}
interface AddSeriesArgs {
builder: UPlotConfigBuilder;
spec: DashboardtypesBarChartPanelSpecDTO;
builderQueries: BuilderQuery[];
apiResponse: MetricQueryRangeSuccessResponse | undefined;
isDarkMode: boolean;
}
/**
* Adds one bar series per result row, plus uPlot bands for stacking when
* `spec.visualization.stackedBarChart` is set. Each series receives its
* own per-query step interval so bar widths line up with the actual
* sampling cadence reported by the backend.
*/
function addSeriesFromResponse({
builder,
spec,
builderQueries,
apiResponse,
isDarkMode,
}: AddSeriesArgs): void {
const result = apiResponse?.payload?.data?.result;
if (!result) {
return;
}
const stepIntervals =
apiResponse?.payload?.data?.newResult?.meta?.stepIntervals;
const colorMapping = spec.legend?.customColors ?? {};
if (spec.visualization?.stackedBarChart) {
// uPlot uses 1-based series indices (index 0 is the timestamp axis);
// `+1` keeps the band targets aligned with the series we're about to add.
builder.setBands(getInitialStackedBands(result.length + 1));
}
result.forEach((series) => {
const baseLabel = getLabelName(
series.metric,
series.queryName || '',
series.legend || '',
);
const label = resolveSeriesLabel(series, builderQueries, baseLabel);
const stepInterval = series.queryName
? stepIntervals?.[series.queryName]
: undefined;
builder.addSeries({
scaleKey: 'y',
drawStyle: DrawStyle.Bar,
label,
colorMapping,
isDarkMode,
stepInterval,
metric: series.metric,
});
});
}

View File

@@ -1,13 +0,0 @@
import { DataSource } from 'types/common/queryBuilder';
import type { PanelDefinition } from '../../types/panelDefinition';
import Renderer from './Renderer';
import { sections } from './sections';
export const definition: PanelDefinition<'signoz/BarChartPanel'> = {
kind: 'signoz/BarChartPanel',
displayName: 'Bar Chart',
Renderer,
sections,
supportedSignals: [DataSource.METRICS, DataSource.LOGS, DataSource.TRACES],
};

View File

@@ -1,9 +0,0 @@
import type { SectionConfig } from '../../types/sections';
export const sections: SectionConfig[] = [
{ kind: 'formatting', controls: { unit: true, decimals: true } },
{ kind: 'axes', controls: { minMax: true, unit: true, logScale: true } },
{ kind: 'legend', controls: { position: true, mode: true } },
{ kind: 'thresholds', controls: { list: true } },
{ kind: 'chartAppearance', controls: { stacked: true } },
];

View File

@@ -1,126 +0,0 @@
import { useCallback, useMemo, useRef } from 'react';
import type { DashboardtypesHistogramPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import Histogram from 'container/DashboardContainer/visualization/charts/Histogram/Histogram';
import TooltipFooter from 'container/DashboardContainer/visualization/panels/components/TooltipFooter';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { IRenderTooltipFooterArgs } from 'lib/uPlotV2/components/types';
import { useTimezone } from 'providers/Timezone';
import type uPlot from 'uplot';
import PanelStyles from '../../panel.module.scss';
import { PanelRendererProps } from '../../types/rendererProps';
import { resolveLegendPosition } from '../../utils/chartAppearanceMappings';
import { getBuilderQueries } from '../../utils/getBuilderQueries';
import { buildHistogramConfig } from './buildConfig';
import { prepareHistogramData } from './prepareData';
import { ChartClickData } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
function HistogramPanelRenderer({
panelId,
panel,
data,
panelMode,
onClick,
}: PanelRendererProps<'signoz/HistogramPanel'>): JSX.Element {
const graphRef = useRef<HTMLDivElement>(null);
const containerDimensions = useResizeObserver(graphRef);
const isDarkMode = useIsDarkMode();
const { timezone } = useTimezone();
// The registry guarantees this Renderer only runs when
// `panel.spec.plugin.kind === 'signoz/HistogramPanel'`, so the cast is a
// documented boundary narrowing.
const spec = useMemo<DashboardtypesHistogramPanelSpecDTO>(
() =>
(panel?.spec?.plugin?.spec ?? {}) as DashboardtypesHistogramPanelSpecDTO,
[panel?.spec?.plugin?.spec],
);
const builderQueries = useMemo(
() => getBuilderQueries(panel?.spec?.queries),
[panel?.spec?.queries],
);
const config = useMemo(
() =>
buildHistogramConfig({
panelId,
spec,
builderQueries,
apiResponse: data,
isDarkMode,
timezone,
panelMode,
}),
[panelId, spec, builderQueries, data, isDarkMode, timezone, panelMode],
);
const chartData = useMemo(
() =>
prepareHistogramData({
payload: data?.payload,
bucketWidth: spec.histogramBuckets?.bucketWidth ?? undefined,
bucketCount: spec.histogramBuckets?.bucketCount ?? undefined,
mergeAllActiveQueries: spec.histogramBuckets?.mergeAllActiveQueries,
}),
[
data?.payload,
spec.histogramBuckets?.bucketWidth,
spec.histogramBuckets?.bucketCount,
spec.histogramBuckets?.mergeAllActiveQueries,
],
);
const legendPosition = useMemo(
() => resolveLegendPosition(spec.legend?.position),
[spec.legend?.position],
);
const renderTooltipFooter = useCallback(
({ isPinned, dismiss }: IRenderTooltipFooterArgs) => (
<TooltipFooter
id={panelId}
isPinned={isPinned}
dismiss={dismiss}
canDrilldown={false}
/>
),
[panelId],
);
const isQueriesMerged = spec.histogramBuckets?.mergeAllActiveQueries ?? false;
const handleChartClick = useCallback(
(args: ChartClickData) => {
onClick?.(args);
},
[onClick],
);
return (
<div
ref={graphRef}
data-testid="histogram-panel-renderer"
className={PanelStyles.panelContainer}
>
{containerDimensions.width > 0 && containerDimensions.height > 0 && (
<Histogram
key={panelId}
config={config}
data={chartData as uPlot.AlignedData}
legendConfig={{ position: legendPosition }}
canPinTooltip
isQueriesMerged={isQueriesMerged}
width={containerDimensions.width}
height={containerDimensions.height}
renderTooltipFooter={renderTooltipFooter}
onClick={handleChartClick}
/>
)}
</div>
);
}
export default HistogramPanelRenderer;

View File

@@ -1,142 +0,0 @@
import type { DashboardtypesHistogramPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import { buildBaseConfig } from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/baseConfigBuilder';
import { resolveSeriesLabel } from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/resolveSeriesLabel';
import getLabelName from 'lib/getLabelName';
import { DrawStyle } from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
import type { BuilderQuery } from 'types/api/v5/queryRange';
const POINT_SIZE = 5;
const BAR_WIDTH_FACTOR = 1;
// Merged-series colors mirror the V1 default — single histogram bin gets a
// fixed blue-ish pair so the merged view looks the same as before.
const MERGED_SERIES_LINE_COLOR = '#3f5ecc';
const MERGED_SERIES_FILL_COLOR = '#4E74F8';
export interface BuildHistogramConfigArgs {
panelId: string;
spec: DashboardtypesHistogramPanelSpecDTO;
/** Builder queries on this panel — used to resolve per-series labels. */
builderQueries: BuilderQuery[];
apiResponse: MetricQueryRangeSuccessResponse | undefined;
isDarkMode: boolean;
timezone: Timezone;
panelMode: PanelMode;
}
/**
* Builds a fully-wired `UPlotConfigBuilder` for a Histogram panel.
*
* Unlike time-axis panels, histograms have no time scale and no drag-to-zoom.
* We still reuse `buildBaseConfig` for the consistent scaffolding (thresholds,
* axes, click plugin) but then override the X/Y scales to be auto-linear
* (`time: false, auto: true`) and install a histogram-specific cursor that
* disables drag-pan and tightens focus proximity.
*/
export function buildHistogramConfig({
panelId,
spec,
builderQueries,
apiResponse,
isDarkMode,
timezone,
panelMode,
}: BuildHistogramConfigArgs): UPlotConfigBuilder {
const builder = buildBaseConfig({
panelId,
panelType: PANEL_TYPES.HISTOGRAM,
isDarkMode,
timezone,
panelMode,
apiResponse,
});
builder.setCursor({
drag: { x: false, y: false, setScale: true },
focus: { prox: 1e3 },
});
// Override the time-axis scales from `buildBaseConfig` — histograms are
// distribution plots, not time series.
builder.addScale({ scaleKey: 'x', time: false, auto: true });
builder.addScale({ scaleKey: 'y', time: false, auto: true, min: 0 });
addSeriesFromResponse({
builder,
spec,
builderQueries,
apiResponse,
isDarkMode,
});
return builder;
}
interface AddSeriesArgs {
builder: UPlotConfigBuilder;
spec: DashboardtypesHistogramPanelSpecDTO;
builderQueries: BuilderQuery[];
apiResponse: MetricQueryRangeSuccessResponse | undefined;
isDarkMode: boolean;
}
/**
* Adds histogram bar series to the builder. When `mergeAllActiveQueries` is
* set, `prepareHistogramData` produces a single Y column, so we add exactly
* one series with the fixed merged-mode colors. Otherwise one series per
* result row, with labels resolved via the standard legend matrix.
*/
function addSeriesFromResponse({
builder,
spec,
builderQueries,
apiResponse,
isDarkMode,
}: AddSeriesArgs): void {
const colorMapping = spec.legend?.customColors ?? {};
const mergeAllActiveQueries =
spec.histogramBuckets?.mergeAllActiveQueries ?? false;
if (mergeAllActiveQueries) {
builder.addSeries({
scaleKey: 'y',
label: '',
drawStyle: DrawStyle.Histogram,
colorMapping,
barWidthFactor: BAR_WIDTH_FACTOR,
pointSize: POINT_SIZE,
lineColor: MERGED_SERIES_LINE_COLOR,
fillColor: MERGED_SERIES_FILL_COLOR,
isDarkMode,
});
return;
}
const result = apiResponse?.payload?.data?.result;
if (!result) {
return;
}
result.forEach((series) => {
const baseLabel = getLabelName(
series.metric,
series.queryName || '',
series.legend || '',
);
const label = resolveSeriesLabel(series, builderQueries, baseLabel);
builder.addSeries({
scaleKey: 'y',
label,
drawStyle: DrawStyle.Histogram,
colorMapping,
barWidthFactor: BAR_WIDTH_FACTOR,
pointSize: POINT_SIZE,
isDarkMode,
});
});
}

View File

@@ -1,13 +0,0 @@
import { DataSource } from 'types/common/queryBuilder';
import type { PanelDefinition } from '../../types/panelDefinition';
import Renderer from './Renderer';
import { sections } from './sections';
export const definition: PanelDefinition<'signoz/HistogramPanel'> = {
kind: 'signoz/HistogramPanel',
displayName: 'Histogram',
Renderer,
sections,
supportedSignals: [DataSource.METRICS, DataSource.LOGS, DataSource.TRACES],
};

View File

@@ -1,148 +0,0 @@
import { histogramBucketSizes } from '@grafana/data';
import { DEFAULT_BUCKET_COUNT } from 'container/PanelWrapper/constants';
import {
buildHistogramBuckets,
mergeAlignedDataTables,
prependNullBinToFirstHistogramSeries,
replaceUndefinedWithNullInAlignedData,
} from 'container/DashboardContainer/visualization/panels/utils/histogram';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { AlignedData } from 'uplot';
import { incrRoundDn, roundDecimals } from 'utils/round';
export interface PrepareHistogramDataArgs {
payload: MetricRangePayloadProps | undefined;
bucketWidth?: number;
bucketCount?: number;
mergeAllActiveQueries?: boolean;
}
const BUCKET_OFFSET = 0;
const sortAscending = (a: number, b: number): number => a - b;
/**
* Bins raw series values into a uPlot-aligned histogram. Picks a bucket size
* either from `bucketWidth` (explicit override) or the smallest predefined
* Grafana bucket that fits the data's `range / bucketCount` target while
* staying ≥ the data's smallest non-zero delta (so we never sub-divide below
* the resolution of the input).
*
* Empty input → `[[]]` (a valid empty AlignedData uPlot accepts).
*/
export function prepareHistogramData({
payload,
bucketWidth,
bucketCount = DEFAULT_BUCKET_COUNT,
mergeAllActiveQueries = false,
}: PrepareHistogramDataArgs): AlignedData {
if (!payload) {
return [[]];
}
const result = payload.data.result;
const values = extractNumericValues(result);
if (values.length === 0) {
return [[]];
}
const sorted = [...values].sort(sortAscending);
const range = sorted[sorted.length - 1] - sorted[0];
const smallestDelta = computeSmallestDelta(sorted);
let bucketSize = selectBucketSize({
range,
bucketCount,
smallestDelta,
bucketWidthOverride: bucketWidth,
});
if (bucketSize <= 0) {
bucketSize = range > 0 ? range / bucketCount : 1;
}
const getBucket = (v: number): number =>
roundDecimals(incrRoundDn(v - BUCKET_OFFSET, bucketSize) + BUCKET_OFFSET, 9);
const frames = buildFrames(result, mergeAllActiveQueries);
// Merged mode folds every query into frame 0 and leaves trailing empty
// frames — drop those. Per-query mode must keep one column per result row
// (even empty queries), or the data column count drifts below the series
// count `buildHistogramConfig` adds per row → uPlot renders nothing.
const histograms: AlignedData[] = frames
.filter((frame) => !mergeAllActiveQueries || frame.length > 0)
.map((frame) => buildHistogramBuckets(frame, getBucket, sortAscending));
if (histograms.length === 0) {
return [[]];
}
const merged = mergeAlignedDataTables(histograms);
replaceUndefinedWithNullInAlignedData(merged);
prependNullBinToFirstHistogramSeries(merged, bucketSize);
return merged;
}
function extractNumericValues(
result: MetricRangePayloadProps['data']['result'],
): number[] {
const values: number[] = [];
for (const item of result) {
for (const [, valueStr] of item.values) {
values.push(Number.parseFloat(valueStr) || 0);
}
}
return values;
}
function computeSmallestDelta(sortedValues: number[]): number {
if (sortedValues.length <= 1) {
return 0;
}
let smallest = Infinity;
for (let i = 1; i < sortedValues.length; i++) {
const delta = sortedValues[i] - sortedValues[i - 1];
if (delta > 0) {
smallest = Math.min(smallest, delta);
}
}
return smallest === Infinity ? 0 : smallest;
}
function selectBucketSize({
range,
bucketCount,
smallestDelta,
bucketWidthOverride,
}: {
range: number;
bucketCount: number;
smallestDelta: number;
bucketWidthOverride?: number;
}): number {
if (bucketWidthOverride != null && bucketWidthOverride > 0) {
return bucketWidthOverride;
}
const targetSize = range / bucketCount;
for (const candidate of histogramBucketSizes) {
if (targetSize < candidate && candidate >= smallestDelta) {
return candidate;
}
}
return 0;
}
// When merging is on, fold all frames into the first; the trailing empty
// frames stay in the array so downstream `.filter(length > 0)` drops them.
function buildFrames(
result: MetricRangePayloadProps['data']['result'],
mergeAllActiveQueries: boolean,
): number[][] {
const frames: number[][] = result.map((item) =>
item.values.map(([, valueStr]) => Number.parseFloat(valueStr) || 0),
);
if (mergeAllActiveQueries && frames.length > 1) {
const first = frames[0];
for (let i = 1; i < frames.length; i++) {
first.push(...frames[i]);
frames[i] = [];
}
}
return frames;
}

View File

@@ -1,6 +0,0 @@
import type { SectionConfig } from '../../types/sections';
export const sections: SectionConfig[] = [
{ kind: 'legend', controls: { position: true, mode: true } },
{ kind: 'buckets', controls: { count: true } },
];

View File

@@ -1,76 +0,0 @@
import { useCallback, useMemo } from 'react';
import type { DashboardtypesPieChartPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import Pie from 'container/DashboardContainer/visualization/charts/Pie/Pie';
import type { PieSlice } from 'container/DashboardContainer/visualization/charts/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import PanelStyles from '../../panel.module.scss';
import { PanelRendererProps } from '../../types/rendererProps';
import {
resolveDecimalPrecision,
resolveLegendPosition,
} from '../../utils/chartAppearanceMappings';
import { preparePieData } from './prepareData';
function PiePanelRenderer({
panelId,
panel,
data,
onClick,
}: PanelRendererProps<'signoz/PieChartPanel'>): JSX.Element {
const isDarkMode = useIsDarkMode();
// The registry guarantees this Renderer only runs when
// `panel.spec.plugin.kind === 'signoz/PieChartPanel'`, so the cast is a
// documented boundary narrowing. Memoized so the `?? {}` fallback doesn't
// produce a fresh object on each render.
const spec = useMemo<DashboardtypesPieChartPanelSpecDTO>(
() => (panel?.spec?.plugin?.spec ?? {}) as DashboardtypesPieChartPanelSpecDTO,
[panel?.spec?.plugin?.spec],
);
const slices = useMemo(
() =>
preparePieData({
payload: data?.payload,
customColors: spec.legend?.customColors,
isDarkMode,
}),
[data?.payload, spec.legend?.customColors, isDarkMode],
);
const decimalPrecision = useMemo(
() => resolveDecimalPrecision(spec.formatting?.decimalPrecision),
[spec.formatting?.decimalPrecision],
);
const legendPosition = useMemo(
() => resolveLegendPosition(spec.legend?.position),
[spec.legend?.position],
);
const handleSliceClick = useCallback(
(slice: PieSlice) => {
onClick?.({ label: slice.label, value: slice.value });
},
[onClick],
);
return (
<div data-testid="pie-panel-renderer" className={PanelStyles.panelContainer}>
<Pie
data={slices}
yAxisUnit={spec.formatting?.unit}
decimalPrecision={decimalPrecision}
isDarkMode={isDarkMode}
position={legendPosition}
id={panelId}
onSliceClick={handleSliceClick}
data-testid="pie-chart"
/>
</div>
);
}
export default PiePanelRenderer;

View File

@@ -1,13 +0,0 @@
import { DataSource } from 'types/common/queryBuilder';
import type { PanelDefinition } from '../../types/panelDefinition';
import Renderer from './Renderer';
import { sections } from './sections';
export const definition: PanelDefinition<'signoz/PieChartPanel'> = {
kind: 'signoz/PieChartPanel',
displayName: 'Pie Chart',
Renderer,
sections,
supportedSignals: [DataSource.METRICS, DataSource.LOGS, DataSource.TRACES],
};

View File

@@ -1,89 +0,0 @@
import { themeColors } from 'constants/theme';
import type { PieSlice } from 'container/DashboardContainer/visualization/charts/types';
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
import type { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
export interface PreparePieDataArgs {
payload: MetricRangePayloadProps | undefined;
/** Per-label colour overrides from `spec.legend.customColors`. */
customColors?: Record<string, string> | null;
isDarkMode: boolean;
}
// Local view of the scalar/table response. A pie issues a TABLE request (see
// `getGraphType`), which the API returns web-formatted: one aggregation column
// holds the value, the remaining (group) columns form the label, and each row's
// `data` is keyed by `column.id || column.name`. The generated `QueryData.table`
// types its columns loosely (no `isValueColumn`), so we cast to this precise
// shape once at the boundary rather than threading `any` through.
interface ScalarTableColumn {
name: string;
id?: string;
isValueColumn: boolean;
}
interface ScalarTableEntry {
queryName?: string;
legend?: string;
table?: {
columns: ScalarTableColumn[];
rows: { data: Record<string, string | number | null> }[];
};
}
/**
* Turns a query response into pie slices: one slice per group row.
*
* The reduced value-per-series lives in the scalar `table`, not the time-series
* `result[].values`. Because the pie's graphType is `TABLE`, the response is
* web-formatted and the table sits on `result[]`; we fall back to `newResult`
* for the non-`formatForWeb` shape. Labels come from the group column(s),
* colours honour `customColors` then fall back to a deterministic palette
* colour, and non-positive / non-numeric values are dropped.
*/
export function preparePieData({
payload,
customColors,
isDarkMode,
}: PreparePieDataArgs): PieSlice[] {
const primary = (payload?.data?.result ?? []) as unknown as ScalarTableEntry[];
const fallback = (payload?.data?.newResult?.data?.result ??
[]) as unknown as ScalarTableEntry[];
const entries = primary.some((entry) => entry.table) ? primary : fallback;
const colorMap = isDarkMode
? themeColors.chartcolors
: themeColors.lightModeColor;
const slices: PieSlice[] = [];
entries.forEach((entry) => {
const { table } = entry;
if (!table) {
return;
}
const valueColumn = table.columns.find((column) => column.isValueColumn);
if (!valueColumn) {
return;
}
const valueKey = valueColumn.id || valueColumn.name;
const labelColumns = table.columns.filter((column) => !column.isValueColumn);
table.rows.forEach((row) => {
const value = Number(row.data[valueKey]);
const label =
labelColumns
.map((column) => row.data[column.id || column.name])
.filter((part) => part != null)
.join(', ') ||
entry.legend ||
entry.queryName ||
'';
const color = customColors?.[label] ?? generateColor(label, colorMap);
slices.push({ label, value, color });
});
});
return slices.filter(
(slice) => Number.isFinite(slice.value) && slice.value > 0,
);
}

View File

@@ -1,8 +0,0 @@
import type { SectionConfig } from '../../types/sections';
// Pie has no axes, thresholds, or stacking — just value formatting and a
// legend. `mode` is omitted: the pie legend is always interactive swatches.
export const sections: SectionConfig[] = [
{ kind: 'formatting', controls: { unit: true, decimals: true } },
{ kind: 'legend', controls: { position: true } },
];

View File

@@ -1,154 +0,0 @@
import { useCallback, useMemo, useRef } from 'react';
import type { DashboardtypesTimeSeriesPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
import TooltipFooter from 'container/DashboardContainer/visualization/panels/components/TooltipFooter';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { IRenderTooltipFooterArgs } from 'lib/uPlotV2/components/types';
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
import { useTimezone } from 'providers/Timezone';
import { useGroupByPerQuery } from '../../hooks/useGroupByPerQuery';
import { useTimeScale } from '../../hooks/useTimeScale';
import PanelStyles from '../../panel.module.scss';
import { PanelRendererProps } from '../../types/rendererProps';
import {
resolveDecimalPrecision,
resolveLegendPosition,
} from '../../utils/chartAppearanceMappings';
import { getBuilderQueries } from '../../utils/getBuilderQueries';
import { buildTimeSeriesConfig } from './buildConfig';
import { ChartClickData } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
function TimeSeriesPanelRenderer({
panelId,
panel,
data,
onClick,
onDragSelect,
dashboardPreference,
panelMode,
}: PanelRendererProps<'signoz/TimeSeriesPanel'>): JSX.Element {
const graphRef = useRef<HTMLDivElement>(null);
const containerDimensions = useResizeObserver(graphRef);
const isDarkMode = useIsDarkMode();
const { timezone } = useTimezone();
// The registry guarantees this Renderer only runs when
// `panel.spec.plugin.kind === 'signoz/TimeSeriesPanel'`, so the cast is a
// documented boundary narrowing — not a blind assertion. Memoized so the
// `?? {}` fallback doesn't produce a fresh object on each render.
const spec = useMemo<DashboardtypesTimeSeriesPanelSpecDTO>(
() =>
(panel?.spec?.plugin?.spec ?? {}) as DashboardtypesTimeSeriesPanelSpecDTO,
[panel?.spec?.plugin?.spec],
);
const builderQueries = useMemo(
() => getBuilderQueries(panel?.spec?.queries),
[panel?.spec?.queries],
);
const { minTimeScale, maxTimeScale } = useTimeScale(data);
const groupByPerQuery = useGroupByPerQuery(builderQueries);
const config = useMemo(
() =>
buildTimeSeriesConfig({
panelId,
spec,
builderQueries,
apiResponse: data,
isDarkMode,
timezone,
panelMode,
minTimeScale,
maxTimeScale,
onDragSelect,
}),
[
panelId,
spec,
builderQueries,
data,
isDarkMode,
timezone,
panelMode,
minTimeScale,
maxTimeScale,
onDragSelect,
// `config` gets mutated by TooltipPlugin (config.setCursor for cursor sync).
// Rebuild it on syncMode changes so the new chart instance starts from a
// clean config — otherwise switching to "No Sync" would inherit stale sync
// settings from the previous mode.
dashboardPreference?.syncMode,
],
);
const chartData = useMemo(
() => (data?.payload ? prepareChartData(data.payload) : []),
[data?.payload],
);
const decimalPrecision = useMemo(
() => resolveDecimalPrecision(spec.formatting?.decimalPrecision),
[spec.formatting?.decimalPrecision],
);
const legendPosition = useMemo(() => {
return resolveLegendPosition(spec.legend?.position);
}, [spec.legend?.position]);
const renderTooltipFooter = useCallback(
({ isPinned, dismiss }: IRenderTooltipFooterArgs) => (
<TooltipFooter id={panelId} isPinned={isPinned} dismiss={dismiss} />
),
[panelId],
);
/**
* The uPlot key prop is the only way to force a full teardown and re-mount
* of the chart. By including the syncMode and syncFilterMode in the key,
* we ensure that changes to these preferences trigger a fresh chart instance,
* preventing stale sync settings from being inherited.
*/
const key = `${dashboardPreference?.syncMode}-${dashboardPreference?.syncFilterMode}`;
const handleChartClick = useCallback(
(args: ChartClickData) => {
onClick?.(args);
},
[onClick],
);
return (
<div
ref={graphRef}
data-testid="time-series-renderer"
className={PanelStyles.panelContainer}
>
{containerDimensions.width > 0 && containerDimensions.height > 0 && (
<TimeSeries
key={key}
config={config}
data={chartData}
legendConfig={{ position: legendPosition }}
groupByPerQuery={groupByPerQuery}
canPinTooltip
timezone={timezone}
yAxisUnit={spec.formatting?.unit}
decimalPrecision={decimalPrecision}
width={containerDimensions.width}
height={containerDimensions.height}
syncMode={dashboardPreference?.syncMode}
syncFilterMode={dashboardPreference?.syncFilterMode}
renderTooltipFooter={renderTooltipFooter}
onClick={handleChartClick}
/>
)}
</div>
);
}
export default TimeSeriesPanelRenderer;

View File

@@ -1,163 +0,0 @@
import type { DashboardtypesTimeSeriesPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import { buildBaseConfig } from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/baseConfigBuilder';
import {
FILL_MODE_MAP,
LINE_INTERPOLATION_MAP,
LINE_STYLE_MAP,
resolveSpanGaps,
} from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/chartAppearanceMappings';
import { resolveSeriesLabel } from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/resolveSeriesLabel';
import getLabelName from 'lib/getLabelName';
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
import {
DrawStyle,
FillMode,
LineInterpolation,
LineStyle,
} from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { hasSingleVisiblePoint } from 'lib/uPlotV2/utils/dataUtils';
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
import type { BuilderQuery } from 'types/api/v5/queryRange';
const DEFAULT_POINT_SIZE = 5;
export interface BuildTimeSeriesConfigArgs {
panelId: string;
spec: DashboardtypesTimeSeriesPanelSpecDTO;
/**
* Flat list of builder queries on this panel (see `getBuilderQueries`).
* Powers per-query legend resolution; empty for non-builder panels.
*/
builderQueries: BuilderQuery[];
apiResponse: MetricQueryRangeSuccessResponse | undefined;
isDarkMode: boolean;
timezone: Timezone;
panelMode: PanelMode;
onDragSelect?: (start: number, end: number) => void;
onClick?: OnClickPluginOpts['onClick'];
minTimeScale?: number;
maxTimeScale?: number;
}
/**
* Builds a fully-wired `UPlotConfigBuilder` for a TimeSeries panel.
*
* Delegates the panel-agnostic scaffolding (scales, thresholds, axes,
* drag-to-zoom, click plugin) to the shared `buildBaseConfig`, then layers
* in the TimeSeries-specific concern: one series per result, with visuals
* resolved from `spec.chartAppearance`.
*/
export function buildTimeSeriesConfig({
panelId,
spec,
builderQueries,
apiResponse,
isDarkMode,
timezone,
panelMode,
onDragSelect,
onClick,
minTimeScale,
maxTimeScale,
}: BuildTimeSeriesConfigArgs): UPlotConfigBuilder {
const builder = buildBaseConfig({
panelId,
panelType: PANEL_TYPES.TIME_SERIES,
isDarkMode,
timezone,
panelMode,
isLogScale: spec.axes?.isLogScale,
softMin: spec.axes?.softMin ?? undefined,
softMax: spec.axes?.softMax ?? undefined,
formatting: spec.formatting,
thresholds: spec.thresholds,
apiResponse,
minTimeScale,
maxTimeScale,
onDragSelect,
onClick,
});
addSeriesFromResponse({
builder,
spec,
builderQueries,
apiResponse,
isDarkMode,
});
return builder;
}
interface AddSeriesArgs {
builder: UPlotConfigBuilder;
spec: DashboardtypesTimeSeriesPanelSpecDTO;
builderQueries: BuilderQuery[];
apiResponse: MetricQueryRangeSuccessResponse | undefined;
isDarkMode: boolean;
}
/**
* Adds one uPlot series per result row to the scaffolded builder. The visual
* resolution (line style, interpolation, fill mode, span gaps) reads from
* `spec.chartAppearance`; the label is resolved via the legend matrix in
* `resolveSeriesLabel`. Mutates the builder in place.
*/
function addSeriesFromResponse({
builder,
spec,
builderQueries,
apiResponse,
isDarkMode,
}: AddSeriesArgs): void {
if (!apiResponse?.payload?.data?.result) {
return;
}
const chartAppearance = spec.chartAppearance;
// `customColors` is nullable on the spec; coerce so `addSeries` always gets
// a defined record (it dereferences keys without a guard).
const colorMapping = spec.legend?.customColors ?? {};
const spanGaps = resolveSpanGaps(chartAppearance?.spanGaps?.fillLessThan);
const lineStyle = chartAppearance?.lineStyle
? LINE_STYLE_MAP[chartAppearance.lineStyle]
: LineStyle.Solid;
const lineInterpolation = chartAppearance?.lineInterpolation
? LINE_INTERPOLATION_MAP[chartAppearance.lineInterpolation]
: LineInterpolation.Spline;
const fillMode = chartAppearance?.fillMode
? FILL_MODE_MAP[chartAppearance.fillMode]
: FillMode.None;
apiResponse.payload.data.result.forEach((series) => {
const hasSingleValidPoint = hasSingleVisiblePoint(series.values);
const baseLabel = getLabelName(
series.metric,
series.queryName || '',
series.legend || '',
);
const label = resolveSeriesLabel(series, builderQueries, baseLabel);
builder.addSeries({
scaleKey: 'y',
// A single visible point can't be drawn as a line — degrade to points
// so the user still sees the datum (matches V1 behavior).
drawStyle: hasSingleValidPoint ? DrawStyle.Points : DrawStyle.Line,
label,
colorMapping,
spanGaps,
lineStyle,
lineInterpolation,
showPoints: chartAppearance?.showPoints || hasSingleValidPoint,
pointSize: DEFAULT_POINT_SIZE,
fillMode,
isDarkMode,
metric: series.metric,
});
});
}

View File

@@ -1,13 +0,0 @@
import { DataSource } from 'types/common/queryBuilder';
import type { PanelDefinition } from '../../types/panelDefinition';
import Renderer from './Renderer';
import { sections } from './sections';
export const definition: PanelDefinition<'signoz/TimeSeriesPanel'> = {
kind: 'signoz/TimeSeriesPanel',
displayName: 'Time Series',
Renderer,
sections,
supportedSignals: [DataSource.METRICS, DataSource.LOGS, DataSource.TRACES],
};

View File

@@ -1,15 +0,0 @@
import type { SectionConfig } from '../../types/sections';
export const sections: SectionConfig[] = [
{
kind: 'formatting',
controls: {
unit: true,
decimals: true,
},
},
{ kind: 'axes', controls: { minMax: true, unit: true, logScale: true } },
{ kind: 'legend', controls: { position: true, mode: true } },
{ kind: 'thresholds', controls: { list: true } },
{ kind: 'chartAppearance', controls: { lineStyle: true, fillOpacity: true } },
];

View File

@@ -1,9 +0,0 @@
.panelContainer {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
}

View File

@@ -1,59 +0,0 @@
import type { ChartClickData } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
/**
* Source-tagged click events. The three uPlot panels share `ChartClickEvent`;
* each non-chart kind carries the context its drill-down needs. The `source`
* tag lets a kind-agnostic consumer (the render boundary, a shared drill-down
* handler) discriminate without assuming a chart shape.
*/
export type ChartClickEvent = ChartClickData;
export type TableClickEvent = {
rowData: Record<string, unknown>;
columnId?: string;
};
export type ListClickEvent = {
rowData: Record<string, unknown>;
};
export type PieClickEvent = { label: string; value: number };
/** Union of every panel click event — switched on by `source` at the boundary. */
export type PanelClickEvent =
| ChartClickEvent
| TableClickEvent
| ListClickEvent
| PieClickEvent;
type DragSelect = (start: number, end: number) => void;
/**
* Per-kind interaction props. Each panel kind exposes ONLY the gestures it
* supports: chart panels get a chart-shaped `onClick`, time-axis charts add
* `onDragSelect`, histograms have no drag-to-zoom, a NumberPanel has no
* interactions at all. Keys mirror `PanelKind`; `PanelRendererProps<K>` in
* rendererProps.ts indexes this map, so a missing kind is a compile error there.
*/
export interface PanelInteractionMap {
'signoz/TimeSeriesPanel': {
onClick?: (event: ChartClickEvent) => void;
onDragSelect?: DragSelect;
};
'signoz/BarChartPanel': {
onClick?: (event: ChartClickEvent) => void;
onDragSelect?: DragSelect;
};
'signoz/HistogramPanel': { onClick?: (event: ChartClickEvent) => void };
'signoz/TablePanel': { onClick?: (event: TableClickEvent) => void };
'signoz/ListPanel': { onClick?: (event: ListClickEvent) => void };
'signoz/PieChartPanel': { onClick?: (event: PieClickEvent) => void };
'signoz/NumberPanel': Record<string, never>;
}
/**
* Widest interaction surface — used where the panel kind is not known
* statically (the registry render boundary; see `getPanelDefinition`). It is
* the structural supertype the per-kind shapes are cast to exactly once.
*/
export interface AnyPanelInteractionProps {
onClick?: (event: PanelClickEvent) => void;
onDragSelect?: DragSelect;
}

View File

@@ -1,31 +0,0 @@
import type { ComponentType } from 'react';
import { DataSource } from 'types/common/queryBuilder';
import type { SectionConfig } from './sections';
import type { AnyPanelInteractionProps } from './interactions';
import type { PanelKind } from './panelKind';
import type { BaseRendererProps, PanelRendererProps } from './rendererProps';
export interface PanelDefinition<K extends PanelKind = PanelKind> {
kind: K;
displayName: string;
Renderer: ComponentType<PanelRendererProps<K>>;
sections: SectionConfig[];
supportedSignals: DataSource[];
}
// Keyed registry that preserves the kind ↔ definition correlation: indexing
// with a literal kind yields that kind's exactly-typed PanelDefinition.
export type PanelRegistry = { [K in PanelKind]?: PanelDefinition<K> };
// A PanelDefinition whose Renderer is widened to the kind-agnostic prop surface.
// At the render boundary the concrete kind isn't known statically (a registry
// lookup returns a union over kinds), so getPanelDefinition resolves to this —
// concentrating the single unavoidable cast in one place instead of leaking it
// to every call site.
export interface RenderablePanelDefinition extends Omit<
PanelDefinition,
'Renderer'
> {
Renderer: ComponentType<BaseRendererProps & AnyPanelInteractionProps>;
}

View File

@@ -1,20 +0,0 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
export type PanelKind =
| 'signoz/TimeSeriesPanel'
| 'signoz/BarChartPanel'
| 'signoz/NumberPanel'
| 'signoz/PieChartPanel'
| 'signoz/TablePanel'
| 'signoz/HistogramPanel'
| 'signoz/ListPanel';
export const PANEL_KIND_TO_PANEL_TYPE: Record<PanelKind, PANEL_TYPES> = {
'signoz/TimeSeriesPanel': PANEL_TYPES.TIME_SERIES,
'signoz/BarChartPanel': PANEL_TYPES.BAR,
'signoz/NumberPanel': PANEL_TYPES.VALUE,
'signoz/PieChartPanel': PANEL_TYPES.PIE,
'signoz/TablePanel': PANEL_TYPES.TABLE,
'signoz/HistogramPanel': PANEL_TYPES.HISTOGRAM,
'signoz/ListPanel': PANEL_TYPES.LIST,
};

View File

@@ -1,74 +0,0 @@
import type { DashboardtypesPanelDTO } from 'api/generated/services/sigNoz.schemas';
import type {
DashboardCursorSync,
SyncTooltipFilterMode,
} from 'lib/uPlotV2/plugins/TooltipPlugin/types';
import type { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import type { PanelInteractionMap } from './interactions';
import type { PanelKind } from './panelKind';
/**
* Dashboard-wide rendering preferences propagated down to every panel renderer
* on the same dashboard. Lets the shell push cross-panel concerns (cursor
* sync, tooltip filter mode, dashboard id for scoped state) without each
* renderer rediscovering them via hooks. All fields are optional — non-
* dashboard render contexts (PanelEditor preview, standalone view) can pass
* an empty object and the renderer will fall back to sensible defaults.
*/
export interface DashboardPreference {
/**
* Cursor-sync mode for the dashboard. Drives the uPlot tooltip plugin so
* hovering one panel highlights the corresponding x on every other panel.
*/
syncMode?: DashboardCursorSync;
/**
* Filter applied to the synced tooltip across panels (e.g. only show series
* whose label matches the hovered series).
*/
syncFilterMode?: SyncTooltipFilterMode;
/**
* Dashboard id — useful for renderers that scope per-dashboard state
* (e.g. pinned-tooltip persistence, drill-down history).
*/
dashboardId?: string;
}
// Kind-agnostic props every renderer receives, regardless of panel kind. The
// kind-specific interaction props (onClick payload, onDragSelect) are layered
// on per-kind by PanelRendererProps<K>.
export interface BaseRendererProps {
panelId: string;
/**
* The whole perses panel — renderers derive their concrete `spec` and the
* perses-shaped `queries` from this. Passing the full panel keeps the prop
* surface stable as new panel-level fields are added to the wire format.
*/
panel: DashboardtypesPanelDTO | undefined;
data: MetricQueryRangeSuccessResponse | undefined;
isLoading: boolean;
error: Error | null;
/** Gate for the drill-down right-click menu. Off by default in V2. */
enableDrillDown?: boolean;
/**
* Render context — varies behavior (e.g. dashboard widget vs. standalone
* full-screen vs. inside the editor). See PanelMode for the contract.
*/
panelMode: PanelMode;
/**
* Dashboard-level preferences that should propagate to every panel
* (cursor sync, tooltip filter mode, dashboard id). The shell owns
* resolving these; the renderer just consumes them.
*/
dashboardPreference?: DashboardPreference;
}
// Renderer props for a specific panel kind: the shared base plus that kind's
// interaction surface (PanelInteractionMap[K]). Each renderer annotates with
// its own kind — e.g. PanelRendererProps<'signoz/TimeSeriesPanel'> — so it can
// only reference the gestures that kind supports. Indexing PanelInteractionMap
// here forces the map to cover every PanelKind. The default K = PanelKind
// yields the widest surface (a union over all kinds).
export type PanelRendererProps<K extends PanelKind = PanelKind> =
BaseRendererProps & PanelInteractionMap[K];

View File

@@ -1,55 +0,0 @@
import {
BarChart,
Columns3,
Hash,
ListEnd,
Palette,
Ruler,
SlidersHorizontal,
} from '@signozhq/icons';
// Derived from an actual icon component so the type stays exact (size is a
// constrained IconSize union, not arbitrary strings) and ForwardRef-compatible.
export type SectionIcon = typeof Hash;
export interface SectionMetadata {
title: string;
icon: SectionIcon;
description?: string;
}
// Per-kind control toggles (type-only — runtime metadata is in SECTIONS).
// Section components type their controls prop via `SectionControls['axes']`.
export type SectionControls = {
formatting: { unit?: boolean; decimals?: boolean };
axes: { minMax?: boolean; unit?: boolean; logScale?: boolean };
legend: { position?: boolean; mode?: boolean };
thresholds: { list?: boolean };
chartAppearance: {
lineStyle?: boolean;
fillOpacity?: boolean;
stacked?: boolean;
};
columnUnits: { perColumnUnit?: boolean };
buckets: { count?: boolean; min?: boolean; max?: boolean };
};
// Source of truth for sections. Its keys define SectionKind; its values are the
// runtime UI metadata (consumed by PanelEditor in 1.8). Adding a new section =
// one entry here + one entry in SectionControls.
export const SECTIONS = {
formatting: { title: 'Formatting', icon: Hash },
axes: { title: 'Axes', icon: Ruler },
legend: { title: 'Legend', icon: ListEnd },
thresholds: { title: 'Thresholds', icon: SlidersHorizontal },
chartAppearance: { title: 'Chart appearance', icon: Palette },
columnUnits: { title: 'Column units', icon: Columns3 },
buckets: { title: 'Buckets', icon: BarChart },
} as const satisfies Record<string, SectionMetadata>;
export type SectionKind = keyof typeof SECTIONS;
// Discriminated union derived from SectionControls — kept in lockstep automatically.
export type SectionConfig = {
[K in SectionKind]: { kind: K; controls: SectionControls[K] };
}[SectionKind];

View File

@@ -1,208 +0,0 @@
import type {
DashboardtypesPanelFormattingDTO,
DashboardtypesThresholdWithLabelDTO,
} from 'api/generated/services/sigNoz.schemas';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import onClickPlugin, {
OnClickPluginOpts,
} from 'lib/uPlotLib/plugins/onClickPlugin';
import {
DistributionType,
SelectionPreferencesSource,
} from 'lib/uPlotV2/config/types';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { ThresholdsDrawHookOptions } from 'lib/uPlotV2/hooks/types';
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
/**
* Inputs for the shared V2 chart pipeline. Mirrors the V1 helper of the same
* name but accepts perses-shaped inputs directly (so callers don't translate
* once per panel). The series-rendering step is panel-specific and lives in
* each panel's `utils.ts` — this helper only wires the scaffolding (scales,
* thresholds, axes, drag-to-zoom, click plugin).
*/
export interface BuildBaseConfigArgs {
panelId: string;
panelType: PANEL_TYPES;
isDarkMode: boolean;
timezone: Timezone;
panelMode: PanelMode;
/** From `spec.axes` — drives the Y scale and (when log) both scales' base. */
isLogScale?: boolean;
softMin?: number;
softMax?: number;
/** From `spec.formatting.unit` — drives Y axis tick formatting + threshold formatting. */
formatting?: DashboardtypesPanelFormattingDTO;
/** From `spec.thresholds` — perses shape, mapped to the draw-hook shape internally. */
thresholds?: DashboardtypesThresholdWithLabelDTO[] | null;
/** Query-range response — used by the click plugin to map pixel → data. */
apiResponse: MetricQueryRangeSuccessResponse | undefined;
/** Time-range clamps for the X scale (typically from `getTimeRange(apiResponse)`). */
minTimeScale?: number;
maxTimeScale?: number;
/** Optional — histogram and other non-time panels omit drag-to-zoom. */
onDragSelect?: (start: number, end: number) => void;
onClick?: OnClickPluginOpts['onClick'];
}
/**
* Builds the panel-agnostic scaffolding of a uPlot chart: scales, thresholds,
* axes, drag-to-zoom, click plugin. Callers (TimeSeriesPanel, BarPanel, …)
* then call `addSeries`/`addPlugin` on the returned builder for their own
* panel-specific rendering.
*/
export function buildBaseConfig({
panelId,
panelType,
isDarkMode,
timezone,
panelMode,
isLogScale,
softMin,
softMax,
formatting,
thresholds,
apiResponse,
minTimeScale,
maxTimeScale,
onDragSelect,
onClick,
}: BuildBaseConfigArgs): UPlotConfigBuilder {
const yAxisUnit = formatting?.unit;
const builder = new UPlotConfigBuilder({
id: panelId,
onDragSelect,
tzDate: makeTzDate(timezone),
shouldSaveSelectionPreference: panelMode === PanelMode.DASHBOARD_VIEW,
selectionPreferencesSource: resolveSelectionPreferencesSource(panelMode),
stepInterval: resolveStepInterval(apiResponse),
});
const thresholdOptions: ThresholdsDrawHookOptions = {
scaleKey: 'y',
thresholds: mapThresholds(thresholds),
yAxisUnit,
};
builder.addThresholds(thresholdOptions);
builder.addScale({
scaleKey: 'x',
time: true,
min: minTimeScale,
max: maxTimeScale,
logBase: isLogScale ? 10 : undefined,
distribution: isLogScale
? DistributionType.Logarithmic
: DistributionType.Linear,
});
builder.addScale({
scaleKey: 'y',
time: false,
min: undefined,
max: undefined,
softMin,
softMax,
thresholds: thresholdOptions,
logBase: isLogScale ? 10 : undefined,
distribution: isLogScale
? DistributionType.Logarithmic
: DistributionType.Linear,
});
if (typeof onClick === 'function') {
builder.addPlugin(
onClickPlugin({ onClick, apiResponse: apiResponse?.payload }),
);
}
builder.addAxis({
scaleKey: 'x',
show: true,
side: 2,
isDarkMode,
isLogScale,
panelType,
});
builder.addAxis({
scaleKey: 'y',
show: true,
side: 3,
isDarkMode,
isLogScale,
yAxisUnit,
panelType,
});
return builder;
}
function makeTzDate(
timezone: Timezone,
): ((timestamp: number) => Date) | undefined {
if (!timezone) {
return undefined;
}
return (timestamp: number): Date =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value);
}
function resolveSelectionPreferencesSource(
panelMode: PanelMode,
): SelectionPreferencesSource {
return panelMode === PanelMode.DASHBOARD_VIEW ||
panelMode === PanelMode.STANDALONE_VIEW
? SelectionPreferencesSource.LOCAL_STORAGE
: SelectionPreferencesSource.IN_MEMORY;
}
// Perses-shape thresholds → the draw-hook shape uPlotV2 consumes. Exported so
// panels that need to feed the same threshold list elsewhere (e.g. to a series
// `addSeries` thresholds hook) don't have to redo the mapping.
export function mapThresholds(
thresholds: DashboardtypesThresholdWithLabelDTO[] | null | undefined,
): ThresholdsDrawHookOptions['thresholds'] {
if (!thresholds || thresholds.length === 0) {
return [];
}
return thresholds.map((t) => ({
thresholdValue: t.value,
thresholdColor: t.color,
thresholdUnit: t.unit,
thresholdLabel: t.label,
}));
}
/**
* V5 backend reports per-query step intervals; we feed the smallest one through
* to uPlot so the X-axis tick density matches the densest query. An empty map
* yields `Infinity` from `Math.min`, which would corrupt downstream scale math —
* fall back to `undefined` (uPlot's "auto") in that case.
*/
export function resolveStepInterval(
apiResponse: MetricQueryRangeSuccessResponse | undefined,
): number | undefined {
const stepIntervals =
apiResponse?.payload?.data?.newResult?.meta?.stepIntervals;
if (!stepIntervals) {
return undefined;
}
const values = Object.values(stepIntervals);
if (values.length === 0) {
return undefined;
}
const min = Math.min(...values);
return Number.isFinite(min) ? min : undefined;
}

View File

@@ -1,108 +0,0 @@
import {
DashboardtypesFillModeDTO,
DashboardtypesLegendPositionDTO,
DashboardtypesLineInterpolationDTO,
DashboardtypesLineStyleDTO,
DashboardtypesPrecisionOptionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import {
FillMode,
LineInterpolation,
LineStyle,
} from 'lib/uPlotV2/config/types';
/**
* Bridges the V2 dashboard wire-format enums (snake_case, generated from Go)
* to the uPlotV2 chart enums (PascalCase). String values diverge between the
* two — don't coerce, map.
*
* Kept as a single source of truth so every panel that reads chart-appearance
* fields stays in sync as either side's enum evolves.
*/
export const LINE_STYLE_MAP: Record<DashboardtypesLineStyleDTO, LineStyle> = {
[DashboardtypesLineStyleDTO.solid]: LineStyle.Solid,
[DashboardtypesLineStyleDTO.dashed]: LineStyle.Dashed,
};
export const LINE_INTERPOLATION_MAP: Record<
DashboardtypesLineInterpolationDTO,
LineInterpolation
> = {
[DashboardtypesLineInterpolationDTO.linear]: LineInterpolation.Linear,
[DashboardtypesLineInterpolationDTO.spline]: LineInterpolation.Spline,
[DashboardtypesLineInterpolationDTO.step_after]: LineInterpolation.StepAfter,
[DashboardtypesLineInterpolationDTO.step_before]: LineInterpolation.StepBefore,
};
export const FILL_MODE_MAP: Record<DashboardtypesFillModeDTO, FillMode> = {
[DashboardtypesFillModeDTO.solid]: FillMode.Solid,
[DashboardtypesFillModeDTO.gradient]: FillMode.Gradient,
[DashboardtypesFillModeDTO.none]: FillMode.None,
};
export const LEGEND_POSITION_MAP: Record<
DashboardtypesLegendPositionDTO,
LegendPosition
> = {
[DashboardtypesLegendPositionDTO.bottom]: LegendPosition.BOTTOM,
[DashboardtypesLegendPositionDTO.right]: LegendPosition.RIGHT,
};
/**
* `spec.formatting.decimalPrecision` is a stringified-digit enum on the wire
* (`'0'``'4'` plus the sentinel `'full'`). The chart consumes a numeric
* `PrecisionOption` (`0``4`) or the same `'full'` sentinel from its own
* enum. Missing / unknown → `undefined` (chart uses its default).
*/
export function resolveDecimalPrecision(
precision: DashboardtypesPrecisionOptionDTO | undefined,
): PrecisionOption | undefined {
if (!precision) {
return undefined;
}
if (precision === DashboardtypesPrecisionOptionDTO.full) {
return PrecisionOptionsEnum.FULL;
}
const parsed = Number(precision);
if (
parsed === 0 ||
parsed === 1 ||
parsed === 2 ||
parsed === 3 ||
parsed === 4
) {
return parsed;
}
return undefined;
}
/**
* `spec.chartAppearance.spanGaps.fillLessThan` is a stringified number on the
* wire. Empty / missing → span all gaps (the chart default). Numeric → forward
* the threshold so uPlot only bridges short runs of nulls.
*/
export function resolveSpanGaps(
fillLessThan: string | undefined,
): boolean | number {
if (!fillLessThan) {
return true;
}
const parsed = Number(fillLessThan);
return Number.isFinite(parsed) ? parsed : true;
}
/**
* Resolves the legend position for a panel. Missing / unknown values fall
* back to `BOTTOM` to match the chart's default and the V1 behavior.
*/
export function resolveLegendPosition(
position: DashboardtypesLegendPositionDTO | undefined,
): LegendPosition {
if (position && position in LEGEND_POSITION_MAP) {
return LEGEND_POSITION_MAP[position];
}
return LegendPosition.BOTTOM;
}

View File

@@ -1,38 +0,0 @@
import type { DashboardtypesQueryDTO } from 'api/generated/services/sigNoz.schemas';
import type { BuilderQuery } from 'types/api/v5/queryRange';
/**
* Flattens a panel's queries into the list of builder queries it contains —
* unwrapping `CompositeQuery` envelopes along the way. Non-builder kinds
* (PromQL, ClickHouseSQL, Formula, TraceOperator) are dropped: they don't
* carry the legend / groupBy / aggregation context downstream code needs.
*
* Returns the generated v5 `BuilderQuery` shape directly — no intermediate
* summary type — so callers consume the same type the wire format defines.
*/
export function getBuilderQueries(
queries: DashboardtypesQueryDTO[] | undefined,
): BuilderQuery[] {
if (!queries) {
return [];
}
const flattened: BuilderQuery[] = [];
queries.forEach((envelope) => {
const plugin = envelope?.spec?.plugin;
if (!plugin) {
return;
}
if (plugin.kind === 'signoz/BuilderQuery') {
flattened.push(plugin.spec as BuilderQuery);
return;
}
if (plugin.kind === 'signoz/CompositeQuery') {
(plugin.spec.queries ?? []).forEach((sub) => {
if (sub.type === 'builder_query') {
flattened.push(sub.spec as BuilderQuery);
}
});
}
});
return flattened;
}

View File

@@ -1,112 +0,0 @@
import type { BuilderQuery } from 'types/api/v5/queryRange';
import type { QueryData } from 'types/api/widgets/getQuery';
/**
* Resolves the display label for one series, applying the V1 legend matrix:
* `single-vs-many builder queries × with/without groupBy × single-vs-many
* aggregations`. Returns `baseLabel` unchanged for panels without builder
* queries (PromQL, ClickHouseSQL) and for builder series whose aggregation
* carries no alias/expression — metric aggregations don't have those fields,
* so they naturally short-circuit to the base label here.
*/
export function resolveSeriesLabel(
series: QueryData,
builderQueries: BuilderQuery[],
baseLabel: string,
): string {
if (builderQueries.length === 0) {
return baseLabel;
}
const matching = builderQueries.find((q) => q.name === series.queryName);
if (!matching) {
return baseLabel;
}
// `series.metaData.index` points to the aggregation in the matched query
// that produced this series. Default to 0 so single-aggregation panels
// still resolve when the backend omits the field.
const aggIndex = series.metaData?.index ?? 0;
const aggregations = matching.aggregations ?? [];
const aggregation = aggregations[aggIndex];
// `alias` / `expression` exist on Log/Trace aggregations only —
// `MetricAggregation` carries `metricName`/`temporality`/… instead. The
// `in` guards narrow the union without a cast.
const aggregationAlias =
aggregation && 'alias' in aggregation ? (aggregation.alias ?? '') : '';
const aggregationExpression =
aggregation && 'expression' in aggregation
? (aggregation.expression ?? '')
: '';
if (!aggregationAlias && !aggregationExpression) {
return baseLabel || series.metaData?.queryName || matching.name || '';
}
const ctx: FormatContext = {
aggregationAlias,
aggregationExpression,
baseLabel,
legend: matching.legend ?? '',
hasGroupBy: (matching.groupBy?.length ?? 0) > 0,
singleAggregation: aggregations.length === 1,
};
return builderQueries.length === 1
? formatForSinglePanelQuery(ctx)
: formatForMultiplePanelQueries(ctx);
}
interface FormatContext {
aggregationAlias: string;
aggregationExpression: string;
baseLabel: string;
legend: string;
hasGroupBy: boolean;
singleAggregation: boolean;
}
// Panel has one builder query — ports V1's `getLegendForSingleAggregation`.
function formatForSinglePanelQuery({
aggregationAlias,
aggregationExpression,
baseLabel,
legend,
hasGroupBy,
singleAggregation,
}: FormatContext): string {
if (hasGroupBy) {
if (singleAggregation) {
return baseLabel;
}
return `${aggregationAlias || aggregationExpression}-${baseLabel}`;
}
if (singleAggregation) {
return aggregationAlias || legend || aggregationExpression;
}
return aggregationAlias || aggregationExpression;
}
// Panel has multiple builder queries — ports V1's `getLegendForMultipleAggregations`.
// Differs from the single-query path in two cells: the no-groupBy / single-agg
// cell falls through to `baseLabel` instead of `legend`, and the no-groupBy /
// multi-agg cell prepends the base label.
function formatForMultiplePanelQueries({
aggregationAlias,
aggregationExpression,
baseLabel,
hasGroupBy,
singleAggregation,
}: FormatContext): string {
if (hasGroupBy) {
if (singleAggregation) {
return baseLabel;
}
return `${aggregationAlias || aggregationExpression}-${baseLabel}`;
}
if (singleAggregation) {
return aggregationAlias || baseLabel || aggregationExpression;
}
return `${aggregationAlias || aggregationExpression}-${baseLabel}`;
}

View File

@@ -3,8 +3,8 @@
flex-direction: column;
height: 100%;
width: 100%;
background: var(--l2-background);
border: 1px solid var(--l2-border);
background: var(--bg-ink-400, #0b0c0e);
border: 1px solid var(--l1-border);
border-radius: 4px;
overflow: hidden;
}
@@ -14,7 +14,7 @@
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid var(--l2-border);
border-bottom: 1px solid var(--l1-border);
cursor: grab;
}
@@ -36,15 +36,6 @@
margin-inline-end: 0;
}
// Actions sit inside the drag-handle row but opt out of dragging
// (`panel-no-drag`); reset the grab cursor so the menu reads as clickable.
.actions {
display: flex;
align-items: center;
gap: 4px;
cursor: default;
}
.body {
flex: 1;
display: flex;
@@ -59,41 +50,3 @@
.bodyKind {
margin-bottom: 6px;
}
// Container for the rendered chart — fills the panel below the header and lets
// the chart shrink (min-* 0) so it resizes with the grid cell.
.chartBody {
flex: 1;
display: flex;
min-height: 0;
min-width: 0;
}
// Subtle background-refetch spinner in the header (chart stays mounted).
.refetchIndicator {
color: var(--l2-foreground);
flex-shrink: 0;
}
// Error state — shown only when there's no stale data to fall back to.
.error {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px;
text-align: center;
}
.errorIcon {
color: var(--bg-cherry-500);
}
.errorMessage {
color: var(--l2-foreground);
font-size: 12px;
max-width: 90%;
overflow-wrap: anywhere;
}

View File

@@ -1,15 +1,15 @@
import { useMemo } from 'react';
import { Badge } from '@signozhq/ui/badge';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import { EllipsisVertical } from '@signozhq/icons';
import type { DashboardtypesPanelDTO } from 'api/generated/services/sigNoz.schemas';
import { getPanelDefinition } from 'pages/DashboardPageV2/DashboardContainer/Panels';
import { usePanelQuery } from 'pages/DashboardPageV2/DashboardContainer/hooks/usePanelQuery';
import cx from 'classnames';
import type { DashboardSection } from '../../utils';
import type { DeletePanelArgs } from './hooks/useDeletePanel';
import { usePanelInteractions } from './hooks/usePanelInteractions';
import type { MovePanelArgs } from './hooks/useMovePanelToSection';
import PanelBody from './PanelBody';
import PanelHeader from './PanelHeader';
import PanelActionsMenu from './PanelActionsMenu/PanelActionsMenu';
import styles from './Panel.module.scss';
/** Panel action context — present together only in editable sectioned mode. */
@@ -23,18 +23,16 @@ export interface PanelActionsConfig {
interface PanelProps {
panel: DashboardtypesPanelDTO | undefined;
panelId: string;
/** True once this panel's section enters the viewport — gates the fetch. */
/**
* Placeholder: true once this panel's section enters the viewport. The panel
* query-loading implementation (later PR) will consume this to lazily fetch
* data. Currently unused on purpose.
*/
isVisible?: boolean;
/** Move/delete actions — present only in editable sectioned mode. */
panelActions?: PanelActionsConfig;
}
/**
* A single dashboard panel: chrome (header) + content (body). Thin orchestrator
* — data fetching lives in `usePanelQuery`, cross-panel interactions in
* `usePanelInteractions`, and the loading/error/chart state machine in
* `PanelBody`.
*/
function Panel({
panel,
panelId,
@@ -43,22 +41,9 @@ function Panel({
}: PanelProps): JSX.Element {
const name = panel?.spec?.display?.name || `Panel ${panelId.slice(0, 6)}`;
const description = panel?.spec?.display?.description;
const fullKind = panel?.spec?.plugin?.kind;
const kind = fullKind?.replace(/^signoz\//, '') ?? 'unknown';
const kind = panel?.spec?.plugin?.kind?.replace(/^signoz\//, '') ?? 'unknown';
const queryCount = panel?.spec?.queries?.length ?? 0;
const panelDef = getPanelDefinition(fullKind);
const { data, isLoading, isFetching, error, refetch } = usePanelQuery({
panel,
panelId,
// Lazy: only fetch once the section is on screen (undefined → treat as
// visible) and a renderer exists for the kind.
enabled: !!panelDef && isVisible !== false,
});
const { onDragSelect, dashboardPreference } = usePanelInteractions();
const headerTitle = useMemo(() => {
if (!description) {
return name;
@@ -75,27 +60,35 @@ function Panel({
className={styles.panel}
data-panel-visible={isVisible ? 'true' : 'false'}
>
<PanelHeader
title={headerTitle}
panelId={panelId}
isFetching={isFetching}
error={error}
warning={data?.warning}
panelActions={panelActions}
/>
<PanelBody
panelDef={panelDef}
panel={panel}
panelId={panelId}
kind={kind}
queryCount={queryCount}
data={data}
isLoading={isLoading}
error={error}
refetch={refetch}
onDragSelect={onDragSelect}
dashboardPreference={dashboardPreference}
/>
<div className={cx(styles.header, 'panel-drag-handle')}>
<div className={styles.headerLeft}>
<Typography.Text className={styles.headerTitle}>
{headerTitle}
</Typography.Text>
<Badge className={styles.badge}>{kind}</Badge>
</div>
{panelActions ? (
<PanelActionsMenu
panelId={panelId}
currentLayoutIndex={panelActions.currentLayoutIndex}
sections={panelActions.sections}
onMovePanel={panelActions.onMovePanel}
onDeletePanel={panelActions.onDeletePanel}
/>
) : (
<EllipsisVertical size={14} />
)}
</div>
<div className={styles.body}>
<div>
<div className={styles.bodyKind}>{kind} panel</div>
<div>
{queryCount} {queryCount === 1 ? 'query' : 'queries'} · chart rendering
coming next
</div>
</div>
</div>
</div>
);
}

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