Compare commits

...

74 Commits

Author SHA1 Message Date
Naman Verma
7d3612c10a feat: textbox variable 2026-03-25 13:21:46 +05:30
Naman Verma
c82cd32f61 Merge branch 'main' into nv/4172 2026-03-25 13:11:49 +05:30
Nityananda Gohain
658f794842 chore: add tests for trace waterfall (#10690)
* chore: add tests for trace waterfall

* chore: remove unhelpful tests
2026-03-25 07:13:13 +00:00
primus-bot[bot]
e9abd5ddfc chore(release): bump to v0.117.0 (#10707)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-03-25 06:54:49 +00:00
Piyush Singariya
ea2663b145 fix: enrich unspecified fields in logs pipelines filters (#10686)
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
* fix: enrich unspecified fields

* fix: return error in enrich function

* chore: nit change asked
2026-03-25 05:01:26 +00:00
Pandey
234716df53 fix(querier): return proper HTTP status for PromQL timeout errors (#10689)
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
* fix(querier): return proper HTTP status for PromQL timeout errors

PromQL queries hitting the context deadline were incorrectly returning
400 Bad Request with "invalid_input" because enhancePromQLError
unconditionally wrapped all errors as TypeInvalidInput. Extract
tryEnhancePromQLExecError to properly classify timeout, cancellation,
and storage errors before falling through to parse error handling.

Also make the PromQL engine timeout configurable via prometheus.timeout
config (default 2m) instead of hardcoding it.

* chore: refactor files

* fix(prometheus): validate timeout config and fix test setups

Add validation in prometheus.Config to reject zero timeout. Update all
test files to explicitly set Timeout: 2 * time.Minute in prometheus.Config
literals to avoid immediate query timeouts.
2026-03-24 13:32:45 +00:00
Naman Verma
a3980e084c Merge branch 'main' into nv/4172 2026-03-24 16:44:50 +05:30
Naman Verma
4319dd9cef chore: doc for how to add a panel spec 2026-03-24 16:44:34 +05:30
Vinicius Lourenço
531979543c fix(infra-monitoring): volume details charts rendering undefined as legend (#10658)
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-03-24 11:06:19 +00:00
Naman Verma
92a5e9b9c9 chore: common threshold type 2026-03-24 16:23:11 +05:30
Naman Verma
408a914129 chore: change attr name to name 2026-03-24 16:13:09 +05:30
Naman Verma
0e304b1d40 fix: normalise enum defs 2026-03-24 16:12:09 +05:30
swapnil-signoz
4b09f057b9 feat: adding handlers with OpenAPI specs (#10643)
* feat: adding cloud integration type for refactor

* refactor: store interfaces to use local types and error

* feat: adding sql store implementation

* refactor: removing interface check

* feat: adding updated types for cloud integration

* refactor: using struct for map

* refactor: update cloud integration types and module interface

* fix: correct GetService signature and remove shadowed Data field

* feat: implement cloud integration store

* refactor: adding comments and removed wrong code

* refactor: streamlining types

* refactor: add comments for backward compatibility in PostableAgentCheckInRequest

* refactor: update Dashboard struct comments and remove unused fields

* refactor: split upsert store method

* feat: adding integration test

* refactor: clean up types

* refactor: renaming service type to service id

* refactor: using serviceID type

* feat: adding method for service id creation

* refactor: updating store methods

* refactor: clean up

* refactor: clean up

* refactor: review comments

* refactor: clean up

* feat: adding handlers

* fix: lint and ci issues

* fix: lint issues

* fix: update error code for service not found

* feat: adding handler skeleton

* chore: removing todo comment

* feat: adding frontend openapi schema

* refactor: making review changes

* feat: regenerating openapi specs
2026-03-24 10:24:38 +00:00
Naman Verma
58a9be24d3 chore: single version for all schemas 2026-03-24 14:07:24 +05:30
Naman Verma
adf439fcf1 fix: proper type for selectFields 2026-03-24 13:56:14 +05:30
Naman Verma
a1a54c4bb2 fix: promql step duration schema 2026-03-24 13:53:19 +05:30
Naman Verma
3c1961d3fc fix: datasource in perses.json 2026-03-24 13:44:58 +05:30
Naman Verma
c3efa0660b fix: only allow one of metric or expr aggregation in builder query 2026-03-24 13:40:42 +05:30
Naman Verma
183dd09082 fix: functions in formula 2026-03-24 13:34:59 +05:30
Nityananda Gohain
dde7c79b4d fix: prevent duplicate and incorrect results from trace_summary timerange override in list view (#10637)
* fix: updated implementation for using trace summary in list view

* chore: move trace optimisation outside of statement builder

* fix: lint issues

* chore: update comments in integration tests

* chore: remove unnecessary test

* fix: py-fmt
2026-03-24 08:04:28 +00:00
Naman Verma
a351373c49 chore: common package for variables' repeated definitions 2026-03-24 13:23:53 +05:30
Naman Verma
8e7653b90d chore: common package for queries' repeated definitions 2026-03-24 13:22:49 +05:30
Naman Verma
5c40d6b68b chore: common package for panels' repeated definitions 2026-03-24 13:19:12 +05:30
Naman Verma
31115df41c chore: actually name every panel as a panel 2026-03-24 13:05:55 +05:30
Naman Verma
869c3dccb2 fix: less verbose field names in dynamic var 2026-03-24 12:57:34 +05:30
Naman Verma
c5d7a7ef8c fix: no nesting in context links 2026-03-24 12:55:40 +05:30
Naman Verma
544b87b254 chore: remove unimplemented join query schema 2026-03-24 12:48:36 +05:30
Naman Verma
e885fb98e5 chore: no need for threshold prefix inside threshold obj 2026-03-24 12:44:50 +05:30
Naman Verma
be227eec43 chore: replace yAxisUnit by unit 2026-03-24 12:41:16 +05:30
Tushar Vats
c95523c747 feat: export traces (#9991)
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
* feat: added trace export

feat: added types for export

feat: added support for complex queries

fix: added correct open api spec

fix: updated unit tests

fix: type handling logic

fix: improve order by

feat: added integration tests

fix: address comments

* fix: address comments

* fix: removed nits

* fix: go fmt

* fix: rebased main and ran generate cmd

* fix: renamed method

* fix: address comments

* fix: lint error

* fix: lint error

* fix: ran yarn generate:api

* fix: address comments

* fix: address comments

* fix: typo

* fix: better names for functions

* fix: added unit tests, renamed file, added validation

* fix: update integration test

* fix: removed get method for export

* fix: yarn generate:api

* chore: yarn generate:api

* fix: rename file

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-03-23 21:46:56 +00:00
Pandey
63cb54c5b0 feat(factory): add service state tracking, AwaitHealthy, depends_on, and /healthz (#10671)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat(factory): add service state tracking, AwaitHealthy, depends_on, and /healthz endpoint

Add explicit lifecycle state tracking to factory.Registry services
(starting/running/failed) modeled after Guava's ServiceManager. Services
can declare dependencies via NewNamedService(..., dependsOn) which are
validated for unknown refs and cycles at registry creation. AwaitHealthy
blocks until all services reach running state. A /healthz endpoint is
wired through signozapiserver returning 200/503 with per-service state.

* feat(apiserver): move health endpoints to /api/v2/ and register readyz, livez

* refactor(factory): use gonum for cycle detection, return error on cycles, fix test assertions

Replace custom DFS cycle detection with gonum's topo.Sort + TarjanSCC.
Dependency cycles now return an error from NewRegistry instead of being
silently dropped. Use assert for final test assertions and require only
for intermediate setup errors.

* chore: go mod tidy

* refactor(factory): decouple Handler from Registry, wire through Handlers struct

Move Handler implementation to a private handler struct with NewHandler
constructor instead of methods on *Registry. Route handler through the
existing Handlers struct as RegistryHandler. Rename healthz.go to
registry.go in signozapiserver. Fix handler_test.go for new param.

* feat(factory): add ServiceWithHealthy interface, add Healthy to authz, user depends on authz

Add ServiceWithHealthy interface embedding Service + Healthy. NamedService
now delegates Healthy() to the underlying service, eliminating unwrapService.
AuthZ interface requires Healthy(), implemented in both pkg and ee providers.
User service declares dependency on authz via dependsOn.

* test(integration): use /api/v2/healthz for readiness check, log 503 response body

* fix(factory): replace fmt.Errorf with errors.Newf in tests to satisfy linter

* feat: generate openapi spec

* fix(integration): log errors at error level in healthz readiness check

* test(integration): log and assert healthz response in test_setup

* feat(user): implement ServiceWithHealthy for user service

User service signals healthy after successful root user reconciliation
or immediately when disabled. User Service interface now embeds
factory.ServiceWithHealthy.

* fix(factory): reflect service names as strings

* fix(apiserver): document health 503 responses

* feat: generate openapi spec
2026-03-23 18:46:15 +00:00
Vishal Sharma
19e8196472 feat: add onboarding configurations and new datasource (#10680)
Mistral AI, OpenClaw, Claude Agent SDK, and Render, update icon fetching documentation
2026-03-23 18:07:42 +00:00
Ashwin Bhatkal
c360e4498d refactor: move dashboard provider from redux to zustand (#10628)
* chore: move dashboard provider from redux to zustand

* chore: replace useDashboard with useDashboardStore (#10629)

* chore: derive dashboard locked state from global state (#10645)

* chore: remove usage of updatedTimeRef in dashboard provider (#10551)

* chore: removed updatedTimeRef from global store

* chore: removed updatedTimeRef from global store

* chore: remove dashboardQueryRangeCalled from global dashboard state (#10650)

* chore: remove dashboardQueryRangeCalled from global dashboard state

* chore: cleanup dashboard page setup (#10652)

* chore: update tests from dashboard provider migration (#10653)

* chore: update tests from dashboard provider migration

* chore: cleaner local storage variable update (#10656)
2026-03-23 15:14:01 +00:00
Naman Verma
13263c1f25 Merge branch 'main' into nv/4172 2026-03-23 20:01:04 +05:30
Naman Verma
ccbf410d15 fix: no more online validation 2026-03-23 16:23:51 +05:30
Naman Verma
03b98ff824 chore: remaining fields file 2026-03-23 16:10:01 +05:30
Naman Verma
2cdba0d11c chore: examples for panel types 2026-03-23 16:06:29 +05:30
Naman Verma
84d2885530 chore: a more complex example 2026-03-23 16:03:18 +05:30
Naman Verma
b82dcc6138 docs: list panel schema without upstream ref 2026-03-23 15:31:53 +05:30
Naman Verma
a14d5847b9 docs: histogram chart panel schema without upstream ref 2026-03-23 15:21:27 +05:30
Naman Verma
d184746142 docs: table chart panel schema without upstream ref 2026-03-23 15:17:21 +05:30
Naman Verma
c335e17e1d docs: pie chart panel schema without upstream ref 2026-03-23 14:33:05 +05:30
Naman Verma
433dd0b2d0 docs: number panel schema without upstream ref 2026-03-23 14:27:46 +05:30
Naman Verma
05e97e246a docs: number panel schema without upstream ref 2026-03-23 14:27:37 +05:30
Naman Verma
bddfe30f6c docs: bar chart panel schema without upstream ref 2026-03-23 14:22:00 +05:30
Naman Verma
7a01a5250d chore: object for visualization section 2026-03-23 14:16:07 +05:30
Naman Verma
09c98c830d docs: time series panel schema without upstream ref 2026-03-23 14:11:02 +05:30
Naman Verma
0fbb90cc91 fix: promql fix 2026-03-23 14:09:48 +05:30
Naman Verma
15f0787610 chore: remove upstream import 2026-03-23 13:07:48 +05:30
Naman Verma
22ebc7732c feat: promql example 2026-03-22 19:39:57 +05:30
Naman Verma
cff18edf6e chore: comment explaining when to use composite query and when not 2026-03-22 19:22:45 +05:30
Naman Verma
cb49c0bf3b feat: custom time series schema 2026-03-22 19:18:15 +05:30
Naman Verma
1cb6f94d21 fix: proper composite query schema 2026-03-19 16:18:31 +05:30
Naman Verma
68155f374b chore: folders in schemas for arranging 2026-03-19 15:15:06 +05:30
Naman Verma
696524509f chore: folders in schemas for arranging 2026-03-19 15:14:43 +05:30
Naman Verma
705cdab38c chore: rename 2026-03-19 14:24:10 +05:30
Naman Verma
ae9b881413 chore: py script not needed 2026-03-19 01:39:17 +05:30
Naman Verma
05f4e15d07 chore: rearrange specs in package.json 2026-03-19 01:39:08 +05:30
Naman Verma
1653c6d725 chore: checkpoint for half correct setup 2026-03-19 01:34:22 +05:30
Naman Verma
070b4b7061 chore: test file with way more examples 2026-03-18 22:01:16 +05:30
Naman Verma
7f4c06edd6 chore: test file with way more examples 2026-03-18 22:01:10 +05:30
Naman Verma
6bed20b5b9 fix: remove fields from variable specs that are there in ListVariable 2026-03-18 19:19:00 +05:30
Naman Verma
033bd3c9b8 feat: validation script 2026-03-18 15:52:05 +05:30
Naman Verma
d4c9a923fd chore: no commons (for now) 2026-03-18 15:28:35 +05:30
Naman Verma
387dcb529f chore: no config folder 2026-03-18 15:04:16 +05:30
Naman Verma
7a4da7bcc5 chore: no config folder 2026-03-18 15:04:02 +05:30
Naman Verma
b152fae3fa chore: remove validate file 2026-03-18 15:03:19 +05:30
Naman Verma
2ed766726c chore: remove manually written manifest and package 2026-03-18 15:02:23 +05:30
Naman Verma
8767f6a57d chore: remove stub for time series chart 2026-03-18 15:01:58 +05:30
Naman Verma
22d8c7599b chore: rm comment 2026-03-18 13:10:19 +05:30
Naman Verma
1019264272 chore: no need for PageSize type in commons, only used once 2026-03-18 13:07:31 +05:30
Naman Verma
c950d7e784 chore: no need for Signal type in commons, only used once 2026-03-18 13:05:32 +05:30
Naman Verma
1e279e6193 Merge branch 'main' into nv/4172 2026-03-18 13:03:09 +05:30
Naman Verma
d3a278c43e docs: perses schema for dashboards 2026-03-17 14:56:07 +05:30
180 changed files with 13291 additions and 2097 deletions

View File

@@ -17,5 +17,7 @@
},
"[html]": {
"editor.defaultFormatter": "vscode.html-language-features"
}
},
"python-envs.defaultEnvManager": "ms-python.python:system",
"python-envs.pythonProjects": []
}

View File

@@ -144,6 +144,8 @@ telemetrystore:
##################### Prometheus #####################
prometheus:
# The maximum time a PromQL query is allowed to run before being aborted.
timeout: 2m
active_query_tracker:
# Whether to enable the active query tracker.
enabled: true

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.116.1
image: signoz/signoz:v0.117.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.116.1
image: signoz/signoz:v0.117.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.116.1}
image: signoz/signoz:${VERSION:-v0.117.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.116.1}
image: signoz/signoz:${VERSION:-v0.117.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

File diff suppressed because it is too large Load Diff

View File

@@ -123,6 +123,7 @@ if err := router.Handle("/api/v1/things", handler.New(
Description: "This endpoint creates a thing",
Request: new(types.PostableThing),
RequestContentType: "application/json",
RequestQuery: new(types.QueryableThing),
Response: new(types.GettableThing),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
@@ -155,6 +156,8 @@ The `handler.New` function ties the HTTP handler to OpenAPI metadata via `OpenAP
- **Request / RequestContentType**:
- `Request` is a Go type that describes the request body or form.
- `RequestContentType` is usually `"application/json"` or `"application/x-www-form-urlencoded"` (for callbacks like SAML).
- **RequestQuery**:
- `RequestQuery` is a Go type that descirbes query url params.
- **RequestExamples**: An array of `handler.OpenAPIExample` that provide concrete request payloads in the generated spec. See [Adding request examples](#adding-request-examples) below.
- **Response / ResponseContentType**:
- `Response` is the Go type for the successful response payload.

View File

@@ -273,6 +273,7 @@ Options can be simple (direct link) or nested (with another question):
- Place logo files in `public/Logos/`
- Use SVG format
- Reference as `"/Logos/your-logo.svg"`
- **Fetching Icons**: New icons can be easily fetched from [OpenBrand](https://openbrand.sh/). Use the pattern `https://openbrand.sh/?url=<TARGET_URL>`, where `<TARGET_URL>` is the URL-encoded link to the service's website. For example, to get Render's logo, use [https://openbrand.sh/?url=https%3A%2F%2Frender.com](https://openbrand.sh/?url=https%3A%2F%2Frender.com).
- **Optimize new SVGs**: Run any newly downloaded SVGs through an optimizer like [SVGOMG (svgo)](https://svgomg.net/) or use `npx svgo public/Logos/your-logo.svg` to minimise their size before committing.
### 4. Links

View File

@@ -57,6 +57,10 @@ func (provider *provider) Start(ctx context.Context) error {
return provider.openfgaServer.Start(ctx)
}
func (provider *provider) Healthy() <-chan struct{} {
return provider.openfgaServer.Healthy()
}
func (provider *provider) Stop(ctx context.Context) error {
return provider.openfgaServer.Stop(ctx)
}

View File

@@ -16,7 +16,6 @@ type Server struct {
}
func NewOpenfgaServer(ctx context.Context, pkgAuthzService authz.AuthZ) (*Server, error) {
return &Server{
pkgAuthzService: pkgAuthzService,
}, nil
@@ -26,6 +25,10 @@ func (server *Server) Start(ctx context.Context) error {
return server.pkgAuthzService.Start(ctx)
}
func (server *Server) Healthy() <-chan struct{} {
return server.pkgAuthzService.Healthy()
}
func (server *Server) Stop(ctx context.Context) error {
return server.pkgAuthzService.Stop(ctx)
}

View File

@@ -136,6 +136,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
signoz.SQLStore,
integrationsController.GetPipelinesForInstalledIntegrations,
reader,
)
if err != nil {
return nil, err

View File

@@ -257,7 +257,7 @@ func TestManager_TestNotification_SendUnmatched_PromRule(t *testing.T) {
WillReturnRows(samplesRows)
// Create Prometheus provider for this test
promProvider = prometheustest.New(context.Background(), instrumentationtest.New().ToProviderSettings(), prometheus.Config{}, store)
promProvider = prometheustest.New(context.Background(), instrumentationtest.New().ToProviderSettings(), prometheus.Config{Timeout: 2 * time.Minute}, store)
},
ManagerOptionsHook: func(opts *rules.ManagerOptions) {
// Set Prometheus provider for PromQL queries

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#fa520f" viewBox="0 0 24 24"><title>Mistral AI</title><path d="M17.143 3.429v3.428h-3.429v3.429h-3.428V6.857H6.857V3.43H3.43v13.714H0v3.428h10.286v-3.428H6.857v-3.429h3.429v3.429h3.429v-3.429h3.428v3.429h-3.428v3.428H24v-3.428h-3.43V3.429z"/></svg>

After

Width:  |  Height:  |  Size: 294 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 120 120"><defs><linearGradient id="a" x1="0%" x2="100%" y1="0%" y2="100%"><stop offset="0%" stop-color="#ff4d4d"/><stop offset="100%" stop-color="#991b1b"/></linearGradient></defs><path fill="url(#a)" d="M60 10c-30 0-45 25-45 45s15 40 30 45v10h10v-10s5 2 10 0v10h10v-10c15-5 30-25 30-45S90 10 60 10"/><path fill="url(#a)" d="M20 45C5 40 0 50 5 60s15 5 20-5c3-7 0-10-5-10"/><path fill="url(#a)" d="M100 45c15-5 20 5 15 15s-15 5-20-5c-3-7 0-10 5-10"/><path stroke="#ff4d4d" stroke-linecap="round" stroke-width="3" d="M45 15Q35 5 30 8M75 15Q85 5 90 8"/><circle cx="45" cy="35" r="6" fill="#050810"/><circle cx="75" cy="35" r="6" fill="#050810"/><circle cx="46" cy="34" r="2.5" fill="#00e5cc"/><circle cx="76" cy="34" r="2.5" fill="#00e5cc"/></svg>

After

Width:  |  Height:  |  Size: 809 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Render</title><path d="M18.263.007c-3.121-.147-5.744 2.109-6.192 5.082-.018.138-.045.272-.067.405-.696 3.703-3.936 6.507-7.827 6.507a7.9 7.9 0 0 1-3.825-.979.202.202 0 0 0-.302.178V24H12v-8.999c0-1.656 1.338-3 2.987-3h2.988c3.382 0 6.103-2.817 5.97-6.244-.12-3.084-2.61-5.603-5.682-5.75"/></svg>

After

Width:  |  Height:  |  Size: 362 B

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,250 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import type {
InvalidateOptions,
QueryClient,
QueryFunction,
QueryKey,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import { useQuery } from 'react-query';
import type { ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
Healthz200,
Healthz503,
Livez200,
Readyz200,
Readyz503,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
/**
* @summary Health check
*/
export const healthz = (signal?: AbortSignal) => {
return GeneratedAPIInstance<Healthz200>({
url: `/api/v2/healthz`,
method: 'GET',
signal,
});
};
export const getHealthzQueryKey = () => {
return [`/api/v2/healthz`] as const;
};
export const getHealthzQueryOptions = <
TData = Awaited<ReturnType<typeof healthz>>,
TError = ErrorType<Healthz503>
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof healthz>>, TError, TData>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getHealthzQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof healthz>>> = ({
signal,
}) => healthz(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof healthz>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type HealthzQueryResult = NonNullable<
Awaited<ReturnType<typeof healthz>>
>;
export type HealthzQueryError = ErrorType<Healthz503>;
/**
* @summary Health check
*/
export function useHealthz<
TData = Awaited<ReturnType<typeof healthz>>,
TError = ErrorType<Healthz503>
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof healthz>>, TError, TData>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getHealthzQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Health check
*/
export const invalidateHealthz = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getHealthzQueryKey() },
options,
);
return queryClient;
};
/**
* @summary Liveness check
*/
export const livez = (signal?: AbortSignal) => {
return GeneratedAPIInstance<Livez200>({
url: `/api/v2/livez`,
method: 'GET',
signal,
});
};
export const getLivezQueryKey = () => {
return [`/api/v2/livez`] as const;
};
export const getLivezQueryOptions = <
TData = Awaited<ReturnType<typeof livez>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof livez>>, TError, TData>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getLivezQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof livez>>> = ({
signal,
}) => livez(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof livez>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type LivezQueryResult = NonNullable<Awaited<ReturnType<typeof livez>>>;
export type LivezQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Liveness check
*/
export function useLivez<
TData = Awaited<ReturnType<typeof livez>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof livez>>, TError, TData>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getLivezQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Liveness check
*/
export const invalidateLivez = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries({ queryKey: getLivezQueryKey() }, options);
return queryClient;
};
/**
* @summary Readiness check
*/
export const readyz = (signal?: AbortSignal) => {
return GeneratedAPIInstance<Readyz200>({
url: `/api/v2/readyz`,
method: 'GET',
signal,
});
};
export const getReadyzQueryKey = () => {
return [`/api/v2/readyz`] as const;
};
export const getReadyzQueryOptions = <
TData = Awaited<ReturnType<typeof readyz>>,
TError = ErrorType<Readyz503>
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof readyz>>, TError, TData>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getReadyzQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof readyz>>> = ({
signal,
}) => readyz(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof readyz>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ReadyzQueryResult = NonNullable<Awaited<ReturnType<typeof readyz>>>;
export type ReadyzQueryError = ErrorType<Readyz503>;
/**
* @summary Readiness check
*/
export function useReadyz<
TData = Awaited<ReturnType<typeof readyz>>,
TError = ErrorType<Readyz503>
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof readyz>>, TError, TData>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getReadyzQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Readiness check
*/
export const invalidateReadyz = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getReadyzQueryKey() },
options,
);
return queryClient;
};

View File

@@ -20,11 +20,113 @@ import { useMutation, useQuery } from 'react-query';
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
HandleExportRawDataPOSTParams,
ListPromotedAndIndexedPaths200,
PromotetypesPromotePathDTO,
Querybuildertypesv5QueryRangeRequestDTO,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
/**
* This endpoints allows complex query exporting raw data for traces and logs
* @summary Export raw data
*/
export const handleExportRawDataPOST = (
querybuildertypesv5QueryRangeRequestDTO: BodyType<Querybuildertypesv5QueryRangeRequestDTO>,
params?: HandleExportRawDataPOSTParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/export_raw_data`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: querybuildertypesv5QueryRangeRequestDTO,
params,
signal,
});
};
export const getHandleExportRawDataPOSTMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof handleExportRawDataPOST>>,
TError,
{
data: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
params?: HandleExportRawDataPOSTParams;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof handleExportRawDataPOST>>,
TError,
{
data: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
params?: HandleExportRawDataPOSTParams;
},
TContext
> => {
const mutationKey = ['handleExportRawDataPOST'];
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 handleExportRawDataPOST>>,
{
data: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
params?: HandleExportRawDataPOSTParams;
}
> = (props) => {
const { data, params } = props ?? {};
return handleExportRawDataPOST(data, params);
};
return { mutationFn, ...mutationOptions };
};
export type HandleExportRawDataPOSTMutationResult = NonNullable<
Awaited<ReturnType<typeof handleExportRawDataPOST>>
>;
export type HandleExportRawDataPOSTMutationBody = BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
export type HandleExportRawDataPOSTMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Export raw data
*/
export const useHandleExportRawDataPOST = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof handleExportRawDataPOST>>,
TError,
{
data: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
params?: HandleExportRawDataPOSTParams;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof handleExportRawDataPOST>>,
TError,
{
data: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
params?: HandleExportRawDataPOSTParams;
},
TContext
> => {
const mutationOptions = getHandleExportRawDataPOSTMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoints promotes and indexes paths
* @summary Promote and index paths

View File

@@ -437,6 +437,436 @@ export interface AuthtypesUpdateableAuthDomainDTO {
config?: AuthtypesAuthDomainConfigDTO;
}
export interface CloudintegrationtypesAWSAccountConfigDTO {
/**
* @type array
*/
regions: string[];
}
export type CloudintegrationtypesAWSCollectionStrategyDTOS3Buckets = {
[key: string]: string[];
};
export interface CloudintegrationtypesAWSCollectionStrategyDTO {
aws_logs?: CloudintegrationtypesAWSLogsStrategyDTO;
aws_metrics?: CloudintegrationtypesAWSMetricsStrategyDTO;
/**
* @type object
*/
s3_buckets?: CloudintegrationtypesAWSCollectionStrategyDTOS3Buckets;
}
export interface CloudintegrationtypesAWSConnectionArtifactDTO {
/**
* @type string
*/
connectionURL: string;
}
export interface CloudintegrationtypesAWSConnectionArtifactRequestDTO {
/**
* @type string
*/
deploymentRegion: string;
/**
* @type array
*/
regions: string[];
}
export interface CloudintegrationtypesAWSIntegrationConfigDTO {
/**
* @type array
*/
enabledRegions: string[];
telemetry: CloudintegrationtypesAWSCollectionStrategyDTO;
}
export type CloudintegrationtypesAWSLogsStrategyDTOCloudwatchLogsSubscriptionsItem = {
/**
* @type string
*/
filter_pattern?: string;
/**
* @type string
*/
log_group_name_prefix?: string;
};
export interface CloudintegrationtypesAWSLogsStrategyDTO {
/**
* @type array
* @nullable true
*/
cloudwatch_logs_subscriptions?:
| CloudintegrationtypesAWSLogsStrategyDTOCloudwatchLogsSubscriptionsItem[]
| null;
}
export type CloudintegrationtypesAWSMetricsStrategyDTOCloudwatchMetricStreamFiltersItem = {
/**
* @type array
*/
MetricNames?: string[];
/**
* @type string
*/
Namespace?: string;
};
export interface CloudintegrationtypesAWSMetricsStrategyDTO {
/**
* @type array
* @nullable true
*/
cloudwatch_metric_stream_filters?:
| CloudintegrationtypesAWSMetricsStrategyDTOCloudwatchMetricStreamFiltersItem[]
| null;
}
export interface CloudintegrationtypesAWSServiceConfigDTO {
logs?: CloudintegrationtypesAWSServiceLogsConfigDTO;
metrics?: CloudintegrationtypesAWSServiceMetricsConfigDTO;
}
export type CloudintegrationtypesAWSServiceLogsConfigDTOS3Buckets = {
[key: string]: string[];
};
export interface CloudintegrationtypesAWSServiceLogsConfigDTO {
/**
* @type boolean
*/
enabled?: boolean;
/**
* @type object
*/
s3_buckets?: CloudintegrationtypesAWSServiceLogsConfigDTOS3Buckets;
}
export interface CloudintegrationtypesAWSServiceMetricsConfigDTO {
/**
* @type boolean
*/
enabled?: boolean;
}
export interface CloudintegrationtypesAccountDTO {
agentReport: CloudintegrationtypesAgentReportDTO;
config: CloudintegrationtypesAccountConfigDTO;
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
id: string;
/**
* @type string
*/
orgId: string;
/**
* @type string
*/
provider: string;
/**
* @type string
* @nullable true
*/
providerAccountId: string | null;
/**
* @type string
* @format date-time
* @nullable true
*/
removedAt: Date | null;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
}
export interface CloudintegrationtypesAccountConfigDTO {
aws: CloudintegrationtypesAWSAccountConfigDTO;
}
/**
* @nullable
*/
export type CloudintegrationtypesAgentReportDTOData = {
[key: string]: unknown;
} | null;
/**
* @nullable
*/
export type CloudintegrationtypesAgentReportDTO = {
/**
* @type object
* @nullable true
*/
data: CloudintegrationtypesAgentReportDTOData;
/**
* @type integer
* @format int64
*/
timestampMillis: number;
} | null;
export interface CloudintegrationtypesAssetsDTO {
/**
* @type array
* @nullable true
*/
dashboards?: CloudintegrationtypesDashboardDTO[] | null;
}
export interface CloudintegrationtypesCollectedLogAttributeDTO {
/**
* @type string
*/
name?: string;
/**
* @type string
*/
path?: string;
/**
* @type string
*/
type?: string;
}
export interface CloudintegrationtypesCollectedMetricDTO {
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name?: string;
/**
* @type string
*/
type?: string;
/**
* @type string
*/
unit?: string;
}
export interface CloudintegrationtypesCollectionStrategyDTO {
aws: CloudintegrationtypesAWSCollectionStrategyDTO;
}
export interface CloudintegrationtypesConnectionArtifactDTO {
aws: CloudintegrationtypesAWSConnectionArtifactDTO;
}
export interface CloudintegrationtypesConnectionArtifactRequestDTO {
aws: CloudintegrationtypesAWSConnectionArtifactRequestDTO;
}
export interface CloudintegrationtypesDashboardDTO {
definition?: DashboardtypesStorableDashboardDataDTO;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
id?: string;
/**
* @type string
*/
title?: string;
}
export interface CloudintegrationtypesDataCollectedDTO {
/**
* @type array
* @nullable true
*/
logs?: CloudintegrationtypesCollectedLogAttributeDTO[] | null;
/**
* @type array
* @nullable true
*/
metrics?: CloudintegrationtypesCollectedMetricDTO[] | null;
}
export interface CloudintegrationtypesGettableAccountWithArtifactDTO {
connectionArtifact: CloudintegrationtypesConnectionArtifactDTO;
/**
* @type string
*/
id: string;
}
export interface CloudintegrationtypesGettableAccountsDTO {
/**
* @type array
*/
accounts: CloudintegrationtypesAccountDTO[];
}
export interface CloudintegrationtypesGettableAgentCheckInResponseDTO {
/**
* @type string
*/
account_id: string;
/**
* @type string
*/
cloud_account_id: string;
/**
* @type string
*/
cloudIntegrationId: string;
integration_config: CloudintegrationtypesIntegrationConfigDTO;
integrationConfig: CloudintegrationtypesProviderIntegrationConfigDTO;
/**
* @type string
*/
providerAccountId: string;
/**
* @type string
* @format date-time
* @nullable true
*/
removed_at: Date | null;
/**
* @type string
* @format date-time
* @nullable true
*/
removedAt: Date | null;
}
export interface CloudintegrationtypesGettableServicesMetadataDTO {
/**
* @type array
*/
services: CloudintegrationtypesServiceMetadataDTO[];
}
/**
* @nullable
*/
export type CloudintegrationtypesIntegrationConfigDTO = {
/**
* @type array
*/
enabled_regions: string[];
telemetry: CloudintegrationtypesAWSCollectionStrategyDTO;
} | null;
/**
* @nullable
*/
export type CloudintegrationtypesPostableAgentCheckInRequestDTOData = {
[key: string]: unknown;
} | null;
export interface CloudintegrationtypesPostableAgentCheckInRequestDTO {
/**
* @type string
*/
account_id?: string;
/**
* @type string
*/
cloud_account_id?: string;
/**
* @type string
*/
cloudIntegrationId?: string;
/**
* @type object
* @nullable true
*/
data: CloudintegrationtypesPostableAgentCheckInRequestDTOData;
/**
* @type string
*/
providerAccountId?: string;
}
export interface CloudintegrationtypesProviderIntegrationConfigDTO {
aws: CloudintegrationtypesAWSIntegrationConfigDTO;
}
export interface CloudintegrationtypesServiceDTO {
assets: CloudintegrationtypesAssetsDTO;
dataCollected: CloudintegrationtypesDataCollectedDTO;
/**
* @type string
*/
icon: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
overview: string;
serviceConfig?: CloudintegrationtypesServiceConfigDTO;
supported_signals: CloudintegrationtypesSupportedSignalsDTO;
telemetryCollectionStrategy: CloudintegrationtypesCollectionStrategyDTO;
/**
* @type string
*/
title: string;
}
export interface CloudintegrationtypesServiceConfigDTO {
aws: CloudintegrationtypesAWSServiceConfigDTO;
}
export interface CloudintegrationtypesServiceMetadataDTO {
/**
* @type boolean
*/
enabled: boolean;
/**
* @type string
*/
icon: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
title: string;
}
export interface CloudintegrationtypesSupportedSignalsDTO {
/**
* @type boolean
*/
logs?: boolean;
/**
* @type boolean
*/
metrics?: boolean;
}
export interface CloudintegrationtypesUpdatableAccountDTO {
config: CloudintegrationtypesAccountConfigDTO;
}
export interface CloudintegrationtypesUpdatableServiceDTO {
config: CloudintegrationtypesServiceConfigDTO;
}
export interface DashboardtypesDashboardDTO {
/**
* @type string
@@ -543,6 +973,23 @@ export interface ErrorsResponseerroradditionalDTO {
message?: string;
}
/**
* @nullable
*/
export type FactoryResponseDTOServices = { [key: string]: string[] } | null;
export interface FactoryResponseDTO {
/**
* @type boolean
*/
healthy?: boolean;
/**
* @type object
* @nullable true
*/
services?: FactoryResponseDTOServices;
}
/**
* @nullable
*/
@@ -2841,6 +3288,97 @@ export type AuthzResources200 = {
export type ChangePasswordPathParameters = {
id: string;
};
export type AgentCheckInDeprecatedPathParameters = {
cloudProvider: string;
};
export type AgentCheckInDeprecated200 = {
data: CloudintegrationtypesGettableAgentCheckInResponseDTO;
/**
* @type string
*/
status: string;
};
export type ListAccountsPathParameters = {
cloudProvider: string;
};
export type ListAccounts200 = {
data: CloudintegrationtypesGettableAccountsDTO;
/**
* @type string
*/
status: string;
};
export type CreateAccountPathParameters = {
cloudProvider: string;
};
export type CreateAccount200 = {
data: CloudintegrationtypesGettableAccountWithArtifactDTO;
/**
* @type string
*/
status: string;
};
export type DisconnectAccountPathParameters = {
cloudProvider: string;
id: string;
};
export type GetAccountPathParameters = {
cloudProvider: string;
id: string;
};
export type GetAccount200 = {
data: CloudintegrationtypesAccountDTO;
/**
* @type string
*/
status: string;
};
export type UpdateAccountPathParameters = {
cloudProvider: string;
id: string;
};
export type AgentCheckInPathParameters = {
cloudProvider: string;
};
export type AgentCheckIn200 = {
data: CloudintegrationtypesGettableAgentCheckInResponseDTO;
/**
* @type string
*/
status: string;
};
export type ListServicesMetadataPathParameters = {
cloudProvider: string;
};
export type ListServicesMetadata200 = {
data: CloudintegrationtypesGettableServicesMetadataDTO;
/**
* @type string
*/
status: string;
};
export type GetServicePathParameters = {
cloudProvider: string;
serviceId: string;
};
export type GetService200 = {
data: CloudintegrationtypesServiceDTO;
/**
* @type string
*/
status: string;
};
export type UpdateServicePathParameters = {
cloudProvider: string;
serviceId: string;
};
export type CreateSessionByGoogleCallback303 = {
data: AuthtypesGettableTokenDTO;
/**
@@ -2942,6 +3480,19 @@ export type DeleteAuthDomainPathParameters = {
export type UpdateAuthDomainPathParameters = {
id: string;
};
export type HandleExportRawDataPOSTParams = {
/**
* @enum csv,jsonl
* @type string
* @description The output format for the export.
*/
format?: HandleExportRawDataPOSTFormat;
};
export enum HandleExportRawDataPOSTFormat {
csv = 'csv',
jsonl = 'jsonl',
}
export type GetFieldsKeysParams = {
/**
* @description undefined
@@ -3457,6 +4008,30 @@ export type SearchIngestionKeys200 = {
status: string;
};
export type Healthz200 = {
data: FactoryResponseDTO;
/**
* @type string
*/
status: string;
};
export type Healthz503 = {
data: FactoryResponseDTO;
/**
* @type string
*/
status: string;
};
export type Livez200 = {
data: FactoryResponseDTO;
/**
* @type string
*/
status: string;
};
export type ListMetricsParams = {
/**
* @type integer
@@ -3592,6 +4167,22 @@ export type GetMyOrganization200 = {
status: string;
};
export type Readyz200 = {
data: FactoryResponseDTO;
/**
* @type string
*/
status: string;
};
export type Readyz503 = {
data: FactoryResponseDTO;
/**
* @type string
*/
status: string;
};
export type GetSessionContext200 = {
data: AuthtypesSessionContextDTO;
/**

View File

@@ -7,7 +7,7 @@ import ROUTES from 'constants/routes';
import useUpdatedQuery from 'container/GridCardLayout/useResolveQuery';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useNotifications } from 'hooks/useNotifications';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { AppState } from 'store/reducers';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
@@ -79,7 +79,7 @@ export function useNavigateToExplorer(): (
);
const { getUpdatedQuery } = useUpdatedQuery();
const { selectedDashboard } = useDashboard();
const { selectedDashboard } = useDashboardStore();
const { notifications } = useNotifications();
return useCallback(

View File

@@ -86,8 +86,8 @@ jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: (): boolean => false,
}));
jest.mock('providers/Dashboard/Dashboard', () => ({
useDashboard: (): { selectedDashboard: undefined } => ({
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): { selectedDashboard: undefined } => ({
selectedDashboard: undefined,
}),
}));

View File

@@ -1,4 +1,6 @@
import { ReactNode } from 'react';
import { MemoryRouter, useLocation } from 'react-router-dom';
import { useDashboardBootstrap } from 'hooks/dashboard/useDashboardBootstrap';
import {
getDashboardById,
getNonIntegrationDashboardById,
@@ -6,10 +8,9 @@ import {
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import {
DashboardContext,
DashboardProvider,
} from 'providers/Dashboard/Dashboard';
import { IDashboardContext } from 'providers/Dashboard/types';
resetDashboard,
useDashboardStore,
} from 'providers/Dashboard/store/useDashboardStore';
import {
fireEvent,
render,
@@ -21,6 +22,18 @@ import { Dashboard } from 'types/api/dashboard/getAll';
import DashboardDescription from '..';
function DashboardBootstrapWrapper({
dashboardId,
children,
}: {
dashboardId: string;
children: ReactNode;
}): JSX.Element {
useDashboardBootstrap(dashboardId);
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
}
interface MockSafeNavigateReturn {
safeNavigate: jest.MockedFunction<(url: string) => void>;
}
@@ -54,6 +67,7 @@ describe('Dashboard landing page actions header tests', () => {
beforeEach(() => {
mockSafeNavigate.mockClear();
sessionStorage.clear();
resetDashboard();
});
it('unlock dashboard should be disabled for integrations created dashboards', async () => {
@@ -64,7 +78,7 @@ describe('Dashboard landing page actions header tests', () => {
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const { getByTestId } = render(
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
<DashboardProvider dashboardId="4">
<DashboardBootstrapWrapper dashboardId="4">
<DashboardDescription
handle={{
active: false,
@@ -73,7 +87,7 @@ describe('Dashboard landing page actions header tests', () => {
node: { current: null },
}}
/>
</DashboardProvider>
</DashboardBootstrapWrapper>
</MemoryRouter>,
);
@@ -105,7 +119,7 @@ describe('Dashboard landing page actions header tests', () => {
);
const { getByTestId } = render(
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
<DashboardProvider dashboardId="4">
<DashboardBootstrapWrapper dashboardId="4">
<DashboardDescription
handle={{
active: false,
@@ -114,7 +128,7 @@ describe('Dashboard landing page actions header tests', () => {
node: { current: null },
}}
/>
</DashboardProvider>
</DashboardBootstrapWrapper>
</MemoryRouter>,
);
@@ -144,7 +158,7 @@ describe('Dashboard landing page actions header tests', () => {
const { getByText } = render(
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
<DashboardProvider dashboardId="4">
<DashboardBootstrapWrapper dashboardId="4">
<DashboardDescription
handle={{
active: false,
@@ -153,7 +167,7 @@ describe('Dashboard landing page actions header tests', () => {
node: { current: null },
}}
/>
</DashboardProvider>
</DashboardBootstrapWrapper>
</MemoryRouter>,
);
@@ -181,37 +195,26 @@ describe('Dashboard landing page actions header tests', () => {
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const mockContextValue: IDashboardContext = {
isDashboardLocked: false,
handleDashboardLockToggle: jest.fn(),
dashboardResponse: {} as IDashboardContext['dashboardResponse'],
useDashboardStore.setState({
selectedDashboard: (getDashboardById.data as unknown) as Dashboard,
layouts: [],
panelMap: {},
setPanelMap: jest.fn(),
setLayouts: jest.fn(),
setSelectedDashboard: jest.fn(),
updatedTimeRef: { current: null },
updateLocalStorageDashboardVariables: jest.fn(),
dashboardQueryRangeCalled: false,
setDashboardQueryRangeCalled: jest.fn(),
isDashboardFetching: false,
columnWidths: {},
setColumnWidths: jest.fn(),
};
});
const { getByText } = render(
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
<DashboardContext.Provider value={mockContextValue}>
<DashboardDescription
handle={{
active: false,
enter: (): Promise<void> => Promise.resolve(),
exit: (): Promise<void> => Promise.resolve(),
node: { current: null },
}}
/>
</DashboardContext.Provider>
<DashboardDescription
handle={{
active: false,
enter: (): Promise<void> => Promise.resolve(),
exit: (): Promise<void> => Promise.resolve(),
node: { current: null },
}}
/>
</MemoryRouter>,
);

View File

@@ -21,6 +21,7 @@ import { DeleteButton } from 'container/ListOfDashboard/TableComponents/DeleteBu
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
import { useGetPublicDashboardMeta } from 'hooks/dashboard/useGetPublicDashboardMeta';
import { useLockDashboard } from 'hooks/dashboard/useLockDashboard';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
@@ -39,8 +40,11 @@ import {
X,
} from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import {
selectIsDashboardLocked,
useDashboardStore,
} from 'providers/Dashboard/store/useDashboardStore';
import { sortLayout } from 'providers/Dashboard/util';
import { DashboardData } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update';
@@ -79,10 +83,11 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
setPanelMap,
layouts,
setLayouts,
isDashboardLocked,
setSelectedDashboard,
handleDashboardLockToggle,
} = useDashboard();
} = useDashboardStore();
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
const handleDashboardLockToggle = useLockDashboard();
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(

View File

@@ -30,7 +30,7 @@ import {
Pyramid,
X,
} from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { AppState } from 'store/reducers';
import {
IDashboardVariable,
@@ -239,7 +239,7 @@ function VariableItem({
const [selectedWidgets, setSelectedWidgets] = useState<string[]>([]);
const { selectedDashboard } = useDashboard();
const { selectedDashboard } = useDashboardStore();
const widgetsByDynamicVariableId = useWidgetsByDynamicVariableId();
useEffect(() => {

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { CustomMultiSelect } from 'components/NewSelect';
import { PANEL_GROUP_TYPES } from 'constants/queryBuilder';
import { generateGridTitle } from 'container/GridPanelSwitch/utils';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { WidgetRow, Widgets } from 'types/api/dashboard/getAll';
export function WidgetSelector({
@@ -12,7 +12,7 @@ export function WidgetSelector({
selectedWidgets: string[];
setSelectedWidgets: (widgets: string[]) => void;
}): JSX.Element {
const { selectedDashboard } = useDashboard();
const { selectedDashboard } = useDashboardStore();
// Get layout IDs for cross-referencing
const layoutIds = new Set(

View File

@@ -19,8 +19,8 @@ import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useNotifications } from 'hooks/useNotifications';
import { PenLine, Trash2 } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { TVariableMode } from './types';
@@ -87,7 +87,7 @@ function VariablesSettings({
const { t } = useTranslation(['dashboard']);
const { selectedDashboard, setSelectedDashboard } = useDashboard();
const { selectedDashboard, setSelectedDashboard } = useDashboardStore();
const { dashboardVariables } = useDashboardVariables();
const { notifications } = useNotifications();

View File

@@ -5,7 +5,7 @@ import AddTags from 'container/DashboardContainer/DashboardSettings/General/AddT
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { isEqual } from 'lodash-es';
import { Check, X } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { Button } from './styles';
import { Base64Icons } from './utils';
@@ -15,7 +15,7 @@ import './GeneralSettings.styles.scss';
const { Option } = Select;
function GeneralDashboardSettings(): JSX.Element {
const { selectedDashboard, setSelectedDashboard } = useDashboard();
const { selectedDashboard, setSelectedDashboard } = useDashboardStore();
const updateDashboardMutation = useUpdateDashboard();

View File

@@ -7,14 +7,14 @@ import {
unpublishedPublicDashboardMeta,
} from 'mocks-server/__mockdata__/publicDashboard';
import { rest, server } from 'mocks-server/server';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { USER_ROLES } from 'types/roles';
import PublicDashboardSetting from '../index';
// Mock dependencies
jest.mock('providers/Dashboard/Dashboard');
jest.mock('providers/Dashboard/store/useDashboardStore');
jest.mock('react-use', () => ({
...jest.requireActual('react-use'),
useCopyToClipboard: jest.fn(),
@@ -26,7 +26,7 @@ jest.mock('@signozhq/sonner', () => ({
},
}));
const mockUseDashboard = jest.mocked(useDashboard);
const mockUseDashboard = jest.mocked(useDashboardStore);
const mockUseCopyToClipboard = jest.mocked(useCopyToClipboard);
const mockToast = jest.mocked(toast);
@@ -67,10 +67,10 @@ beforeEach(() => {
// Mock window.open
window.open = jest.fn();
// Mock useDashboard
// Mock useDashboardStore
mockUseDashboard.mockReturnValue(({
selectedDashboard: mockSelectedDashboard,
} as unknown) as ReturnType<typeof useDashboard>);
} as unknown) as ReturnType<typeof useDashboardStore>);
// Mock useCopyToClipboard
mockUseCopyToClipboard.mockReturnValue(([

View File

@@ -11,7 +11,7 @@ import { useGetPublicDashboardMeta } from 'hooks/dashboard/useGetPublicDashboard
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { Copy, ExternalLink, Globe, Info, Loader2, Trash } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta';
import APIError from 'types/api/error';
import { USER_ROLES } from 'types/roles';
@@ -59,7 +59,7 @@ function PublicDashboardSetting(): JSX.Element {
const [defaultTimeRange, setDefaultTimeRange] = useState('30m');
const [, setCopyPublicDashboardURL] = useCopyToClipboard();
const { selectedDashboard } = useDashboard();
const { selectedDashboard } = useDashboardStore();
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();

View File

@@ -3,13 +3,14 @@ import { memo, useCallback, useEffect, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { Row } from 'antd';
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
import { updateLocalStorageDashboardVariable } from 'hooks/dashboard/useDashboardFromLocalStorage';
import {
useDashboardVariables,
useDashboardVariablesSelector,
} from 'hooks/dashboard/useDashboardVariables';
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import {
enqueueDescendantsOfVariable,
enqueueFetchOfAllVariables,
@@ -18,23 +19,23 @@ import {
import { AppState } from 'store/reducers';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { useShallow } from 'zustand/react/shallow';
import VariableItem from './VariableItem';
import './DashboardVariableSelection.styles.scss';
function DashboardVariableSelection(): JSX.Element | null {
const {
setSelectedDashboard,
updateLocalStorageDashboardVariables,
} = useDashboard();
const { dashboardId, setSelectedDashboard } = useDashboardStore(
useShallow((s) => ({
dashboardId: s.selectedDashboard?.id ?? '',
setSelectedDashboard: s.setSelectedDashboard,
})),
);
const { updateUrlVariable } = useVariablesFromUrl();
const { dashboardVariables } = useDashboardVariables();
const dashboardId = useDashboardVariablesSelector(
(state) => state.dashboardId,
);
const sortedVariablesArray = useDashboardVariablesSelector(
(state) => state.sortedVariablesArray,
);
@@ -82,7 +83,13 @@ function DashboardVariableSelection(): JSX.Element | null {
// This makes localStorage much lighter by avoiding storing all individual values
const variable = dashboardVariables[id] || dashboardVariables[name];
const isDynamic = variable.type === 'DYNAMIC';
updateLocalStorageDashboardVariables(name, value, allSelected, isDynamic);
updateLocalStorageDashboardVariable(
dashboardId,
name,
value,
allSelected,
isDynamic,
);
if (allSelected) {
updateUrlVariable(name || id, ALL_SELECTED_VALUE);
@@ -150,13 +157,7 @@ function DashboardVariableSelection(): JSX.Element | null {
// Safe to call synchronously now that the store already has the updated value.
enqueueDescendantsOfVariable(name);
},
[
dashboardId,
dashboardVariables,
updateLocalStorageDashboardVariables,
updateUrlVariable,
setSelectedDashboard,
],
[dashboardId, dashboardVariables, updateUrlVariable, setSelectedDashboard],
);
return (

View File

@@ -32,11 +32,22 @@ const mockVariableItemCallbacks: {
// Mock providers/Dashboard/Dashboard
const mockSetSelectedDashboard = jest.fn();
const mockUpdateLocalStorageDashboardVariables = jest.fn();
jest.mock('providers/Dashboard/Dashboard', () => ({
useDashboard: (): Record<string, unknown> => ({
setSelectedDashboard: mockSetSelectedDashboard,
updateLocalStorageDashboardVariables: mockUpdateLocalStorageDashboardVariables,
}),
interface MockDashboardStoreState {
selectedDashboard?: { id: string };
setSelectedDashboard: typeof mockSetSelectedDashboard;
updateLocalStorageDashboardVariables: typeof mockUpdateLocalStorageDashboardVariables;
}
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (
selector?: (s: Record<string, unknown>) => MockDashboardStoreState,
): MockDashboardStoreState => {
const state = {
selectedDashboard: { id: 'dash-1' },
setSelectedDashboard: mockSetSelectedDashboard,
updateLocalStorageDashboardVariables: mockUpdateLocalStorageDashboardVariables,
};
return selector ? selector(state) : state;
},
}));
// Mock hooks/dashboard/useVariablesFromUrl

View File

@@ -1,11 +1,13 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { useCallback } from 'react';
import { useAddDynamicVariableToPanels } from 'hooks/dashboard/useAddDynamicVariableToPanels';
import { updateLocalStorageDashboardVariable } from 'hooks/dashboard/useDashboardFromLocalStorage';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { v4 as uuidv4 } from 'uuid';
import { useShallow } from 'zustand/react/shallow';
import { convertVariablesToDbFormat } from './util';
@@ -37,11 +39,16 @@ interface UseDashboardVariableUpdateReturn {
export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn => {
const {
dashboardId,
selectedDashboard,
setSelectedDashboard,
updateLocalStorageDashboardVariables,
} = useDashboard();
} = useDashboardStore(
useShallow((s) => ({
dashboardId: s.selectedDashboard?.id ?? '',
selectedDashboard: s.selectedDashboard,
setSelectedDashboard: s.setSelectedDashboard,
})),
);
const addDynamicVariableToPanels = useAddDynamicVariableToPanels();
const updateMutation = useUpdateDashboard();
@@ -59,7 +66,13 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
// This makes localStorage much lighter and more efficient.
// currently all the variables are dynamic
const isDynamic = true;
updateLocalStorageDashboardVariables(name, value, allSelected, isDynamic);
updateLocalStorageDashboardVariable(
dashboardId,
name,
value,
allSelected,
isDynamic,
);
if (selectedDashboard) {
setSelectedDashboard((prev) => {
@@ -97,11 +110,7 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
}
}
},
[
selectedDashboard,
setSelectedDashboard,
updateLocalStorageDashboardVariables,
],
[dashboardId, selectedDashboard, setSelectedDashboard],
);
const updateVariables = useCallback(

View File

@@ -49,8 +49,8 @@ const mockDashboard = {
// Mock the dashboard provider with stable functions to prevent infinite loops
const mockSetSelectedDashboard = jest.fn();
const mockUpdateLocalStorageDashboardVariables = jest.fn();
jest.mock('providers/Dashboard/Dashboard', () => ({
useDashboard: (): any => ({
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): any => ({
selectedDashboard: mockDashboard,
setSelectedDashboard: mockSetSelectedDashboard,
updateLocalStorageDashboardVariables: mockUpdateLocalStorageDashboardVariables,

View File

@@ -56,8 +56,8 @@ const mockDashboard = {
},
};
// Mock dependencies
jest.mock('providers/Dashboard/Dashboard', () => ({
useDashboard: (): any => ({
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): any => ({
selectedDashboard: mockDashboard,
}),
}));
@@ -152,8 +152,8 @@ describe('Panel Management Tests', () => {
};
// Temporarily mock the dashboard
jest.doMock('providers/Dashboard/Dashboard', () => ({
useDashboard: (): any => ({
jest.doMock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): any => ({
selectedDashboard: modifiedDashboard,
}),
}));

View File

@@ -4,7 +4,7 @@ import ROUTES from 'constants/routes';
import { DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY } from 'hooks/dashboard/useDashboardsListQueryParams';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { LayoutGrid } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { DashboardData } from 'types/api/dashboard/getAll';
import { Base64Icons } from '../../DashboardSettings/General/utils';
@@ -13,7 +13,7 @@ import './DashboardBreadcrumbs.styles.scss';
function DashboardBreadcrumbs(): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const { selectedDashboard } = useDashboard();
const { selectedDashboard } = useDashboardStore();
const updatedAtRef = useRef(selectedDashboard?.updatedAt);
const selectedData = selectedDashboard

View File

@@ -6,7 +6,10 @@ import { useNotifications } from 'hooks/useNotifications';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { usePlotContext } from 'lib/uPlotV2/context/PlotContext';
import useLegendsSync from 'lib/uPlotV2/hooks/useLegendsSync';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
selectIsDashboardLocked,
useDashboardStore,
} from 'providers/Dashboard/store/useDashboardStore';
import { getChartManagerColumns } from './getChartMangerColumns';
import { ExtendedChartDataset, getDefaultTableDataSet } from './utils';
@@ -50,7 +53,7 @@ export default function ChartManager({
onToggleSeriesVisibility,
syncSeriesVisibilityToLocalStorage,
} = usePlotContext();
const { isDashboardLocked } = useDashboard();
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
const [tableDataSet, setTableDataSet] = useState<ExtendedChartDataset[]>(() =>
getDefaultTableDataSet(

View File

@@ -32,10 +32,18 @@ jest.mock('lib/uPlotV2/hooks/useLegendsSync', () => ({
}),
}));
jest.mock('providers/Dashboard/Dashboard', () => ({
useDashboard: (): { isDashboardLocked: boolean } => ({
isDashboardLocked: false,
}),
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (
selector?: (s: {
selectedDashboard: { locked: boolean } | undefined;
}) => { selectedDashboard: { locked: boolean } },
): { selectedDashboard: { locked: boolean } } => {
const mockState = { selectedDashboard: { locked: false } };
return selector ? selector(mockState) : mockState;
},
selectIsDashboardLocked: (s: {
selectedDashboard: { locked: boolean } | undefined;
}): boolean => s.selectedDashboard?.locked ?? false,
}));
jest.mock('hooks/useNotifications', () => ({

View File

@@ -8,8 +8,11 @@ import { VariablesSettingsTab } from 'container/DashboardContainer/DashboardDesc
import DashboardSettings from 'container/DashboardContainer/DashboardSettings';
import useComponentPermission from 'hooks/useComponentPermission';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import {
selectIsDashboardLocked,
useDashboardStore,
} from 'providers/Dashboard/store/useDashboardStore';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
@@ -20,7 +23,8 @@ export default function DashboardEmptyState(): JSX.Element {
(s) => s.setIsPanelTypeSelectionModalOpen,
);
const { selectedDashboard, isDashboardLocked } = useDashboard();
const { selectedDashboard } = useDashboardStore();
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(

View File

@@ -3,7 +3,10 @@ import { Button, Input } from 'antd';
import type { CheckboxChangeEvent } from 'antd/es/checkbox';
import { ResizeTable } from 'components/ResizeTable';
import { useNotifications } from 'hooks/useNotifications';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
selectIsDashboardLocked,
useDashboardStore,
} from 'providers/Dashboard/store/useDashboardStore';
import { getGraphManagerTableColumns } from './TableRender/GraphManagerColumns';
import { ExtendedChartDataset, GraphManagerProps } from './types';
@@ -34,7 +37,7 @@ function GraphManager({
}, [data, options]);
const { notifications } = useNotifications();
const { isDashboardLocked } = useDashboard();
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
const checkBoxOnChangeHandler = useCallback(
(e: CheckboxChangeEvent, index: number): void => {

View File

@@ -39,7 +39,10 @@ import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariab
import GetMinMax from 'lib/getMinMax';
import { isEmpty } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
selectIsDashboardLocked,
useDashboardStore,
} from 'providers/Dashboard/store/useDashboardStore';
import { AppState } from 'store/reducers';
import { Warning } from 'types/api';
import { GlobalReducer } from 'types/reducer/globalTime';
@@ -81,11 +84,8 @@ function FullView({
setCurrentGraphRef(fullViewRef);
}, [setCurrentGraphRef]);
const {
selectedDashboard,
isDashboardLocked,
setColumnWidths,
} = useDashboard();
const { selectedDashboard, setColumnWidths } = useDashboardStore();
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
const onColumnWidthsChange = useCallback(
(widths: Record<string, number>) => {

View File

@@ -161,8 +161,8 @@ const mockProps: WidgetGraphComponentProps = {
};
// Mock useDashabord hook
jest.mock('providers/Dashboard/Dashboard', () => ({
useDashboard: (): any => ({
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): any => ({
selectedDashboard: {
data: {
variables: [],

View File

@@ -28,7 +28,7 @@ import {
getCustomTimeRangeWindowSweepInMS,
getStartAndEndTimesInMilliseconds,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { Widgets } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update';
import { EQueryType } from 'types/common/dashboard';
@@ -106,7 +106,7 @@ function WidgetGraphComponent({
selectedDashboard,
setSelectedDashboard,
setColumnWidths,
} = useDashboard();
} = useDashboardStore();
const onColumnWidthsChange = useCallback(
(widths: Record<string, number>) => {

View File

@@ -1,6 +1,7 @@
import { memo, useEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import * as Sentry from '@sentry/react';
import logEvent from 'api/common/logEvent';
import { DEFAULT_ENTITY_VERSION, ENTITY_VERSION_V5 } from 'constants/app';
import { QueryParams } from 'constants/query';
@@ -17,7 +18,6 @@ import { getVariableReferencesInQuery } from 'lib/dashboardVariables/variableRef
import getTimeString from 'lib/getTimeString';
import { isEqual } from 'lodash-es';
import isEmpty from 'lodash-es/isEmpty';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import APIError from 'types/api/error';
@@ -68,7 +68,19 @@ function GridCardGraph({
const [isInternalServerError, setIsInternalServerError] = useState<boolean>(
false,
);
const { setDashboardQueryRangeCalled } = useDashboard();
const queryRangeCalledRef = useRef(false);
useEffect(() => {
const timeoutId = setTimeout(() => {
if (!queryRangeCalledRef.current) {
Sentry.captureEvent({
message: `Dashboard query range not called within expected timeframe for widget ${widget?.id}`,
level: 'warning',
});
}
}, 120000);
return (): void => clearTimeout(timeoutId);
}, [widget?.id]);
const {
minTime,
@@ -260,14 +272,14 @@ function GridCardGraph({
});
}
}
setDashboardQueryRangeCalled(true);
queryRangeCalledRef.current = true;
},
onSettled: (data) => {
dataAvailable?.(
isDataAvailableByPanelType(data?.payload?.data, widget?.panelTypes),
);
getGraphData?.(data?.payload?.data);
setDashboardQueryRangeCalled(true);
queryRangeCalledRef.current = true;
},
},
);

View File

@@ -1,10 +1,10 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FullScreen, FullScreenHandle } from 'react-full-screen';
import { ItemCallback, Layout } from 'react-grid-layout';
import { useIsFetching } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import * as Sentry from '@sentry/react';
import { Color } from '@signozhq/design-tokens';
import { Button, Form, Input, Modal, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -12,6 +12,7 @@ import cx from 'classnames';
import { ENTITY_VERSION_V5 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { themeColors } from 'constants/theme';
import { DEFAULT_ROW_NAME } from 'container/DashboardContainer/DashboardDescription/utils';
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
@@ -31,7 +32,10 @@ import {
X,
} from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
selectIsDashboardLocked,
useDashboardStore,
} from 'providers/Dashboard/store/useDashboardStore';
import { sortLayout } from 'providers/Dashboard/util';
import { UpdateTimeInterval } from 'store/actions';
import { Widgets } from 'types/api/dashboard/getAll';
@@ -61,6 +65,9 @@ interface GraphLayoutProps {
function GraphLayout(props: GraphLayoutProps): JSX.Element {
const { handle, enableDrillDown = false } = props;
const { safeNavigate } = useSafeNavigate();
const isDashboardFetching =
useIsFetching([REACT_QUERY_KEY.DASHBOARD_BY_ID]) > 0;
const {
selectedDashboard,
layouts,
@@ -68,12 +75,9 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
panelMap,
setPanelMap,
setSelectedDashboard,
isDashboardLocked,
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
isDashboardFetching,
columnWidths,
} = useDashboard();
} = useDashboardStore();
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
const { data } = selectedDashboard || {};
const { pathname } = useLocation();
const dispatch = useDispatch();
@@ -137,25 +141,6 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
setDashboardLayout(sortLayout(layouts));
}, [layouts]);
useEffect(() => {
setDashboardQueryRangeCalled(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const timeoutId = setTimeout(() => {
// Send Sentry event if query_range is not called within expected timeframe (2 mins) when there are widgets
if (!dashboardQueryRangeCalled && data?.widgets?.length) {
Sentry.captureEvent({
message: `Dashboard query range not called within expected timeframe even when there are ${data?.widgets?.length} widgets`,
level: 'warning',
});
}
}, 120000);
return (): void => clearTimeout(timeoutId);
}, [dashboardQueryRangeCalled, data?.widgets?.length]);
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data)) {

View File

@@ -4,9 +4,12 @@ import { Button, Popover } from 'antd';
import useComponentPermission from 'hooks/useComponentPermission';
import { EllipsisIcon, PenLine, Plus, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import { setSelectedRowWidgetId } from 'providers/Dashboard/helpers/selectedRowWidgetIdHelper';
import {
selectIsDashboardLocked,
useDashboardStore,
} from 'providers/Dashboard/store/useDashboardStore';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
@@ -39,7 +42,8 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
(s) => s.setIsPanelTypeSelectionModalOpen,
);
const { selectedDashboard, isDashboardLocked } = useDashboard();
const { selectedDashboard } = useDashboardStore();
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
const permissions: ComponentTypes[] = ['add_panel'];
const { user } = useAppContext();

View File

@@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { useNotifications } from 'hooks/useNotifications';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { Widgets } from 'types/api/dashboard/getAll';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
@@ -121,7 +121,7 @@ function useNavigateToExplorerPages(): (
) => Promise<{
[queryName: string]: { filters: TagFilterItem[]; dataSource?: string };
}> {
const { selectedDashboard } = useDashboard();
const { selectedDashboard } = useDashboardStore();
const { notifications } = useNotifications();
return useCallback(

View File

@@ -62,9 +62,6 @@ export const getVolumeQueryPayload = (
const k8sPVCNameKey = dotMetricsEnabled
? 'k8s.persistentvolumeclaim.name'
: 'k8s_persistentvolumeclaim_name';
const legendTemplate = dotMetricsEnabled
? '{{k8s.namespace.name}}-{{k8s.pod.name}}'
: '{{k8s_namespace_name}}-{{k8s_pod_name}}';
return [
{
@@ -136,7 +133,7 @@ export const getVolumeQueryPayload = (
functions: [],
groupBy: [],
having: [],
legend: legendTemplate,
legend: 'Available',
limit: null,
orderBy: [],
queryName: 'A',
@@ -228,7 +225,7 @@ export const getVolumeQueryPayload = (
functions: [],
groupBy: [],
having: [],
legend: legendTemplate,
legend: 'Capacity',
limit: null,
orderBy: [],
queryName: 'A',
@@ -319,7 +316,7 @@ export const getVolumeQueryPayload = (
},
groupBy: [],
having: [],
legend: legendTemplate,
legend: 'Inodes Used',
limit: null,
orderBy: [],
queryName: 'A',
@@ -411,7 +408,7 @@ export const getVolumeQueryPayload = (
},
groupBy: [],
having: [],
legend: legendTemplate,
legend: 'Total Inodes',
limit: null,
orderBy: [],
queryName: 'A',
@@ -503,7 +500,7 @@ export const getVolumeQueryPayload = (
},
groupBy: [],
having: [],
legend: legendTemplate,
legend: 'Inodes Free',
limit: null,
orderBy: [],
queryName: 'A',

View File

@@ -92,8 +92,8 @@ jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: (): boolean => false,
}));
jest.mock('providers/Dashboard/Dashboard', () => ({
useDashboard: (): { selectedDashboard: undefined } => ({
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): { selectedDashboard: undefined } => ({
selectedDashboard: undefined,
}),
}));

View File

@@ -6,11 +6,24 @@
// - Handling multiple rows correctly
// - Handling widgets with different heights
import { ReactNode } from 'react';
import { I18nextProvider } from 'react-i18next';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { screen } from '@testing-library/react';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { useDashboardBootstrap } from 'hooks/dashboard/useDashboardBootstrap';
function DashboardBootstrapWrapper({
dashboardId,
children,
}: {
dashboardId: string;
children: ReactNode;
}): JSX.Element {
useDashboardBootstrap(dashboardId);
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
}
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import i18n from 'ReactI18';
import {
@@ -309,7 +322,7 @@ describe('Stacking bar in new panel', () => {
const { container, getByText } = render(
<I18nextProvider i18n={i18n}>
<DashboardProvider dashboardId="">
<DashboardBootstrapWrapper dashboardId="">
<PreferenceContextProvider>
<NewWidget
dashboardId=""
@@ -317,7 +330,7 @@ describe('Stacking bar in new panel', () => {
selectedGraph={PANEL_TYPES.BAR}
/>
</PreferenceContextProvider>
</DashboardProvider>
</DashboardBootstrapWrapper>
</I18nextProvider>,
);
@@ -362,13 +375,13 @@ describe('when switching to BAR panel type', () => {
});
const { getByTestId, getByText, container } = render(
<DashboardProvider dashboardId="">
<DashboardBootstrapWrapper dashboardId="">
<NewWidget
dashboardId=""
selectedDashboard={undefined}
selectedGraph={PANEL_TYPES.BAR}
/>
</DashboardProvider>,
</DashboardBootstrapWrapper>,
);
expect(getByTestId('panel-change-select')).toHaveAttribute(

View File

@@ -2,16 +2,15 @@ import { Dispatch, SetStateAction } from 'react';
import { UseQueryResult } from 'react-query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { IDashboardContext } from 'providers/Dashboard/types';
import { SuccessResponse, Warning } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { timePreferance } from './RightContainer/timeItems';
export interface NewWidgetProps {
dashboardId: string;
selectedDashboard: IDashboardContext['selectedDashboard'];
selectedDashboard: Dashboard | undefined;
selectedGraph: PANEL_TYPES;
enableDrillDown?: boolean;
}
@@ -35,7 +34,7 @@ export interface WidgetGraphProps {
>
>;
enableDrillDown?: boolean;
selectedDashboard: IDashboardContext['selectedDashboard'];
selectedDashboard: Dashboard | undefined;
isNewPanel?: boolean;
}

View File

@@ -6122,5 +6122,95 @@
],
"id": "huggingface-observability",
"link": "/docs/huggingface-observability/"
},
{
"dataSource": "mistral-observability",
"label": "Mistral AI",
"imgUrl": "/Logos/mistral.svg",
"tags": [
"LLM Monitoring"
],
"module": "apm",
"relatedSearchKeywords": [
"llm",
"llm monitoring",
"mistral",
"mistral ai",
"monitoring",
"observability",
"otel mistral",
"traces",
"tracing"
],
"id": "mistral-observability",
"link": "/docs/mistral-observability/"
},
{
"dataSource": "openclaw-observability",
"label": "OpenClaw",
"imgUrl": "/Logos/openclaw.svg",
"tags": [
"LLM Monitoring"
],
"module": "apm",
"relatedSearchKeywords": [
"llm",
"llm monitoring",
"monitoring",
"observability",
"openclaw",
"otel openclaw",
"traces",
"tracing"
],
"id": "openclaw-observability",
"link": "/docs/openclaw-monitoring/"
},
{
"dataSource": "claude-agent-monitoring",
"label": "Claude Agent SDK",
"imgUrl": "/Logos/claude-code.svg",
"tags": [
"LLM Monitoring"
],
"module": "apm",
"relatedSearchKeywords": [
"anthropic",
"claude",
"claude agent",
"claude agent sdk",
"claude sdk",
"llm",
"llm monitoring",
"monitoring",
"observability",
"otel claude",
"traces",
"tracing"
],
"id": "claude-agent-monitoring",
"link": "/docs/claude-agent-monitoring/"
},
{
"dataSource": "render-metrics",
"label": "Render",
"imgUrl": "/Logos/render.svg",
"tags": [
"infrastructure monitoring",
"metrics"
],
"module": "metrics",
"relatedSearchKeywords": [
"infrastructure",
"metrics",
"monitoring",
"observability",
"paas",
"render",
"render metrics",
"render monitoring"
],
"id": "render-metrics",
"link": "/docs/metrics-management/render-metrics/"
}
]

View File

@@ -11,7 +11,7 @@ import useContextVariables from 'hooks/dashboard/useContextVariables';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import createQueryParams from 'lib/createQueryParams';
import ContextMenu from 'periscope/components/ContextMenu';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { ContextLinksData } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -66,7 +66,7 @@ const useBaseAggregateOptions = ({
getUpdatedQuery,
isLoading: isResolveQueryLoading,
} = useUpdatedQuery();
const { selectedDashboard } = useDashboard();
const { selectedDashboard } = useDashboardStore();
useEffect(() => {
if (!aggregateData) {

View File

@@ -12,8 +12,8 @@ jest.mock('react-router-dom', () => ({
}));
// Mock useDashabord hook
jest.mock('providers/Dashboard/Dashboard', () => ({
useDashboard: (): any => ({
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): any => ({
selectedDashboard: {
data: {
variables: [],

View File

@@ -1,5 +1,5 @@
import { renderHook } from '@testing-library/react';
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
import { getLocalStorageDashboardVariables } from 'hooks/dashboard/useDashboardFromLocalStorage';
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
@@ -7,8 +7,8 @@ import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
jest.mock('hooks/dashboard/useDashboardFromLocalStorage');
jest.mock('hooks/dashboard/useVariablesFromUrl');
const mockUseDashboardVariablesFromLocalStorage = useDashboardVariablesFromLocalStorage as jest.MockedFunction<
typeof useDashboardVariablesFromLocalStorage
const mockGetLocalStorageDashboardVariables = getLocalStorageDashboardVariables as jest.MockedFunction<
typeof getLocalStorageDashboardVariables
>;
const mockUseVariablesFromUrl = useVariablesFromUrl as jest.MockedFunction<
typeof useVariablesFromUrl
@@ -46,10 +46,7 @@ const setupHook = (
currentDashboard: Record<string, any> = {},
urlVariables: Record<string, any> = {},
): ReturnType<typeof useTransformDashboardVariables> => {
mockUseDashboardVariablesFromLocalStorage.mockReturnValue({
currentDashboard,
updateLocalStorageDashboardVariables: jest.fn(),
});
mockGetLocalStorageDashboardVariables.mockReturnValue(currentDashboard as any);
mockUseVariablesFromUrl.mockReturnValue({
getUrlVariables: () => urlVariables,
setUrlVariables: jest.fn(),

View File

@@ -0,0 +1,164 @@
import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { Modal } from 'antd';
import dayjs from 'dayjs';
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
import useTabVisibility from 'hooks/useTabFocus';
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
import { defaultTo } from 'lodash-es';
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { sortLayout } from 'providers/Dashboard/util';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
import { Dashboard } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { useDashboardQuery } from './useDashboardQuery';
import { useDashboardVariablesSync } from './useDashboardVariablesSync';
interface UseDashboardBootstrapOptions {
/** Pass `onModal.confirm` from `Modal.useModal()` to get theme-aware modals. Falls back to static `Modal.confirm`. */
confirm?: typeof Modal.confirm;
}
export interface UseDashboardBootstrapReturn {
isLoading: boolean;
isError: boolean;
isFetching: boolean;
error: unknown;
}
export function useDashboardBootstrap(
dashboardId: string,
options: UseDashboardBootstrapOptions = {},
): UseDashboardBootstrapReturn {
const confirm = options.confirm ?? Modal.confirm;
const { t } = useTranslation(['dashboard']);
const dispatch = useDispatch<Dispatch<AppActions>>();
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const {
setSelectedDashboard,
setLayouts,
setPanelMap,
resetDashboardStore,
} = useDashboardStore();
const dashboardRef = useRef<Dashboard>();
const modalRef = useRef<ReturnType<typeof Modal.confirm>>();
const isVisible = useTabVisibility();
const {
getUrlVariables,
updateUrlVariable,
transformDashboardVariables,
} = useTransformDashboardVariables(dashboardId);
// Keep the external variables store in sync with selectedDashboard
useDashboardVariablesSync(dashboardId);
const dashboardQuery = useDashboardQuery(dashboardId);
// Handle new dashboard data: initialize on first load, detect changes on subsequent fetches.
// React Query's structural sharing means this effect only fires when data actually changes.
useEffect(() => {
if (!dashboardQuery.data?.data) {
return;
}
const updatedDashboardData = transformDashboardVariables(
dashboardQuery.data.data,
);
const updatedDate = dayjs(updatedDashboardData?.updatedAt);
// First load: initialize store and URL variables, then return
if (!dashboardRef.current) {
const variables = updatedDashboardData?.data?.variables;
if (variables) {
initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable);
}
setSelectedDashboard(updatedDashboardData);
dashboardRef.current = updatedDashboardData;
setLayouts(sortLayout(getUpdatedLayout(updatedDashboardData?.data.layout)));
setPanelMap(defaultTo(updatedDashboardData?.data?.panelMap, {}));
return;
}
// Subsequent fetches: skip if updatedAt hasn't advanced
if (!updatedDate.isAfter(dayjs(dashboardRef.current.updatedAt))) {
return;
}
// Data has changed: prompt user if tab is visible
if (isVisible && dashboardRef.current.id === updatedDashboardData?.id) {
const modal = confirm({
centered: true,
title: t('dashboard_has_been_updated'),
content: t('do_you_want_to_refresh_the_dashboard'),
onOk() {
setSelectedDashboard(updatedDashboardData);
const { maxTime, minTime } = getMinMaxForSelectedTime(
globalTime.selectedTime,
globalTime.minTime,
globalTime.maxTime,
);
dispatch({
type: UPDATE_TIME_INTERVAL,
payload: { maxTime, minTime, selectedTime: globalTime.selectedTime },
});
dashboardRef.current = updatedDashboardData;
setLayouts(
sortLayout(getUpdatedLayout(updatedDashboardData?.data.layout)),
);
setPanelMap(defaultTo(updatedDashboardData?.data.panelMap, {}));
},
});
modalRef.current = modal;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dashboardQuery.data]);
// Refetch when tab becomes visible (after initial load)
useEffect(() => {
if (isVisible && dashboardRef.current && !!dashboardId) {
dashboardQuery.refetch();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isVisible]);
// Dismiss stale modal when tab is hidden
useEffect(() => {
if (!isVisible && modalRef.current) {
modalRef.current.destroy();
}
}, [isVisible]);
// Reset store on unmount so stale state doesn't bleed across dashboards
useEffect(
() => (): void => {
resetDashboardStore();
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
return {
isLoading: dashboardQuery.isLoading,
isError: dashboardQuery.isError,
isFetching: dashboardQuery.isFetching,
error: dashboardQuery.error,
};
}

View File

@@ -0,0 +1,68 @@
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { LOCALSTORAGE } from 'constants/localStorage';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
export interface LocalStoreDashboardVariables {
[id: string]: {
selectedValue: IDashboardVariable['selectedValue'];
allSelected: boolean;
};
}
interface DashboardLocalStorageVariables {
[id: string]: LocalStoreDashboardVariables;
}
function readAll(): DashboardLocalStorageVariables {
const raw = getLocalStorageKey(LOCALSTORAGE.DASHBOARD_VARIABLES);
if (!raw) {
return {};
}
try {
return JSON.parse(raw);
} catch {
console.error('Failed to parse dashboard variables from local storage');
return {};
}
}
function writeAll(data: DashboardLocalStorageVariables): void {
try {
setLocalStorageKey(LOCALSTORAGE.DASHBOARD_VARIABLES, JSON.stringify(data));
} catch {
console.error('Failed to set dashboard variables in local storage');
}
}
/** Read the saved variable selections for a dashboard from localStorage. */
export function getLocalStorageDashboardVariables(
dashboardId: string,
): LocalStoreDashboardVariables {
return readAll()[dashboardId] ?? {};
}
/**
* Write one variable's selection for a dashboard to localStorage.
* All call sites write to the same store with no React state coordination.
*/
export function updateLocalStorageDashboardVariable(
dashboardId: string,
id: string,
selectedValue: IDashboardVariable['selectedValue'],
allSelected: boolean,
isDynamic?: boolean,
): void {
const all = readAll();
all[dashboardId] = {
...(all[dashboardId] ?? {}),
[id]:
isDynamic && allSelected
? {
selectedValue: (undefined as unknown) as IDashboardVariable['selectedValue'],
allSelected: true,
}
: { selectedValue, allSelected },
};
writeAll(all);
}

View File

@@ -1,110 +0,0 @@
import { useEffect, useState } from 'react';
import getLocalStorageKey from 'api/browser/localstorage/get';
import setLocalStorageKey from 'api/browser/localstorage/set';
import { LOCALSTORAGE } from 'constants/localStorage';
import { defaultTo } from 'lodash-es';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
interface LocalStoreDashboardVariables {
[id: string]: {
selectedValue: IDashboardVariable['selectedValue'];
allSelected: boolean;
};
}
interface DashboardLocalStorageVariables {
[id: string]: LocalStoreDashboardVariables;
}
export interface UseDashboardVariablesFromLocalStorageReturn {
currentDashboard: LocalStoreDashboardVariables;
updateLocalStorageDashboardVariables: (
id: string,
selectedValue: IDashboardVariable['selectedValue'],
allSelected: boolean,
isDynamic?: boolean,
) => void;
}
export const useDashboardVariablesFromLocalStorage = (
dashboardId: string,
): UseDashboardVariablesFromLocalStorageReturn => {
const [
allDashboards,
setAllDashboards,
] = useState<DashboardLocalStorageVariables>({});
const [
currentDashboard,
setCurrentDashboard,
] = useState<LocalStoreDashboardVariables>({});
useEffect(() => {
const localStoreDashboardVariablesString = getLocalStorageKey(
LOCALSTORAGE.DASHBOARD_VARIABLES,
);
let localStoreDashboardVariables: DashboardLocalStorageVariables = {};
if (localStoreDashboardVariablesString === null) {
try {
const serialzedData = JSON.stringify({
[dashboardId]: {},
});
setLocalStorageKey(LOCALSTORAGE.DASHBOARD_VARIABLES, serialzedData);
} catch {
console.error('Failed to seralise the data');
}
} else {
try {
localStoreDashboardVariables = JSON.parse(
localStoreDashboardVariablesString,
);
} catch {
console.error('Failed to parse dashboards from local storage');
localStoreDashboardVariables = {};
} finally {
setAllDashboards(localStoreDashboardVariables);
}
}
setCurrentDashboard(defaultTo(localStoreDashboardVariables[dashboardId], {}));
}, [dashboardId]);
useEffect(() => {
try {
const serializedData = JSON.stringify(allDashboards);
setLocalStorageKey(LOCALSTORAGE.DASHBOARD_VARIABLES, serializedData);
} catch {
console.error('Failed to set dashboards in local storage');
}
}, [allDashboards]);
useEffect(() => {
setAllDashboards((prev) => ({
...prev,
[dashboardId]: { ...currentDashboard },
}));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentDashboard]);
const updateLocalStorageDashboardVariables = (
id: string,
selectedValue: IDashboardVariable['selectedValue'],
allSelected: boolean,
isDynamic?: boolean,
): void => {
setCurrentDashboard((prev) => ({
...prev,
[id]:
isDynamic && allSelected
? {
selectedValue: (undefined as unknown) as IDashboardVariable['selectedValue'],
allSelected: true,
}
: { selectedValue, allSelected },
}));
};
return {
currentDashboard,
updateLocalStorageDashboardVariables,
};
};

View File

@@ -0,0 +1,49 @@
import { useQuery, UseQueryResult } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import getDashboard from 'api/v1/dashboards/id/get';
import {
DASHBOARD_CACHE_TIME,
DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
} from 'constants/queryCacheTime';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { AppState } from 'store/reducers';
import { SuccessResponseV2 } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { GlobalReducer } from 'types/reducer/globalTime';
/**
* Fetches a dashboard by ID. Handles auth gating, cache time based on
* auto-refresh setting, and surfaces API errors via the error modal.
*/
export function useDashboardQuery(
dashboardId: string,
): UseQueryResult<SuccessResponseV2<Dashboard>> {
const { isLoggedIn } = useAppContext();
const { showErrorModal } = useErrorModal();
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
return useQuery(
[
REACT_QUERY_KEY.DASHBOARD_BY_ID,
dashboardId,
globalTime.isAutoRefreshDisabled,
],
{
enabled: !!dashboardId && isLoggedIn,
queryFn: () => getDashboard({ id: dashboardId }),
refetchOnWindowFocus: false,
cacheTime: globalTime.isAutoRefreshDisabled
? DASHBOARD_CACHE_TIME
: DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
onError: (error) => {
showErrorModal(error as APIError);
},
},
);
}

View File

@@ -0,0 +1,33 @@
import { useEffect } from 'react';
import isEqual from 'lodash-es/isEqual';
import {
setDashboardVariablesStore,
updateDashboardVariablesStore,
} from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import {
DashboardStore,
useDashboardStore,
} from 'providers/Dashboard/store/useDashboardStore';
import { useDashboardVariablesSelector } from './useDashboardVariables';
/**
* Keeps the external variables store in sync with the zustand dashboard store.
* When selectedDashboard changes, propagates variable updates to the variables store.
*/
export function useDashboardVariablesSync(dashboardId: string): void {
const dashboardVariables = useDashboardVariablesSelector((s) => s.variables);
const savedDashboardId = useDashboardVariablesSelector((s) => s.dashboardId);
const selectedDashboard = useDashboardStore(
(s: DashboardStore) => s.selectedDashboard,
);
useEffect(() => {
const updatedVariables = selectedDashboard?.data.variables || {};
if (savedDashboardId !== dashboardId) {
setDashboardVariablesStore({ dashboardId, variables: updatedVariables });
} else if (!isEqual(dashboardVariables, updatedVariables)) {
updateDashboardVariablesStore({ dashboardId, variables: updatedVariables });
}
}, [selectedDashboard]); // eslint-disable-line react-hooks/exhaustive-deps
}

View File

@@ -0,0 +1,42 @@
import { useMutation } from 'react-query';
import locked from 'api/v1/dashboards/id/lock';
import {
getSelectedDashboard,
useDashboardStore,
} from 'providers/Dashboard/store/useDashboardStore';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
/**
* Hook for toggling dashboard locked state.
* Calls the lock API and syncs the result into the Zustand store.
*/
export function useLockDashboard(): (value: boolean) => Promise<void> {
const { showErrorModal } = useErrorModal();
const { setSelectedDashboard } = useDashboardStore();
const { mutate: lockDashboard } = useMutation(locked, {
onSuccess: (_, props) => {
setSelectedDashboard((prev) =>
prev ? { ...prev, locked: props.lock } : prev,
);
},
onError: (error) => {
showErrorModal(error as APIError);
},
});
return async (value: boolean): Promise<void> => {
const selectedDashboard = getSelectedDashboard();
if (selectedDashboard) {
try {
await lockDashboard({
id: selectedDashboard.id,
lock: value,
});
} catch (error) {
showErrorModal(error as APIError);
}
}
};
}

View File

@@ -1,8 +1,5 @@
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
import {
useDashboardVariablesFromLocalStorage,
UseDashboardVariablesFromLocalStorageReturn,
} from 'hooks/dashboard/useDashboardFromLocalStorage';
import { getLocalStorageDashboardVariables } from 'hooks/dashboard/useDashboardFromLocalStorage';
import useVariablesFromUrl, {
UseVariablesFromUrlReturn,
} from 'hooks/dashboard/useVariablesFromUrl';
@@ -13,14 +10,10 @@ import { v4 as generateUUID } from 'uuid';
export function useTransformDashboardVariables(
dashboardId: string,
): Pick<UseVariablesFromUrlReturn, 'getUrlVariables' | 'updateUrlVariable'> &
UseDashboardVariablesFromLocalStorageReturn & {
transformDashboardVariables: (data: Dashboard) => Dashboard;
} {
const {
currentDashboard,
updateLocalStorageDashboardVariables,
} = useDashboardVariablesFromLocalStorage(dashboardId);
): Pick<UseVariablesFromUrlReturn, 'getUrlVariables' | 'updateUrlVariable'> & {
transformDashboardVariables: (data: Dashboard) => Dashboard;
currentDashboard: ReturnType<typeof getLocalStorageDashboardVariables>;
} {
const { getUrlVariables, updateUrlVariable } = useVariablesFromUrl();
const mergeDBWithLocalStorage = (
@@ -80,7 +73,7 @@ export function useTransformDashboardVariables(
if (data && data.data && data.data.variables) {
const clonedDashboardData = mergeDBWithLocalStorage(
JSON.parse(JSON.stringify(data)),
currentDashboard,
getLocalStorageDashboardVariables(dashboardId),
);
const { variables } = clonedDashboardData.data;
const existingOrders: Set<number> = new Set();
@@ -122,7 +115,6 @@ export function useTransformDashboardVariables(
transformDashboardVariables,
getUrlVariables,
updateUrlVariable,
currentDashboard,
updateLocalStorageDashboardVariables,
currentDashboard: getLocalStorageDashboardVariables(dashboardId),
};
}

View File

@@ -1,7 +1,5 @@
import { useMutation, UseMutationResult } from 'react-query';
import update from 'api/v1/dashboards/id/update';
import dayjs from 'dayjs';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { SuccessResponseV2 } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
@@ -9,14 +7,8 @@ import { Props } from 'types/api/dashboard/update';
import APIError from 'types/api/error';
export const useUpdateDashboard = (): UseUpdateDashboard => {
const { updatedTimeRef } = useDashboard();
const { showErrorModal } = useErrorModal();
return useMutation(update, {
onSuccess: (data) => {
if (data.data) {
updatedTimeRef.current = dayjs(data.data.updatedAt);
}
},
onError: (error) => {
showErrorModal(error);
},

View File

@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { PANEL_GROUP_TYPES } from 'constants/queryBuilder';
import { createDynamicVariableToWidgetsMap } from 'hooks/dashboard/utils';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { Widgets } from 'types/api/dashboard/getAll';
import { useDashboardVariablesByType } from './useDashboardVariablesByType';
@@ -12,7 +12,7 @@ import { useDashboardVariablesByType } from './useDashboardVariablesByType';
*/
export function useWidgetsByDynamicVariableId(): Record<string, string[]> {
const dynamicVariables = useDashboardVariablesByType('DYNAMIC', 'values');
const { selectedDashboard } = useDashboard();
const { selectedDashboard } = useDashboardStore();
return useMemo(() => {
const widgets =

View File

@@ -17,7 +17,7 @@ import { useNotifications } from 'hooks/useNotifications';
import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariables';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { isEmpty } from 'lodash-es';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
@@ -33,7 +33,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
const { notifications } = useNotifications();
const { selectedDashboard } = useDashboard();
const { selectedDashboard } = useDashboardStore();
const { dashboardVariables } = useDashboardVariables();
const dashboardDynamicVariables = useDashboardVariablesByType(

View File

@@ -1,40 +0,0 @@
import { useEffect } from 'react';
import { Typography } from 'antd';
import { AxiosError } from 'axios';
import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner';
import DashboardContainer from 'container/DashboardContainer';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { ErrorType } from 'types/common';
function DashboardPage(): JSX.Element {
const { dashboardResponse } = useDashboard();
const { isFetching, isError, isLoading } = dashboardResponse;
const errorMessage = isError
? (dashboardResponse?.error as AxiosError<{ errorType: string }>)?.response
?.data?.errorType
: 'Something went wrong';
useEffect(() => {
const dashboardTitle = dashboardResponse.data?.data.data.title;
document.title = dashboardTitle || document.title;
}, [dashboardResponse.data?.data.data.title, isFetching]);
if (isError && !isFetching && errorMessage === ErrorType.NotFound) {
return <NotFound />;
}
if (isError && errorMessage) {
return <Typography>{errorMessage}</Typography>;
}
if (isLoading) {
return <Spinner tip="Loading.." />;
}
return <DashboardContainer />;
}
export default DashboardPage;

View File

@@ -1,16 +1,56 @@
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { Modal, Typography } from 'antd';
import { AxiosError } from 'axios';
import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner';
import DashboardContainer from 'container/DashboardContainer';
import { useDashboardBootstrap } from 'hooks/dashboard/useDashboardBootstrap';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { ErrorType } from 'types/common';
import DashboardPage from './DashboardPage';
function DashboardPageWithProvider(): JSX.Element {
function DashboardPage(): JSX.Element {
const { dashboardId } = useParams<{ dashboardId: string }>();
const [onModal, Content] = Modal.useModal();
const {
isLoading,
isError,
isFetching,
error,
} = useDashboardBootstrap(dashboardId, { confirm: onModal.confirm });
const dashboardTitle = useDashboardStore(
(s) => s.selectedDashboard?.data.title,
);
useEffect(() => {
document.title = dashboardTitle || document.title;
}, [dashboardTitle]);
const errorMessage = isError
? (error as AxiosError<{ errorType: string }>)?.response?.data?.errorType
: 'Something went wrong';
if (isError && !isFetching && errorMessage === ErrorType.NotFound) {
return <NotFound />;
}
if (isError && errorMessage) {
return <Typography>{errorMessage}</Typography>;
}
if (isLoading) {
return <Spinner tip="Loading.." />;
}
return (
<DashboardProvider dashboardId={dashboardId}>
<DashboardPage />
</DashboardProvider>
<>
{Content}
<DashboardContainer />
</>
);
}
export default DashboardPageWithProvider;
export default DashboardPage;

View File

@@ -1,365 +0,0 @@
import {
// eslint-disable-next-line no-restricted-imports
createContext,
PropsWithChildren,
// eslint-disable-next-line no-restricted-imports
useContext,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { Layout } from 'react-grid-layout';
import { useTranslation } from 'react-i18next';
import { useMutation, useQuery, UseQueryResult } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { Modal } from 'antd';
import getDashboard from 'api/v1/dashboards/id/get';
import locked from 'api/v1/dashboards/id/lock';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import dayjs, { Dayjs } from 'dayjs';
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
import useTabVisibility from 'hooks/useTabFocus';
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
import { defaultTo } from 'lodash-es';
import isEqual from 'lodash-es/isEqual';
import isUndefined from 'lodash-es/isUndefined';
import omitBy from 'lodash-es/omitBy';
import { useAppContext } from 'providers/App/App';
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
import { useErrorModal } from 'providers/ErrorModalProvider';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
import { SuccessResponseV2 } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { GlobalReducer } from 'types/reducer/globalTime';
import {
DASHBOARD_CACHE_TIME,
DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
} from '../../constants/queryCacheTime';
import { useDashboardVariablesSelector } from '../../hooks/dashboard/useDashboardVariables';
import {
setDashboardVariablesStore,
updateDashboardVariablesStore,
} from './store/dashboardVariables/dashboardVariablesStore';
import { IDashboardContext, WidgetColumnWidths } from './types';
import { sortLayout } from './util';
export const DashboardContext = createContext<IDashboardContext>({
isDashboardLocked: false,
handleDashboardLockToggle: () => {},
dashboardResponse: {} as UseQueryResult<
SuccessResponseV2<Dashboard>,
APIError
>,
selectedDashboard: {} as Dashboard,
layouts: [],
panelMap: {},
setPanelMap: () => {},
setLayouts: () => {},
setSelectedDashboard: () => {},
updatedTimeRef: {} as React.MutableRefObject<Dayjs | null>,
updateLocalStorageDashboardVariables: () => {},
dashboardQueryRangeCalled: false,
setDashboardQueryRangeCalled: () => {},
isDashboardFetching: false,
columnWidths: {},
setColumnWidths: () => {},
});
// eslint-disable-next-line sonarjs/cognitive-complexity
export function DashboardProvider({
children,
dashboardId,
}: PropsWithChildren<{ dashboardId: string }>): JSX.Element {
const [isDashboardLocked, setIsDashboardLocked] = useState<boolean>(false);
const [
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
] = useState<boolean>(false);
const { showErrorModal } = useErrorModal();
const dispatch = useDispatch<Dispatch<AppActions>>();
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [onModal, Content] = Modal.useModal();
const [layouts, setLayouts] = useState<Layout[]>([]);
const [panelMap, setPanelMap] = useState<
Record<string, { widgets: Layout[]; collapsed: boolean }>
>({});
const { isLoggedIn } = useAppContext();
const [selectedDashboard, setSelectedDashboard] = useState<Dashboard>();
const dashboardVariables = useDashboardVariablesSelector((s) => s.variables);
const savedDashboardId = useDashboardVariablesSelector((s) => s.dashboardId);
useEffect(() => {
const existingVariables = dashboardVariables;
const updatedVariables = selectedDashboard?.data.variables || {};
if (savedDashboardId !== dashboardId) {
setDashboardVariablesStore({
dashboardId,
variables: updatedVariables,
});
} else if (!isEqual(existingVariables, updatedVariables)) {
updateDashboardVariablesStore({
dashboardId,
variables: updatedVariables,
});
}
}, [selectedDashboard]);
const {
currentDashboard,
updateLocalStorageDashboardVariables,
getUrlVariables,
updateUrlVariable,
transformDashboardVariables,
} = useTransformDashboardVariables(dashboardId);
const updatedTimeRef = useRef<Dayjs | null>(null); // Using ref to store the updated time
const modalRef = useRef<any>(null);
const isVisible = useTabVisibility();
const { t } = useTranslation(['dashboard']);
const dashboardRef = useRef<Dashboard>();
const [isDashboardFetching, setIsDashboardFetching] = useState<boolean>(false);
const dashboardResponse = useQuery(
[
REACT_QUERY_KEY.DASHBOARD_BY_ID,
dashboardId,
globalTime.isAutoRefreshDisabled,
],
{
enabled: !!dashboardId && isLoggedIn,
queryFn: async () => {
setIsDashboardFetching(true);
try {
return await getDashboard({
id: dashboardId,
});
} catch (error) {
showErrorModal(error as APIError);
return;
} finally {
setIsDashboardFetching(false);
}
},
refetchOnWindowFocus: false,
cacheTime: globalTime.isAutoRefreshDisabled
? DASHBOARD_CACHE_TIME
: DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
onError: (error) => {
showErrorModal(error as APIError);
},
onSuccess: (data: SuccessResponseV2<Dashboard>) => {
const updatedDashboardData = transformDashboardVariables(data?.data);
// initialize URL variables after dashboard state is set to avoid race conditions
const variables = updatedDashboardData?.data?.variables;
if (variables) {
initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable);
}
const updatedDate = dayjs(updatedDashboardData?.updatedAt);
setIsDashboardLocked(updatedDashboardData?.locked || false);
// on first render
if (updatedTimeRef.current === null) {
setSelectedDashboard(updatedDashboardData);
updatedTimeRef.current = updatedDate;
dashboardRef.current = updatedDashboardData;
setLayouts(
sortLayout(getUpdatedLayout(updatedDashboardData?.data.layout)),
);
setPanelMap(defaultTo(updatedDashboardData?.data?.panelMap, {}));
}
if (
updatedTimeRef.current !== null &&
updatedDate.isAfter(updatedTimeRef.current) &&
isVisible &&
dashboardRef.current?.id === updatedDashboardData?.id
) {
// show modal when state is out of sync
const modal = onModal.confirm({
centered: true,
title: t('dashboard_has_been_updated'),
content: t('do_you_want_to_refresh_the_dashboard'),
onOk() {
setSelectedDashboard(updatedDashboardData);
const { maxTime, minTime } = getMinMaxForSelectedTime(
globalTime.selectedTime,
globalTime.minTime,
globalTime.maxTime,
);
dispatch({
type: UPDATE_TIME_INTERVAL,
payload: {
maxTime,
minTime,
selectedTime: globalTime.selectedTime,
},
});
dashboardRef.current = updatedDashboardData;
updatedTimeRef.current = dayjs(updatedDashboardData?.updatedAt);
setLayouts(
sortLayout(getUpdatedLayout(updatedDashboardData?.data.layout)),
);
setPanelMap(defaultTo(updatedDashboardData?.data.panelMap, {}));
},
});
modalRef.current = modal;
} else {
// normal flow
updatedTimeRef.current = dayjs(updatedDashboardData?.updatedAt);
dashboardRef.current = updatedDashboardData;
if (!isEqual(selectedDashboard, updatedDashboardData)) {
setSelectedDashboard(updatedDashboardData);
}
if (
!isEqual(
[omitBy(layouts, (value): boolean => isUndefined(value))[0]],
updatedDashboardData?.data.layout,
)
) {
setLayouts(
sortLayout(getUpdatedLayout(updatedDashboardData?.data.layout)),
);
setPanelMap(defaultTo(updatedDashboardData?.data.panelMap, {}));
}
}
},
},
);
useEffect(() => {
// make the call on tab visibility only if the user is on dashboard / widget page
if (isVisible && updatedTimeRef.current && !!dashboardId) {
dashboardResponse.refetch();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isVisible]);
useEffect(() => {
if (!isVisible && modalRef.current) {
modalRef.current.destroy();
}
}, [isVisible]);
const { mutate: lockDashboard } = useMutation(locked, {
onSuccess: (_, props) => {
setIsDashboardLocked(props.lock);
},
onError: (error) => {
showErrorModal(error as APIError);
},
});
const handleDashboardLockToggle = async (value: boolean): Promise<void> => {
if (selectedDashboard) {
try {
await lockDashboard({
id: selectedDashboard.id,
lock: value,
});
} catch (error) {
showErrorModal(error as APIError);
}
}
};
const [columnWidths, setColumnWidths] = useState<WidgetColumnWidths>({});
const value: IDashboardContext = useMemo(
() => ({
isDashboardLocked,
handleDashboardLockToggle,
dashboardResponse,
selectedDashboard,
dashboardId,
layouts,
panelMap,
setLayouts,
setPanelMap,
setSelectedDashboard,
updatedTimeRef,
updateLocalStorageDashboardVariables,
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
isDashboardFetching,
columnWidths,
setColumnWidths,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[
isDashboardLocked,
dashboardResponse,
selectedDashboard,
dashboardId,
layouts,
panelMap,
updateLocalStorageDashboardVariables,
currentDashboard,
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
isDashboardFetching,
columnWidths,
setColumnWidths,
],
);
return (
<DashboardContext.Provider value={value}>
{Content}
{children}
</DashboardContext.Provider>
);
}
export const useDashboard = (): IDashboardContext => {
const context = useContext(DashboardContext);
if (!context) {
throw new Error('Should be used inside the context');
}
return context;
};

View File

@@ -1,3 +1,4 @@
import { ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
@@ -6,7 +7,20 @@ import { render, RenderResult, screen, waitFor } from '@testing-library/react';
import getDashboard from 'api/v1/dashboards/id/get';
import { DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED } from 'constants/queryCacheTime';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { DashboardProvider, useDashboard } from 'providers/Dashboard/Dashboard';
import { useDashboardBootstrap } from 'hooks/dashboard/useDashboardBootstrap';
function DashboardBootstrapWrapper({
dashboardId,
children,
}: {
dashboardId: string;
children: ReactNode;
}): JSX.Element {
useDashboardBootstrap(dashboardId);
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
}
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { useDashboardVariables } from '../../../hooks/dashboard/useDashboardVariables';
@@ -55,17 +69,12 @@ jest.mock('react-redux', () => ({
jest.mock('uuid', () => ({ v4: jest.fn(() => 'mock-uuid') }));
function TestComponent(): JSX.Element {
const { dashboardResponse, selectedDashboard } = useDashboard();
const { selectedDashboard } = useDashboardStore();
const { dashboardVariables } = useDashboardVariables();
return (
<div>
<div data-testid="dashboard-id">{selectedDashboard?.id}</div>
<div data-testid="query-status">{dashboardResponse.status}</div>
<div data-testid="is-loading">{dashboardResponse.isLoading.toString()}</div>
<div data-testid="is-fetching">
{dashboardResponse.isFetching.toString()}
</div>
<div data-testid="dashboard-variables">
{dashboardVariables ? JSON.stringify(dashboardVariables) : 'null'}
</div>
@@ -89,7 +98,7 @@ function createTestQueryClient(): QueryClient {
}
// Helper to render with dashboard provider
function renderWithDashboardProvider(
function renderWithDashboardBootstrap(
dashboardId = 'test-dashboard-id',
): RenderResult {
const queryClient = createTestQueryClient();
@@ -98,9 +107,9 @@ function renderWithDashboardProvider(
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[initialRoute]}>
<DashboardProvider dashboardId={dashboardId}>
<DashboardBootstrapWrapper dashboardId={dashboardId}>
<TestComponent />
</DashboardProvider>
</DashboardBootstrapWrapper>
</MemoryRouter>
</QueryClientProvider>,
);
@@ -172,7 +181,7 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
describe('Query Key Behavior', () => {
it('should include route params in query key when on dashboard page', async () => {
const dashboardId = 'test-dashboard-id';
renderWithDashboardProvider(dashboardId);
renderWithDashboardBootstrap(dashboardId);
await waitFor(() => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: dashboardId });
@@ -187,7 +196,7 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
const newDashboardId = 'new-dashboard-id';
// First render with initial dashboard ID
const { rerender } = renderWithDashboardProvider(initialDashboardId);
const { rerender } = renderWithDashboardBootstrap(initialDashboardId);
await waitFor(() => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: initialDashboardId });
@@ -197,9 +206,9 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
rerender(
<QueryClientProvider client={createTestQueryClient()}>
<MemoryRouter initialEntries={[`/dashboard/${newDashboardId}`]}>
<DashboardProvider dashboardId={newDashboardId}>
<DashboardBootstrapWrapper dashboardId={newDashboardId}>
<TestComponent />
</DashboardProvider>
</DashboardBootstrapWrapper>
</MemoryRouter>
</QueryClientProvider>,
);
@@ -213,7 +222,7 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
});
it('should not fetch when no dashboardId is provided', () => {
renderWithDashboardProvider('');
renderWithDashboardBootstrap('');
// Should not call the API
expect(mockGetDashboard).not.toHaveBeenCalled();
@@ -229,9 +238,9 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
const { rerender } = render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[`/dashboard/${dashboardId1}`]}>
<DashboardProvider dashboardId={dashboardId1}>
<DashboardBootstrapWrapper dashboardId={dashboardId1}>
<TestComponent />
</DashboardProvider>
</DashboardBootstrapWrapper>
</MemoryRouter>
</QueryClientProvider>,
);
@@ -243,9 +252,9 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
rerender(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[`/dashboard/${dashboardId2}`]}>
<DashboardProvider dashboardId={dashboardId2}>
<DashboardBootstrapWrapper dashboardId={dashboardId2}>
<TestComponent />
</DashboardProvider>
</DashboardBootstrapWrapper>
</MemoryRouter>
</QueryClientProvider>,
);
@@ -286,9 +295,9 @@ describe('Dashboard Provider - Query Key with Route Params', () => {
render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[`/dashboard/${dashboardId}`]}>
<DashboardProvider dashboardId={dashboardId}>
<DashboardBootstrapWrapper dashboardId={dashboardId}>
<TestComponent />
</DashboardProvider>
</DashboardBootstrapWrapper>
</MemoryRouter>
</QueryClientProvider>,
);
@@ -365,7 +374,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
// Empty URL variables - tests initialization flow
mockGetUrlVariables.mockReturnValue({});
renderWithDashboardProvider(DASHBOARD_ID);
renderWithDashboardBootstrap(DASHBOARD_ID);
await waitFor(() => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
@@ -421,7 +430,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
.mockReturnValueOnce('development')
.mockReturnValueOnce(['db', 'cache']);
renderWithDashboardProvider(DASHBOARD_ID);
renderWithDashboardBootstrap(DASHBOARD_ID);
await waitFor(() => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
@@ -481,7 +490,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
mockGetUrlVariables.mockReturnValue(urlVariables);
renderWithDashboardProvider(DASHBOARD_ID);
renderWithDashboardBootstrap(DASHBOARD_ID);
await waitFor(() => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
@@ -517,7 +526,7 @@ describe('Dashboard Provider - URL Variables Integration', () => {
.mockReturnValueOnce('development')
.mockReturnValueOnce(['api']);
renderWithDashboardProvider(DASHBOARD_ID);
renderWithDashboardBootstrap(DASHBOARD_ID);
await waitFor(() => {
// Verify normalization was called with the specific values and variable configs
@@ -584,7 +593,7 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
} as any);
/* eslint-enable @typescript-eslint/no-explicit-any */
renderWithDashboardProvider(DASHBOARD_ID);
renderWithDashboardBootstrap(DASHBOARD_ID);
await waitFor(() => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
@@ -626,7 +635,7 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
} as any);
/* eslint-enable @typescript-eslint/no-explicit-any */
renderWithDashboardProvider(DASHBOARD_ID);
renderWithDashboardBootstrap(DASHBOARD_ID);
await waitFor(() => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
@@ -669,7 +678,7 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
} as any);
/* eslint-enable @typescript-eslint/no-explicit-any */
renderWithDashboardProvider(DASHBOARD_ID);
renderWithDashboardBootstrap(DASHBOARD_ID);
await waitFor(() => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });
@@ -711,7 +720,7 @@ describe('Dashboard Provider - Textbox Variable Backward Compatibility', () => {
} as any);
/* eslint-enable @typescript-eslint/no-explicit-any */
renderWithDashboardProvider(DASHBOARD_ID);
renderWithDashboardBootstrap(DASHBOARD_ID);
await waitFor(() => {
expect(mockGetDashboard).toHaveBeenCalledWith({ id: DASHBOARD_ID });

View File

@@ -0,0 +1,51 @@
import type { Layout } from 'react-grid-layout';
import type { StateCreator } from 'zustand';
import type { DashboardStore } from '../useDashboardStore';
export interface DashboardLayoutSlice {
//
layouts: Layout[];
setLayouts: (updater: Layout[] | ((prev: Layout[]) => Layout[])) => void;
//
panelMap: Record<string, { widgets: Layout[]; collapsed: boolean }>;
setPanelMap: (
updater:
| Record<string, { widgets: Layout[]; collapsed: boolean }>
| ((
prev: Record<string, { widgets: Layout[]; collapsed: boolean }>,
) => Record<string, { widgets: Layout[]; collapsed: boolean }>),
) => void;
// resetDashboardLayout: () => void;
}
export const initialDashboardLayoutState = {
layouts: [] as Layout[],
panelMap: {} as Record<string, { widgets: Layout[]; collapsed: boolean }>,
};
export const createDashboardLayoutSlice: StateCreator<
DashboardStore,
[['zustand/immer', never]],
[],
DashboardLayoutSlice
> = (set) => ({
...initialDashboardLayoutState,
setLayouts: (updater): void =>
set((state) => {
state.layouts =
typeof updater === 'function' ? updater(state.layouts) : updater;
}),
setPanelMap: (updater): void =>
set((state) => {
state.panelMap =
typeof updater === 'function' ? updater(state.panelMap) : updater;
}),
// resetDashboardLayout: () =>
// set((state) => {
// Object.assign(state, initialDashboardLayoutState);
// }),
});

View File

@@ -0,0 +1,57 @@
import type { Dashboard } from 'types/api/dashboard/getAll';
import type { StateCreator } from 'zustand';
import type { DashboardStore } from '../useDashboardStore';
export type WidgetColumnWidths = {
[widgetId: string]: Record<string, number>;
};
export interface DashboardUISlice {
//
selectedDashboard: Dashboard | undefined;
setSelectedDashboard: (
updater:
| Dashboard
| undefined
| ((prev: Dashboard | undefined) => Dashboard | undefined),
) => void;
//
columnWidths: WidgetColumnWidths;
setColumnWidths: (
updater:
| WidgetColumnWidths
| ((prev: WidgetColumnWidths) => WidgetColumnWidths),
) => void;
}
export const initialDashboardUIState = {
selectedDashboard: undefined as Dashboard | undefined,
columnWidths: {} as WidgetColumnWidths,
};
export const createDashboardUISlice: StateCreator<
DashboardStore,
[['zustand/immer', never]],
[],
DashboardUISlice
> = (set) => ({
...initialDashboardUIState,
setSelectedDashboard: (updater): void =>
set((state: DashboardUISlice): void => {
state.selectedDashboard =
typeof updater === 'function' ? updater(state.selectedDashboard) : updater;
}),
setColumnWidths: (updater): void =>
set((state: DashboardUISlice): void => {
state.columnWidths =
typeof updater === 'function' ? updater(state.columnWidths) : updater;
}),
resetDashboardUI: (): void =>
set((state: DashboardUISlice): void => {
Object.assign(state, initialDashboardUIState);
}),
});

View File

@@ -0,0 +1,50 @@
import type { Layout } from 'react-grid-layout';
import type { Dashboard } from 'types/api/dashboard/getAll';
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import {
createDashboardLayoutSlice,
DashboardLayoutSlice,
initialDashboardLayoutState,
} from './slices/dashboardLayoutSlice';
import {
createDashboardUISlice,
DashboardUISlice,
initialDashboardUIState,
} from './slices/dashboardUISlice';
export type DashboardStore = DashboardUISlice &
DashboardLayoutSlice & {
resetDashboardStore: () => void;
};
/**
* 'select*' is a redux naming convention that can be carried over to zustand.
* It is used to select a piece of state from the store.
* In this case, we are selecting the locked state of the selected dashboard.
* */
export const selectIsDashboardLocked = (s: DashboardStore): boolean =>
s.selectedDashboard?.locked ?? false;
export const useDashboardStore = create<DashboardStore>()(
immer((set, get, api) => ({
...createDashboardUISlice(set, get, api),
...createDashboardLayoutSlice(set, get, api),
resetDashboardStore: (): void =>
set((state: DashboardStore) => {
Object.assign(state, initialDashboardUIState, initialDashboardLayoutState);
}),
})),
);
// Standalone imperative accessors — use these instead of calling useDashboardStore.getState() at call sites.
export const getSelectedDashboard = (): Dashboard | undefined =>
useDashboardStore.getState().selectedDashboard;
export const getDashboardLayouts = (): Layout[] =>
useDashboardStore.getState().layouts;
export const resetDashboard = (): void =>
useDashboardStore.getState().resetDashboardStore();

View File

@@ -1,41 +0,0 @@
import { Layout } from 'react-grid-layout';
import { UseQueryResult } from 'react-query';
import dayjs from 'dayjs';
import { SuccessResponseV2 } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
export type WidgetColumnWidths = {
[widgetId: string]: Record<string, number>;
};
export interface IDashboardContext {
isDashboardLocked: boolean;
handleDashboardLockToggle: (value: boolean) => void;
dashboardResponse: UseQueryResult<SuccessResponseV2<Dashboard>, unknown>;
selectedDashboard: Dashboard | undefined;
layouts: Layout[];
panelMap: Record<string, { widgets: Layout[]; collapsed: boolean }>;
setPanelMap: React.Dispatch<React.SetStateAction<Record<string, any>>>;
setLayouts: React.Dispatch<React.SetStateAction<Layout[]>>;
setSelectedDashboard: React.Dispatch<
React.SetStateAction<Dashboard | undefined>
>;
updatedTimeRef: React.MutableRefObject<dayjs.Dayjs | null>;
updateLocalStorageDashboardVariables: (
id: string,
selectedValue:
| string
| number
| boolean
| (string | number | boolean)[]
| null
| undefined,
allSelected: boolean,
isDynamic?: boolean,
) => void;
dashboardQueryRangeCalled: boolean;
setDashboardQueryRangeCalled: (value: boolean) => void;
isDashboardFetching: boolean;
columnWidths: WidgetColumnWidths;
setColumnWidths: React.Dispatch<React.SetStateAction<WidgetColumnWidths>>;
}

4
go.mod
View File

@@ -81,6 +81,8 @@ require (
golang.org/x/oauth2 v0.34.0
golang.org/x/sync v0.19.0
golang.org/x/text v0.33.0
gonum.org/v1/gonum v0.17.0
google.golang.org/api v0.265.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
@@ -377,8 +379,6 @@ require (
golang.org/x/sys v0.40.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.41.0 // indirect
gonum.org/v1/gonum v0.17.0 // indirect
google.golang.org/api v0.265.0
google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/grpc v1.78.0 // indirect

115
perses/common/common.cue Normal file
View File

@@ -0,0 +1,115 @@
package common
// ──────────────────────────────────────────────
// Shared types
// ──────────────────────────────────────────────
#TelemetryFieldKey: {
name: string
key?: string
description?: string
unit?: string
signal?: string
fieldContext?: string
fieldDataType?: string
materialized?: bool
isIndexed?: bool
}
// ──────────────────────────────────────────────
// Panel types
// ──────────────────────────────────────────────
#ContextLinkProps: {
url: string
label: string
}
#TimePreference: *"globalTime" | "last5Min" | "last15Min" | "last30Min" | "last1Hr" | "last6Hr" | "last1Day" | "last3Days" | "last1Week" | "last1Month"
#PrecisionOption: *2 | 0 | 1 | 3 | 4 | "full"
#Axes: {
softMin?: number | *null
softMax?: number | *null
isLogScale?: bool | *false
}
#LegendPosition: *"bottom" | "right"
#ThresholdWithLabel: {
value: number
unit?: string
color: string
format: "Text" | "Background"
label?: string
}
#ComparisonThreshold: {
value: number
operator: ">" | "<" | ">=" | "<=" | "="
unit?: string
color: string
format: "Text" | "Background"
}
// ──────────────────────────────────────────────
// Query types
// ──────────────────────────────────────────────
#QueryName: =~"^[A-Za-z][A-Za-z0-9_]*$"
#Limit: int & >=0 & <=10000
#Offset: int & >=0
#ReduceTo: "sum" | "count" | "avg" | "min" | "max" | "last" | "median"
#MetricAggregation: close({
metricName: string & !=""
timeAggregation: "latest" | "sum" | "avg" | "min" | "max" | "count" | "rate" | "increase"
spaceAggregation: "sum" | "avg" | "min" | "max" | "count" | "p50" | "p75" | "p90" | "p95" | "p99"
reduceTo?: #ReduceTo
temporality?: "delta" | "cumulative" | "unspecified"
})
#ExpressionAggregation: close({
expression: string & !=""
alias?: string
})
#Aggregation: #MetricAggregation | #ExpressionAggregation
#FilterExpression: close({
expression: string
})
#GroupByItem: close({
name: string & !=""
fieldDataType?: string
fieldContext?: string
})
#OrderByItem: close({
columnName: string & !=""
order: "asc" | "desc"
})
#HavingExpression: close({
expression: string
})
#Function: close({
name: "cutOffMin" | "cutOffMax" | "clampMin" | "clampMax" |
"absolute" | "runningDiff" | "log2" | "log10" |
"cumulativeSum" | "ewma3" | "ewma5" | "ewma7" |
"median3" | "median5" | "median7" | "timeShift" |
"anomaly" | "fillZero"
args?: [...close({value: number | string | bool})]
})
// ──────────────────────────────────────────────
// Variable types
// ──────────────────────────────────────────────
#VariableSortOrder: *"disabled" | "asc" | "desc"

View File

@@ -0,0 +1,4 @@
module: "github.com/signoz"
language: {
version: "v0.12.0"
}

2134
perses/examples/current.json Normal file

File diff suppressed because it is too large Load Diff

882
perses/examples/perses.json Normal file
View File

@@ -0,0 +1,882 @@
{
"kind": "Dashboard",
"metadata": {
"name": "the-everything-dashboard",
"project": "signoz"
},
"spec": {
"display": {
"name": "The everything dashboard",
"description": "Trying to cover as many concepts here as possible"
},
"duration": "1h",
"datasources": {
"SigNozDatasource": {
"default": true,
"plugin": {
"kind": "SigNozDatasource",
"spec": {}
}
}
},
"variables": [
{
"kind": "ListVariable",
"spec": {
"name": "serviceName",
"display": {
"name": "serviceName"
},
"allowAllValue": true,
"allowMultiple": false,
"plugin": {
"kind": "SigNozDynamicVariable",
"spec": {
"name": "service.name",
"source": "Metrics",
"sort": "disabled"
}
}
}
},
{
"kind": "ListVariable",
"spec": {
"name": "statusCodesFromQuery",
"display": {
"name": "statusCodesFromQuery"
},
"allowAllValue": true,
"allowMultiple": true,
"plugin": {
"kind": "SigNozQueryVariable",
"spec": {
"queryValue": "SELECT JSONExtractString(labels, 'http.status_code') AS status_code FROM signoz_metrics.distributed_time_series_v4_1day WHERE status_code != '' GROUP BY status_code",
"sort": "asc"
}
}
}
},
{
"kind": "ListVariable",
"spec": {
"name": "limit",
"display": {
"name": "limit"
},
"allowAllValue": false,
"allowMultiple": false,
"plugin": {
"kind": "SigNozCustomVariable",
"spec": {
"customValue": "1,10,20,40,80,160,200",
"sort": "disabled"
}
}
}
},
{
"kind": "TextVariable",
"spec": {
"name": "textboxvar",
"display": {
"name": "textboxvar"
},
"value": "defaultvaluegoeshere",
"plugin": {
"kind": "SigNozTextboxVariable",
"spec": {}
}
}
}
],
"panels": {
"24e2697b": {
"kind": "Panel",
"spec": {
"display": {
"name": "total resp size",
"description": ""
},
"plugin": {
"kind": "SigNozTimeSeriesPanel",
"spec": {
"visualization": {
"fillSpans": true
},
"formatting": {
"unit": "By",
"decimalPrecision": 3
},
"axes": {
"softMax": 800,
"isLogScale": true
},
"legend": {
"position": "right",
"customColors": {
"{service.name=\"sampleapp-gateway\"}": "#9ea5f7"
}
},
"contextLinks": [
{
"label": "View service details",
"url": "http://localhost:8080/{{_service.name}}?dfddf=%7B%7Blimit%7D%7D"
}
],
"thresholds": [
{
"value": 1024,
"unit": "By",
"color": "Red",
"format": "Text",
"label": "upper limit"
},
{
"value": 100,
"unit": "By",
"color": "Orange",
"format": "Text",
"label": "kinda bad"
}
]
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "metrics",
"expression": "A",
"aggregations": [
{
"metricName": "http.server.response.body.size.sum",
"reduceTo": "sum",
"spaceAggregation": "sum",
"timeAggregation": "rate"
}
],
"filter": {
"expression": "http.response.status_code IN $statusCodesFromQuery"
},
"groupBy": [
{
"name": "service.name",
"fieldDataType": "string",
"fieldContext": "tag"
}
]
}
}
}
}
]
}
},
"ff2f72f1": {
"kind": "Panel",
"spec": {
"display": {
"name": "fraction of calls",
"description": ""
},
"plugin": {
"kind": "SigNozTimeSeriesPanel",
"spec": {
"visualization": {
"fillSpans": true
},
"formatting": {
"decimalPrecision": 1
},
"thresholds": [
{
"value": 1,
"color": "Blue",
"format": "Background",
"label": "max possible"
}
]
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozCompositeQuery",
"spec": {
"queries": [
{
"type": "builder_query",
"spec": {
"name": "A",
"signal": "metrics",
"expression": "A",
"disabled": true,
"aggregations": [
{
"metricName": "signoz_calls_total",
"reduceTo": "sum",
"spaceAggregation": "sum",
"timeAggregation": "rate"
}
],
"filter": {
"expression": "service.name IN $serviceName AND http.status_code IN $statusCodesFromQuery"
}
}
},
{
"type": "builder_query",
"spec": {
"name": "B",
"signal": "metrics",
"expression": "B",
"disabled": true,
"aggregations": [
{
"metricName": "signoz_calls_total",
"reduceTo": "sum",
"spaceAggregation": "sum",
"timeAggregation": "rate"
}
],
"filter": {
"expression": "service.name in $serviceName"
}
}
},
{
"type": "builder_formula",
"spec": {
"name": "F1",
"expression": "A / B"
}
}
]
}
}
}
}
]
}
},
"011605e7": {
"kind": "Panel",
"spec": {
"display": {
"name": "total resp size"
},
"plugin": {
"kind": "SigNozBarChartPanel",
"spec": {
"visualization": {
"stackedBarChart": false
},
"formatting": {
"unit": "By"
}
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "metrics",
"expression": "A",
"aggregations": [
{
"metricName": "http.server.response.body.size.sum",
"reduceTo": "sum",
"spaceAggregation": "sum",
"timeAggregation": "rate"
}
],
"filter": {
"expression": "http.response.status_code IN $statusCodesFromQuery"
},
"groupBy": [
{
"name": "service.name",
"fieldDataType": "string",
"fieldContext": "tag"
}
]
}
}
}
}
]
}
},
"e23516fc": {
"kind": "Panel",
"spec": {
"display": {
"name": "num traces for service"
},
"plugin": {
"kind": "SigNozNumberPanel",
"spec": {
"formatting": {
"unit": "none",
"decimalPrecision": 1
},
"thresholds": [
{
"value": 1200000,
"operator": ">",
"unit": "none",
"color": "Red",
"format": "Text"
},
{
"value": 1200000,
"operator": "<=",
"unit": "none",
"color": "Green",
"format": "Text"
}
]
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "traces",
"expression": "A",
"aggregations": [
{
"expression": "count() "
}
],
"filter": {
"expression": "service.name = $serviceName "
}
}
}
}
}
]
}
},
"130c8d6b": {
"kind": "Panel",
"spec": {
"display": {
"name": "num logs for service"
},
"plugin": {
"kind": "SigNozNumberPanel",
"spec": {
"formatting": {
"unit": "none",
"decimalPrecision": 1
}
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "logs",
"expression": "A",
"aggregations": [
{
"expression": "count() "
}
],
"filter": {
"expression": "service.name = $serviceName "
}
}
}
}
}
]
}
},
"246f7c6d": {
"kind": "Panel",
"spec": {
"display": {
"name": "num traces for service per resp code"
},
"plugin": {
"kind": "SigNozPieChartPanel",
"spec": {
"formatting": {
"decimalPrecision": 1
},
"legend": {
"customColors": {
"\"201\"": "#2bc051",
"\"400\"": "#cc462e",
"\"500\"": "#ff0000"
}
}
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "traces",
"expression": "A",
"aggregations": [
{
"expression": "count() "
}
],
"filter": {
"expression": "service.name = $serviceName isEntryPoint = 'true'"
},
"groupBy": [
{
"name": "http.response.status_code",
"fieldDataType": "float64",
"fieldContext": "tag"
}
],
"legend": "\"{{http.response.status_code}}\""
}
}
}
}
]
}
},
"21f7d4d0": {
"kind": "Panel",
"spec": {
"display": {
"name": "average latency per service"
},
"plugin": {
"kind": "SigNozTablePanel",
"spec": {
"formatting": {
"columnUnits": {
"A": "s"
}
},
"thresholds": [
{
"value": 1,
"operator": ">",
"unit": "min",
"color": "Red",
"format": "Text",
"tableOptions": "A"
}
]
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozClickHouseSQL",
"spec": {
"name": "A",
"query": "WITH\n __spatial_aggregation_cte AS\n (\n SELECT\n toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(60)) AS ts,\n `service.name`,\n le,\n sum(value) / 60 AS value\n FROM signoz_metrics.distributed_samples_v4 AS points\n INNER JOIN\n (\n SELECT\n fingerprint,\n JSONExtractString(labels, 'service.name') AS `service.name`,\n JSONExtractString(labels, 'le') AS le\n FROM signoz_metrics.time_series_v4\n WHERE (metric_name IN ('signoz_latency.bucket')) AND (LOWER(temporality) LIKE LOWER('delta')) AND (__normalized = 0)\n GROUP BY\n fingerprint,\n `service.name`,\n le\n ) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint\n WHERE metric_name IN ('signoz_latency.bucket')\n GROUP BY\n ts,\n `service.name`,\n le\n ),\n __histogramCTE AS\n (\n SELECT\n ts,\n `service.name`,\n histogramQuantile(arrayMap(x -> toFloat64(x), groupArray(le)), groupArray(value), 0.9) AS value\n FROM __spatial_aggregation_cte\n GROUP BY\n `service.name`,\n ts\n ORDER BY\n `service.name` ASC,\n ts ASC\n )\nSELECT\n `service.name` AS service,\n avg(value) AS A\nFROM __histogramCTE\nGROUP BY `service.name`"
}
}
}
}
]
}
},
"ad5fd556": {
"kind": "Panel",
"spec": {
"display": {
"name": "logs from service"
},
"plugin": {
"kind": "SigNozListPanel",
"spec": {
"selectedLogFields": [
{
"name": "timestamp",
"type": "log",
"dataType": ""
},
{
"name": "body",
"type": "log",
"dataType": ""
},
{
"name": "error",
"type": "",
"dataType": "string"
}
]
}
},
"queries": [
{
"kind": "LogQuery",
"spec": {
"plugin": {
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "logs",
"expression": "A",
"aggregations": [
{
"expression": "count() "
}
],
"filter": {
"expression": "service.name = $serviceName"
},
"groupBy": [],
"order": [
{
"columnName": "timestamp",
"order": "desc"
},
{
"columnName": "id",
"order": "desc"
}
]
}
}
}
}
]
}
},
"f07b59ee": {
"kind": "Panel",
"spec": {
"display": {
"name": "response size buckets"
},
"plugin": {
"kind": "SigNozHistogramPanel",
"spec": {
"histogramBuckets": {
"bucketCount": 60,
"mergeAllActiveQueries": true
}
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "metrics",
"expression": "A",
"aggregations": [
{
"metricName": "http.server.response.body.size.bucket",
"reduceTo": "avg",
"spaceAggregation": "p90",
"timeAggregation": "rate"
}
]
}
}
}
}
]
}
},
"e1a41831": {
"kind": "Panel",
"spec": {
"display": {
"name": "trace operator",
"description": ""
},
"plugin": {
"kind": "SigNozTimeSeriesPanel",
"spec": {
"legend": {
"position": "right"
}
}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozCompositeQuery",
"spec": {
"queries": [
{
"type": "builder_query",
"spec": {
"name": "A",
"signal": "traces",
"expression": "A",
"aggregations": [
{
"expression": "count() "
}
],
"filter": {
"expression": "service.name = 'sampleapp-gateway' "
},
"legend": "Gateway"
}
},
{
"type": "builder_query",
"spec": {
"name": "B",
"signal": "traces",
"expression": "B",
"aggregations": [
{
"expression": "count() "
}
],
"filter": {
"expression": "http.response.status_code = 200"
},
"legend": "$serviceName"
}
},
{
"type": "builder_trace_operator",
"spec": {
"name": "T1",
"expression": "A -> B ",
"aggregations": [
{
"expression": "count()",
"alias": "request_count"
}
]
}
}
]
}
}
}
}
]
}
},
"f0d70491": {
"kind": "Panel",
"spec": {
"display": {
"name": "no results in this promql",
"description": ""
},
"plugin": {
"kind": "SigNozTimeSeriesPanel",
"spec": {}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozCompositeQuery",
"spec": {
"queries": [
{
"type": "promql",
"spec": {
"name": "A",
"query": "sum(rate(flask_exporter_info[5m]))"
}
},
{
"type": "promql",
"spec": {
"name": "B",
"query": "sum(increase(flask_exporter_info[5m]))"
}
}
]
}
}
}
}
]
}
},
"0e6eb4ca": {
"kind": "Panel",
"spec": {
"display": {
"name": "no results in this promql",
"description": ""
},
"plugin": {
"kind": "SigNozTimeSeriesPanel",
"spec": {}
},
"queries": [
{
"kind": "TimeSeriesQuery",
"spec": {
"plugin": {
"kind": "SigNozPromQLQuery",
"spec": {
"name": "A",
"query": "sum(rate(flask_exporter_info[5m]))"
}
}
}
}
]
}
}
},
"layouts": [
{
"kind": "Grid",
"spec": {
"items": [
{
"x": 0,
"y": 0,
"width": 6,
"height": 6,
"content": {
"$ref": "#/spec/panels/24e2697b"
}
},
{
"x": 6,
"y": 0,
"width": 6,
"height": 6,
"content": {
"$ref": "#/spec/panels/ff2f72f1"
}
},
{
"x": 0,
"y": 6,
"width": 6,
"height": 6,
"content": {
"$ref": "#/spec/panels/011605e7"
}
},
{
"x": 6,
"y": 6,
"width": 6,
"height": 3,
"content": {
"$ref": "#/spec/panels/e23516fc"
}
},
{
"x": 6,
"y": 9,
"width": 6,
"height": 3,
"content": {
"$ref": "#/spec/panels/130c8d6b"
}
},
{
"x": 0,
"y": 12,
"width": 6,
"height": 6,
"content": {
"$ref": "#/spec/panels/246f7c6d"
}
},
{
"x": 6,
"y": 12,
"width": 6,
"height": 6,
"content": {
"$ref": "#/spec/panels/21f7d4d0"
}
},
{
"x": 0,
"y": 18,
"width": 6,
"height": 6,
"content": {
"$ref": "#/spec/panels/ad5fd556"
}
},
{
"x": 6,
"y": 18,
"width": 6,
"height": 6,
"content": {
"$ref": "#/spec/panels/f07b59ee"
}
},
{
"x": 0,
"y": 24,
"width": 12,
"height": 6,
"content": {
"$ref": "#/spec/panels/e1a41831"
}
},
{
"x": 0,
"y": 30,
"width": 6,
"height": 6,
"content": {
"$ref": "#/spec/panels/f0d70491"
}
},
{
"x": 6,
"y": 30,
"width": 6,
"height": 6,
"content": {
"$ref": "#/spec/panels/0e6eb4ca"
}
}
]
}
}
]
}
}

140
perses/howToDefineAPanel.md Normal file
View File

@@ -0,0 +1,140 @@
# Adding a new panel plugin
This guide is for developers adding a new panel kind to the Perses schema. It covers the file structure you need to create, the shared types available in `common.cue`, and how to compose them into your panel's spec.
---
## 1. File structure
Create a new directory under `schemas-wrapper/schemas/` named after your panel:
```
schemas-wrapper/schemas/your-panel-name/
├── your-panel-name.cue # Schema definition
└── example.json # Example JSON that validates against the schema
```
The CUE file must use `package model` and declare a `kind` and `spec`:
```cue
package model
import "github.com/signoz/common"
kind: "YourPanelName"
spec: close({
// your fields here
})
```
Register the panel in `schemas-wrapper/schemas/package.json` by adding an entry to the `plugins` array:
```json
{
"kind": "Panel",
"spec": {
"name": "YourPanelName"
}
}
```
---
## 2. Available shared types from `common.cue`
These types are defined in `common/common.cue` and can be used via `import "github.com/signoz/common"`. Use them instead of redefining the same structures in your panel.
## 3. Defining your spec
Your spec is a `close({})` block containing the fields relevant to your panel. Pick from the shared types above and add panel-specific types as needed. Every field should be optional (suffixed with `?`) since a panel should be valid with just defaults.
### Typical patterns
**Panel with graphs** (has axes, legend, thresholds as reference lines):
```cue
spec: close({
visualization?: #Visualization
formatting?: #Formatting
axes?: common.#Axes
legend?: #Legend
contextLinks?: [...common.#ContextLinkProps]
thresholds?: [...common.#ThresholdWithLabel]
})
```
**Panel with a single value** (no axes/legend, thresholds as conditional formatting):
```cue
spec: close({
visualization?: #Visualization
formatting?: #Formatting
contextLinks?: [...common.#ContextLinkProps]
thresholds?: [...common.#ComparisonThreshold]
})
```
**Panel with custom data structure** (panel-specific fields only):
```cue
spec: close({
yourCustomConfig?: #YourCustomConfig
contextLinks?: [...common.#ContextLinkProps]
})
```
### Defining panel-local types
Types that are specific to your panel (not reusable) should be defined in your CUE file, not in common. For example, `#Visualization` varies per panel because each panel has different rendering options:
```cue
#Visualization: {
timePreference?: common.#TimePreference
yourCustomFlag?: bool | *false
}
```
If your panel needs a threshold type that extends a common one, embed it:
```cue
#YourThreshold: {
common.#ComparisonThreshold
extraField: string
}
```
---
## 4. Writing the example JSON
Create an `example.json` that exercises all fields in your spec. This file is used for validation — `./validate.sh` will check it against your schema.
```json
{
"kind": "YourPanelName",
"spec": {
...
}
}
```
Also add at least one panel using your new kind to `examples/perses.json` so it gets validated as part of a full dashboard.
---
## 5. Validation
Run `./validate.sh` from the `perses/` directory. It lints `examples/perses.json` against all registered schemas. If your schema or example has issues, the error will point to the specific field.
---
## Quick reference: existing panels and what they use
| Field | Time-series | Bar-chart | Number | Pie | Table | Histogram | List |
|-------|:-----------:|:---------:|:------:|:---:|:-----:|:---------:|:----:|
| `visualization` | yes | yes | yes | yes | yes | — | — |
| `formatting` | yes | yes | yes | yes | yes | — | — |
| `axes` | yes | yes | — | — | — | — | — |
| `legend` | yes | yes | — | yes | — | yes | — |
| `contextLinks` | yes | yes | yes | yes | yes | yes | — |
| `thresholds` | yes | yes | yes | — | yes | — | — |

View File

@@ -0,0 +1,9 @@
{
"id": "signoz",
"name": "signoz",
"metaData": {
"buildInfo": {
"buildVersion": "0.0.1"
}
}
}

View File

@@ -0,0 +1,136 @@
{
"name": "@signoz/signoz-schemas",
"version": "0.0.1",
"description": "SigNoz schema plugins",
"perses": {
"schemasPath": ".",
"plugins": [
{
"kind": "Datasource",
"spec": {
"name": "SigNozDatasource"
}
},
{
"kind": "Panel",
"spec": {
"name": "SigNozTimeSeriesPanel"
}
},
{
"kind": "Panel",
"spec": {
"name": "SigNozBarChartPanel"
}
},
{
"kind": "Panel",
"spec": {
"name": "SigNozNumberPanel"
}
},
{
"kind": "Panel",
"spec": {
"name": "SigNozPieChartPanel"
}
},
{
"kind": "Panel",
"spec": {
"name": "SigNozTablePanel"
}
},
{
"kind": "Panel",
"spec": {
"name": "SigNozHistogramPanel"
}
},
{
"kind": "Panel",
"spec": {
"name": "SigNozListPanel"
}
},
{
"kind": "Panel",
"spec": {
"name": "SigNozIgnoredFieldsChart"
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozBuilderQuery"
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozClickHouseSQL"
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozPromQLQuery"
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozCompositeQuery"
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozTraceOperator"
}
},
{
"kind": "TimeSeriesQuery",
"spec": {
"name": "SigNozFormula"
}
},
{
"kind": "LogQuery",
"spec": {
"name": "SigNozBuilderQuery"
}
},
{
"kind": "TraceQuery",
"spec": {
"name": "SigNozBuilderQuery"
}
},
{
"kind": "Variable",
"spec": {
"name": "SigNozCustomVariable"
}
},
{
"kind": "Variable",
"spec": {
"name": "SigNozDynamicVariable"
}
},
{
"kind": "Variable",
"spec": {
"name": "SigNozQueryVariable"
}
},
{
"kind": "Variable",
"spec": {
"name": "SigNozTextboxVariable"
}
}
]
}
}

View File

@@ -0,0 +1,40 @@
{
"kind": "SigNozBarChartPanel",
"spec": {
"visualization": {
"timePreference": "globalTime",
"fillSpans": false,
"stackedBarChart": false
},
"formatting": {
"unit": "By",
"decimalPrecision": 2
},
"axes": {
"softMin": 0,
"softMax": 1000,
"isLogScale": false
},
"legend": {
"position": "bottom",
"customColors": {
"series-A": "#9ea5f7"
}
},
"contextLinks": [
{
"label": "View details",
"url": "http://localhost:8080/{{_service.name}}"
}
],
"thresholds": [
{
"value": 500,
"unit": "By",
"color": "Red",
"format": "Text",
"label": "upper limit"
}
]
}
}

View File

@@ -0,0 +1,29 @@
package model
import "github.com/signoz/common"
kind: "SigNozBarChartPanel"
spec: close({
visualization?: #Visualization
formatting?: #Formatting
axes?: common.#Axes
legend?: #Legend
contextLinks?: [...common.#ContextLinkProps]
thresholds?: [...common.#ThresholdWithLabel]
})
#Visualization: {
timePreference?: common.#TimePreference
fillSpans?: bool | *false
stackedBarChart?: bool | *true
}
#Formatting: {
unit?: string | *""
decimalPrecision?: common.#PrecisionOption
}
#Legend: {
position?: common.#LegendPosition
customColors?: [string]: string
}

View File

@@ -0,0 +1,35 @@
package model
import "github.com/signoz/common"
// Source: pkg/types/querybuildertypes/querybuildertypesv5/builder_query.go — QueryBuilderQuery
kind: "SigNozBuilderQuery"
spec: close({
name: common.#QueryName
signal: "metrics" | "logs" | "traces"
expression: string
disabled?: bool | *false
aggregations?: [...common.#Aggregation]
filter?: common.#FilterExpression
groupBy?: [...common.#GroupByItem]
order?: [...common.#OrderByItem]
selectFields?: [...common.#TelemetryFieldKey]
limit?: common.#Limit
limitBy?: #LimitBy
offset?: common.#Offset
cursor?: string
having?: common.#HavingExpression
// secondaryAggregations not added — not yet implemented.
functions?: [...common.#Function]
legend?: string
stepInterval?: number
reduceTo?: common.#ReduceTo
pageSize?: int & >=1
source?: string
})
#LimitBy: close({
keys: [...string]
value: string
})

View File

@@ -0,0 +1,24 @@
{
"kind": "SigNozBuilderQuery",
"spec": {
"name": "A",
"signal": "metrics",
"expression": "A",
"aggregations": [
{
"metricName": "redis_keyspace_hits",
"timeAggregation": "rate",
"spaceAggregation": "sum",
"reduceTo": "sum"
}
],
"filter": {
"expression": "host_name IN $host_name"
},
"groupBy": [],
"order": [],
"disabled": false,
"legend": "Hit/s across all hosts",
"stepInterval": 60
}
}

View File

@@ -0,0 +1,12 @@
package model
import "github.com/signoz/common"
// Source: pkg/types/querybuildertypes/querybuildertypesv5/clickhouse_query.go — ClickHouseQuery
kind: "SigNozClickHouseSQL"
spec: close({
name: common.#QueryName
query: string & !=""
disabled?: bool | *false
legend?: string
})

View File

@@ -0,0 +1,9 @@
{
"kind": "SigNozClickHouseSQL",
"spec": {
"name": "A",
"query": "SELECT toStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS ts, count() AS total FROM signoz_logs.distributed_logs GROUP BY ts ORDER BY ts",
"disabled": false,
"legend": "Log count"
}
}

View File

@@ -0,0 +1,45 @@
package model
import (
bq "github.com/signoz/schemas-wrapper/schemas/signoz-builder-query:model"
f "github.com/signoz/schemas-wrapper/schemas/signoz-formula:model"
to "github.com/signoz/schemas-wrapper/schemas/signoz-trace-operator:model"
pql "github.com/signoz/schemas-wrapper/schemas/signoz-promql:model"
ch "github.com/signoz/schemas-wrapper/schemas/signoz-clickhouse-sql:model"
)
// Source: pkg/types/querybuildertypes/querybuildertypesv5/req.go — CompositeQuery
// SigNozCompositeQuery groups multiple query plugins into a single
// query request. Each entry is a typed envelope whose spec is
// validated by the corresponding plugin schema.
// this is to be used when there are multiple queries in a panel
// in most cases, there will be only one query, and there it is a better idea to
// use the corresponding kind for that query instead of this composite query
kind: "SigNozCompositeQuery"
spec: close({
queries: [...#QueryEnvelope]
})
// QueryEnvelope wraps a single query plugin with a type discriminator.
#QueryEnvelope:
close({
type: "builder_query",
spec: bq.spec
}) |
close({
type: "builder_formula",
spec: f.spec
}) |
close({
type: "builder_trace_operator",
spec: to.spec
}) |
close({
type: "promql",
spec: pql.spec
}) |
close({
type: "clickhouse_sql",
spec: ch.spec
})

View File

@@ -0,0 +1,53 @@
{
"kind": "SigNozCompositeQuery",
"spec": {
"queries": [
{
"type": "builder_query",
"spec": {
"name": "A",
"signal": "metrics",
"expression": "A",
"aggregations": [
{
"metricName": "redis_keyspace_hits",
"timeAggregation": "rate",
"spaceAggregation": "sum",
"reduceTo": "sum"
}
],
"filter": {
"expression": "host_name IN $host_name"
}
}
},
{
"type": "builder_query",
"spec": {
"name": "B",
"signal": "metrics",
"expression": "B",
"aggregations": [
{
"metricName": "redis_keyspace_misses",
"timeAggregation": "rate",
"spaceAggregation": "sum",
"reduceTo": "sum"
}
],
"filter": {
"expression": "host_name IN $host_name"
}
}
},
{
"type": "builder_formula",
"spec": {
"name": "F1",
"expression": "A / (A + B) * 100",
"legend": "Hit rate %"
}
}
]
}
}

View File

@@ -0,0 +1,10 @@
package model
import "github.com/signoz/common"
// defaultValue lives on the Perses ListVariable wrapper (spec level).
kind: "SigNozCustomVariable"
spec: close({
customValue: =~"^[^,]+(,[^,]+)*$"
sort?: common.#VariableSortOrder
})

View File

@@ -0,0 +1,7 @@
{
"kind": "SigNozCustomVariable",
"spec": {
"customValue": "production,staging,development",
"sort": "disabled"
}
}

View File

@@ -0,0 +1,9 @@
package model
kind: "SigNozDatasource"
// SigNoz has a single built-in backend — the frontend already knows
// the API endpoint, so there is no connection config to validate.
// Add fields here if SigNoz ever supports multiple backends or
// configurable API versions.
spec: close({})

View File

@@ -0,0 +1,4 @@
{
"kind": "SigNozDatasource",
"spec": {}
}

View File

@@ -0,0 +1,11 @@
package model
import "github.com/signoz/common"
// defaultValue lives on the Perses ListVariable wrapper (spec level).
kind: "SigNozDynamicVariable"
spec: close({
name: string
source: string
sort?: common.#VariableSortOrder
})

View File

@@ -0,0 +1,8 @@
{
"kind": "SigNozDynamicVariable",
"spec": {
"name": "host_name",
"source": "metrics",
"sort": "asc"
}
}

View File

@@ -0,0 +1,25 @@
package model
// Fields from IBaseWidget that are NOT in any dedicated panel CUE file.
// This file exists as a reference only and will be removed in the end.
// Needs to be removed from package.json as well.
kind: "SigNozIgnoredFieldsChart"
spec: close({
opacity?: string // not wired to chart rendering
nullZeroValues?: string // not wired to chart rendering
stepSize?: number
columnWidths?: [string]: number // not a config choice — persisted user-resized column widths
// "Chart Appearance" section in UI — could not find this section in the app.
// These 4 fields are gated behind panelTypeVs* constants (TIME_SERIES only).
lineInterpolation?: #LineInterpolation
showPoints?: bool
lineStyle?: #LineStyle
fillMode?: #FillMode
})
#LineInterpolation: "linear" | "spline" | "stepAfter" | "stepBefore"
#LineStyle: "solid" | "dashed"
#FillMode: "solid" | "gradient" | "none"

View File

@@ -0,0 +1,17 @@
package model
import "github.com/signoz/common"
// Source: pkg/types/querybuildertypes/querybuildertypesv5/formula.go — QueryBuilderFormula
kind: "SigNozFormula"
spec: close({
name: common.#QueryName
expression: string
disabled?: bool | *false
legend?: string
limit?: common.#Limit
having?: common.#HavingExpression
stepInterval?: number
order?: [...common.#OrderByItem]
functions?: [...common.#Function]
})

View File

@@ -0,0 +1,8 @@
{
"kind": "SigNozFormula",
"spec": {
"name": "F1",
"expression": "A / B * 100",
"legend": "Hit rate %"
}
}

View File

@@ -0,0 +1,21 @@
{
"kind": "SigNozHistogramPanel",
"spec": {
"histogramBuckets": {
"bucketCount": 60,
"bucketWidth": 100,
"mergeAllActiveQueries": true
},
"legend": {
"customColors": {
"series-A": "#9ea5f7"
}
},
"contextLinks": [
{
"label": "View histogram details",
"url": "http://localhost:8080/histogram"
}
]
}
}

View File

@@ -0,0 +1,20 @@
package model
import "github.com/signoz/common"
kind: "SigNozHistogramPanel"
spec: close({
histogramBuckets?: #HistogramBuckets
legend?: #Legend
contextLinks?: [...common.#ContextLinkProps]
})
#HistogramBuckets: {
bucketCount?: number | *30
bucketWidth?: number | *0
mergeAllActiveQueries?: bool | *false
}
#Legend: {
customColors?: [string]: string
}

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