Compare commits

..

7 Commits

Author SHA1 Message Date
makeavish
a5441e4512 fix(ai-assistant): map apply_filter requestType to explorer panel type
"Open in Explorer" chips opened the Logs/Traces Explorer in the default raw
List view even when the agent's query was a grouped aggregation
(requestType=scalar with groupBy): applyFilter never set a panelTypes URL
param, so the explorer fell back to PANEL_TYPES.LIST.

Derive the panel type from the query's requestType (scalar/distribution ->
table, time_series -> graph, raw/other -> list) and set panelTypes on both the
on-page (redirectWithQueryBuilderData) and off-page (buildExplorerNavigationUrl)
navigation paths, mirroring the saved-view path that already worked. Removes
leftover [apply_filter] debug logs.
2026-06-16 16:24:36 +05:30
Yunus M
74d5d0fd38 feat: streamline error handling with resolveAssistantErrorMessage utility 2026-06-15 20:57:53 +05:30
Yunus M
a9c13f41de fix: reduce size of fixed footer when assistant side panel is open 2026-06-15 20:40:50 +05:30
Yunus M
9372eb955d fix: show Noz in dashboard edit panel view 2026-06-15 20:25:48 +05:30
Yunus M
4392364fd3 feat: links should open in new tab 2026-06-15 20:17:50 +05:30
Yunus M
cc6605b64e feat: show current alert name in context in alert details page 2026-06-15 20:10:44 +05:30
Yunus M
94b475794e fix: wire open view and open channel flows 2026-06-15 19:30:06 +05:30
344 changed files with 3682 additions and 45817 deletions

View File

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

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.129.0
image: signoz/signoz:v0.128.0
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port

View File

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

View File

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

View File

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

View File

@@ -1357,14 +1357,6 @@ components:
- appservice
- containerapp
- aks
- sqldatabase
- sqldatabasemi
- mysqlflexibleserver
- postgresqlflexibleserver
- mongodb
- cosmosdb
- cassandradb
- redis
type: string
CloudintegrationtypesServiceMetadata:
properties:
@@ -2591,41 +2583,6 @@ components:
- panels
- layouts
type: object
DashboardtypesDashboardView:
properties:
createdAt:
format: date-time
type: string
data:
$ref: '#/components/schemas/DashboardtypesDashboardViewData'
id:
type: string
name:
type: string
orgId:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- name
- data
- orgId
type: object
DashboardtypesDashboardViewData:
properties:
order:
$ref: '#/components/schemas/DashboardtypesListOrder'
query:
type: string
sort:
$ref: '#/components/schemas/DashboardtypesListSort'
version:
type: string
required:
- version
type: object
DashboardtypesDatasourcePlugin:
discriminator:
mapping:
@@ -2910,15 +2867,6 @@ components:
- total
- tags
type: object
DashboardtypesListableDashboardView:
properties:
views:
items:
$ref: '#/components/schemas/DashboardtypesDashboardView'
type: array
required:
- views
type: object
DashboardtypesListedDashboardForUserV2:
properties:
createdAt:
@@ -3223,16 +3171,6 @@ components:
- tags
- spec
type: object
DashboardtypesPostableDashboardView:
properties:
data:
$ref: '#/components/schemas/DashboardtypesDashboardViewData'
name:
type: string
required:
- name
- data
type: object
DashboardtypesPostablePublicDashboard:
properties:
defaultTimeRange:
@@ -3628,6 +3566,10 @@ components:
items:
$ref: '#/components/schemas/ErrorsResponseerroradditional'
type: array
invalidReferences:
items:
type: string
type: array
message:
type: string
retry:
@@ -3648,10 +3590,6 @@ components:
properties:
message:
type: string
suggestions:
items:
type: string
type: array
type: object
ErrorsResponseretryjson:
properties:
@@ -9066,6 +9004,10 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"401":
content:
@@ -9218,6 +9160,10 @@ paths:
$ref: '#/components/schemas/DashboardtypesUpdatablePublicDashboard'
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"401":
content:
@@ -9812,6 +9758,10 @@ paths:
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeRequest'
responses:
"200":
content:
application/json:
schema:
type: string
description: OK
"400":
content:
@@ -10996,6 +10946,10 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"401":
content:
@@ -11109,6 +11063,10 @@ paths:
$ref: '#/components/schemas/AuthtypesPatchableRole'
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"401":
content:
@@ -11255,6 +11213,10 @@ paths:
$ref: '#/components/schemas/CoretypesPatchableObjects'
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:
@@ -11704,6 +11666,10 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"401":
content:
@@ -11811,6 +11777,10 @@ paths:
$ref: '#/components/schemas/ServiceaccounttypesPostableServiceAccount'
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:
@@ -11992,6 +11962,10 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"401":
content:
@@ -12049,6 +12023,10 @@ paths:
$ref: '#/components/schemas/ServiceaccounttypesUpdatableFactorAPIKey'
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:
@@ -12231,6 +12209,10 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"401":
content:
@@ -12306,6 +12288,10 @@ paths:
$ref: '#/components/schemas/ServiceaccounttypesPostableServiceAccount'
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"404":
content:
@@ -12797,53 +12783,6 @@ paths:
summary: Update a span mapper
tags:
- spanmapper
/api/v1/stats:
get:
deprecated: false
description: This endpoint returns the collected stats for the organization
operationId: GetStats
responses:
"200":
content:
application/json:
schema:
properties:
data:
additionalProperties: {}
type: object
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get stats
tags:
- stats
/api/v1/testChannel:
post:
deprecated: true
@@ -13382,231 +13321,6 @@ paths:
summary: Update user preference
tags:
- preferences
/api/v2/dashboard_views:
get:
deprecated: false
description: Returns every saved view in the calling user's org. Saved views
are shared org-wide.
operationId: ListDashboardViews
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesListableDashboardView'
status:
type: string
required:
- status
- data
type: object
description: OK
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List dashboard saved views
tags:
- dashboard
post:
deprecated: false
description: Persists the calling user's dashboard listing state (query, sort,
order) as a named, reusable view shared across the org.
operationId: CreateDashboardView
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/DashboardtypesPostableDashboardView'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesDashboardView'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- EDITOR
- tokenizer:
- EDITOR
summary: Create dashboard saved view
tags:
- dashboard
/api/v2/dashboard_views/{id}:
delete:
deprecated: false
description: Removes a saved view. Saved views are shared org-wide. Deleting
a non-existent view returns 404.
operationId: DeleteDashboardView
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- EDITOR
- tokenizer:
- EDITOR
summary: Delete dashboard saved view
tags:
- dashboard
put:
deprecated: false
description: Replaces a saved view's name and data. Saved views are shared org-wide.
operationId: UpdateDashboardView
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/DashboardtypesPostableDashboardView'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesDashboardView'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- EDITOR
- tokenizer:
- EDITOR
summary: Update dashboard saved view
tags:
- dashboard
/api/v2/dashboards:
get:
deprecated: false
@@ -13755,6 +13469,10 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:
@@ -14000,74 +13718,6 @@ paths:
summary: Update dashboard (v2)
tags:
- dashboard
/api/v2/dashboards/{id}/clone:
post:
deprecated: false
description: This endpoint clones an existing v2-shape dashboard. User and integration
dashboards can be cloned; system dashboards are rejected. The clone keeps
the source's display name, panels, and tags, but gets a freshly generated
unique internal name and is always created as an unlocked user dashboard owned
by the caller.
operationId: CloneDashboardV2
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesGettableDashboardV2'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- EDITOR
- tokenizer:
- EDITOR
summary: Clone dashboard (v2)
tags:
- dashboard
/api/v2/dashboards/{id}/lock:
delete:
deprecated: false
@@ -14082,6 +13732,10 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:
@@ -14134,6 +13788,10 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:
@@ -15864,6 +15522,10 @@ paths:
$ref: '#/components/schemas/MetricsexplorertypesUpdateMetricMetadataRequest'
responses:
"200":
content:
application/json:
schema:
type: string
description: OK
"400":
content:
@@ -21162,6 +20824,10 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:
@@ -21209,6 +20875,10 @@ paths:
type: string
responses:
"204":
content:
application/json:
schema:
type: string
description: No Content
"400":
content:

View File

@@ -109,20 +109,6 @@ func (h *handler) CreateThing(rw http.ResponseWriter, req *http.Request) {
}
```
When you need an ID from `claims` as a `valuer.UUID` (for example to pass it to a module), derive it with the `Must*` constructor instead of `NewUUID` plus an error check. Claims are validated by the auth middleware, so the conversion cannot fail and the error branch would be dead code:
```go
// Good — claims are pre-validated, the conversion cannot fail.
orgID := valuer.MustNewUUID(claims.OrgID)
// Avoid — the error path is unreachable.
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
```
### 3. Register the handler in `signozapiserver`
In `pkg/apiserver/signozapiserver`, add a route in the appropriate `add*Routes` function (`addUserRoutes`, `addSessionRoutes`, `addOrgRoutes`, etc.). The pattern is:
@@ -401,4 +387,3 @@ Note the discriminator property lives in the variants, not on the parent — the
- **Add `nullable:"true"`** on fields that can be `null`. Pay special attention to slices and maps -- in Go these default to `nil` which serializes to `null`. If the field should always be an array, initialize it and do not mark it nullable.
- **Implement `Enum()`** on every type that has a fixed set of acceptable values so the JSON schema generates proper `enum` constraints.
- **Add request examples** via `RequestExamples` in `OpenAPIDef` for any non-trivial endpoint. See `pkg/apiserver/signozapiserver/querier.go` for reference.
- **Derive IDs from `claims` with `valuer.MustNewUUID`** (e.g. `claims.OrgID`, `claims.UserID`). Claims are pre-validated by the auth middleware, so use the `Must*` constructor — don't write `NewUUID` followed by an `if err != nil { render.Error(...); return }` block.

View File

@@ -217,10 +217,6 @@ func (module *module) CreateV2(ctx context.Context, orgID valuer.UUID, createdBy
return module.pkgDashboardModule.CreateV2(ctx, orgID, createdBy, creator, source, postable)
}
func (module *module) CloneV2(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.CloneV2(ctx, orgID, createdBy, creator, id)
}
func (module *module) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.GetV2(ctx, orgID, id)
}
@@ -266,22 +262,6 @@ func (module *module) DeletePreferencesForUser(ctx context.Context, orgID valuer
return module.pkgDashboardModule.DeletePreferencesForUser(ctx, orgID, userID)
}
func (module *module) CreateView(ctx context.Context, orgID valuer.UUID, postable dashboardtypes.PostableDashboardView) (*dashboardtypes.DashboardView, error) {
return module.pkgDashboardModule.CreateView(ctx, orgID, postable)
}
func (module *module) ListViews(ctx context.Context, orgID valuer.UUID) (*dashboardtypes.ListableDashboardView, error) {
return module.pkgDashboardModule.ListViews(ctx, orgID)
}
func (module *module) UpdateView(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updateable dashboardtypes.UpdatableDashboardView) (*dashboardtypes.DashboardView, error) {
return module.pkgDashboardModule.UpdateView(ctx, orgID, id, updateable)
}
func (module *module) DeleteView(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
return module.pkgDashboardModule.DeleteView(ctx, orgID, id)
}
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.Get(ctx, orgID, id)
}

View File

@@ -3,13 +3,13 @@ package querier
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
anomalyV2 "github.com/SigNoz/signoz/ee/anomaly"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/types/authtypes"
@@ -48,8 +48,8 @@ func (h *handler) QueryRange(rw http.ResponseWriter, req *http.Request) {
}
var queryRangeRequest qbtypes.QueryRangeRequest
if err := binding.JSON.BindBody(req.Body, &queryRangeRequest); err != nil {
render.Error(rw, err)
if err := json.NewDecoder(req.Body).Decode(&queryRangeRequest); err != nil {
render.Error(rw, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to decode request body: %v", err))
return
}

View File

@@ -10,6 +10,7 @@ import (
"go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
"go.opentelemetry.io/otel/propagation"
"github.com/SigNoz/signoz/pkg/cache/memorycache"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/gorilla/handlers"
@@ -19,6 +20,7 @@ import (
"github.com/SigNoz/signoz/ee/query-service/app/api"
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/web"
@@ -57,12 +59,25 @@ type Server struct {
// NewServer creates and initializes Server
func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
cacheForTraceDetail, err := memorycache.New(context.TODO(), signoz.Instrumentation.ToProviderSettings(), cache.Config{
Provider: "memory",
Memory: cache.Memory{
NumCounters: 10 * 10000,
MaxCost: 1 << 27, // 128 MB
},
})
if err != nil {
return nil, err
}
reader := clickhouseReader.NewReader(
signoz.Instrumentation.Logger(),
signoz.SQLStore,
signoz.TelemetryStore,
signoz.Prometheus,
signoz.TelemetryStore.Cluster(),
config.Querier.FluxInterval,
cacheForTraceDetail,
signoz.Cache,
nil,
)

View File

@@ -291,8 +291,6 @@
// Prevents the usage of specific antd components in favor of our lib
"signoz/no-signozhq-ui-barrel": "error",
// Forces subpath imports (@signozhq/ui/<component>) instead of the eagerly-loaded barrel
"signoz/no-css-module-bracket-access": "warn",
// Prevents bracket access on CSS modules (styles['kebab-case']) which fails with camelCaseOnly config
"no-restricted-globals": [
"error",
{

View File

@@ -2,33 +2,9 @@
const path = require('path');
module.exports = {
plugins: [
path.join(__dirname, 'stylelint-rules/no-unsupported-asset-url.js'),
path.join(__dirname, 'stylelint-rules/css-modules/no-deep-nesting.js'),
path.join(__dirname, 'stylelint-rules/css-modules/no-id-selectors.js'),
path.join(
__dirname,
'stylelint-rules/css-modules/no-bare-element-selectors.js',
),
path.join(__dirname, 'stylelint-rules/css-modules/prefer-css-variables.js'),
path.join(__dirname, 'stylelint-rules/css-modules/class-name-pattern.js'),
],
plugins: [path.join(__dirname, 'stylelint-rules/no-unsupported-asset-url.js')],
customSyntax: 'postcss-scss',
rules: {
// Applies to all SCSS files
'local/no-unsupported-asset-url': true,
},
overrides: [
{
// CSS module-specific rules
files: ['**/*.module.scss'],
rules: {
'local/no-deep-nesting': [true, { severity: 'warning' }],
'local/no-id-selectors': true,
'local/no-bare-element-selectors': true,
'local/prefer-css-variables': [true, { severity: 'warning' }],
'local/class-name-pattern': [true, { severity: 'warning' }],
},
},
],
};

View File

@@ -23,8 +23,6 @@ You are operating within a constrained context window and strict system prompts.
- Always add data-testid or testId (if supported) to critical/behavioral components like inputs, buttons, etc...
- When creating test, these IDs should be used instead of finding by role.
- Never create barrel files.
- When writing new css, prefer CSS Modules
- Use ./docs/css-modules-guide.md as reference on how to write good CSS Modules.
3. FORCED VERIFICATION: Your internal tools mark file writes as successful even if the code does not compile. You are FORBIDDEN from reporting a task as complete until you have:
- Run `pnpm tsgo --noEmit`

View File

@@ -1,513 +0,0 @@
# CSS Modules Guide
## Checklist Before Committing
- [ ] All class names use camelCase in CSS
- [ ] State classes use `is-`/`has-` prefix (e.g., `isActive`, `hasError`)
- [ ] No bracket access (`styles['...']`) in JS unless verified
- [ ] No dynamic class lookup - use explicit variant maps instead
- [ ] No deep class nesting (max 3 class levels; pseudo-classes/elements and parent-reference selectors like `&.active`, `&#bar` are not counted)
- [ ] No hardcoded colors - use `--l1/l2/l3-*` semantic tokens (not `--bg-*` primitives)
- [ ] No magic numbers - use `--spacing-*` tokens
- [ ] Typography uses `--periscope-font-size-*` or `--font-size-*` tokens
- [ ] @signozhq/ui overrides use CSS variables, not direct class overrides
- [ ] Global escapes only for third-party overrides
- [ ] No ID selectors
- [ ] No bare element selectors
- [ ] Keyframes use `:local(@keyframes name)` to avoid global collisions
## Config (vite.config.ts)
```ts
css: {
modules: {
localsConvention: 'camelCaseOnly',
},
}
```
**Critical:** `camelCaseOnly` exports ONLY camelCase keys. Original kebab-case NOT accessible.
## Quick Reference
| CSS Class | JS Access | Works? | Preferred? |
|-----------|-----------|--------|----------------------------|
| `.alertHistory` | `styles.alertHistory` | Yes | Yes |
| `.alert-history` | `styles.alertHistory` | Yes | No, use `.alertHistory` |
| `.alert-history` | `styles['alert-history']` | NO - undefined | Never, use `.alertHistory` |
## Bad Patterns
### Class Naming
```scss
// BAD: Bracket access won't work
.my-class { }
// Then in JS: styles['my-class'] -> undefined
// BAD: Collision - both become same key
.alertHistory { }
.alert-history { } // -> styles.alertHistory (conflicts)
// BAD: Underscore inconsistency
.my_class { } // -> styles.myClass (confusing)
// GOOD: Direct camelCase
.alertHistory { }
.statsCard { }
// GOOD: State classes with is-/has- prefix
.isDisabled { }
.isActive { }
.hasError { }
.isLoading { }
```
### Nesting
```scss
// BAD: Deep nesting - specificity wars, hard to override
.container {
.wrapper {
.inner {
.content { }
}
}
}
// BAD: Nesting creates separate classes you might not expect
.button {
.icon { } // -> styles.icon (separate class, not scoped under .button)
}
// GOOD: Flat structure
.container { }
.containerWrapper { }
.containerContent { }
// GOOD: Nesting only for pseudo/states
.button {
&:hover { }
&:disabled { }
&::before { }
}
```
### Global Escapes
```scss
// BAD: Overusing global
:global {
.everything { }
.in-here { }
.is-global { }
}
// BAD: Global without necessity
:global(.myComponent) { } // defeats purpose of modules
// GOOD: Targeted global for third-party overrides
.container {
:global(.ant-modal-content) {
padding: 0;
}
}
```
### Selectors
```scss
// BAD: ID selectors - not reusable
#myComponent { }
// BAD: Element selectors without scope
div { } // affects ALL divs in component
// BAD: Complex selectors
.container > div + span ~ p { }
// GOOD: Class-only selectors
.container { }
.title { }
```
### Variables & Values
```scss
// BAD: Hardcoded colors
.button {
background: #1890ff;
color: white;
}
// BAD: Magic numbers
.container {
padding: 17px;
margin-left: 43px;
}
// GOOD: Semantic tokens (theme-aware)
.button {
background: var(--primary-background);
color: var(--primary-foreground);
}
.card {
background: var(--l2-background);
color: var(--l2-foreground);
}
// GOOD: Spacing system
.container {
padding: var(--spacing-4);
margin-left: var(--spacing-5);
}
```
## Design Tokens (@signozhq/design-tokens)
Prefer semantic tokens over hardcoded values.
You can read the ./node_modules/@signozhq/design-tokens/dist/style.css to find complete list of available tokens.
### Spacing
```scss
// Spacing scale (index -> px):
// --spacing-0=0 --spacing-1=2 --spacing-2=4 --spacing-3=6 --spacing-4=8
// --spacing-5=10 --spacing-6=12 --spacing-7=14 --spacing-8=16 --spacing-10=20
// --spacing-12=24 --spacing-16=32 --spacing-20=40 --spacing-24=48 --spacing-32=64
// --spacing-40=80 --spacing-48=96 --spacing-56=112 --spacing-64=128
// (index != px; --spacing-2 is 4px, not 2px)
.container {
padding: var(--spacing-4); // 8px
gap: var(--spacing-6); // 12px
margin-bottom: var(--spacing-8); // 16px
}
// Also available: --padding-* and --margin-* (rem-based)
// --padding-1 = 0.25rem, --padding-4 = 1rem, etc.
```
### Typography
```scss
// Font sizes (preferred)
.title {
font-size: var(--periscope-font-size-large); // 18px
font-size: var(--periscope-font-size-medium); // 16px
font-size: var(--periscope-font-size-base); // 13px
font-size: var(--periscope-font-size-small); // 11px
}
// Alternative scale (rem-based)
.heading {
font-size: var(--font-size-xl); // 1.25rem
font-size: var(--font-size-lg); // 1.125rem
font-size: var(--font-size-base); // 1rem
font-size: var(--font-size-sm); // 0.875rem
}
// Font weights
.bold {
font-weight: var(--font-weight-semibold); // 600
font-weight: var(--font-weight-medium); // 500
font-weight: var(--font-weight-normal); // 400
}
// Line heights
.text {
line-height: var(--line-height-20); // 20px
line-height: var(--line-height-24); // 24px
}
```
### Colors (Prefer Semantic Tokens)
Use L1/L2/L3 semantic tokens - they handle light/dark theme automatically.
```scss
// BAD: Primitive tokens (fixed value across themes, won't swap on theme change)
.card {
background: var(--bg-ink-400);
color: var(--text-vanilla-100);
}
// GOOD: L1/L2/L3 tokens (theme-aware - swap automatically light/dark)
.card {
background: var(--l1-background); // base layer
color: var(--l1-foreground); // primary text
}
.panel {
background: var(--l2-background); // elevated surface
color: var(--l2-foreground); // secondary text
border-color: var(--l2-border);
}
.nested {
background: var(--l3-background); // nested/inset
color: var(--l3-foreground); // tertiary text
}
// Hover states
.card:hover {
background: var(--l1-background-hover);
color: var(--l1-foreground-hover);
}
// Semantic action colors (also theme-aware)
.primary {
background: var(--primary-background);
color: var(--primary-foreground);
}
.danger {
background: var(--danger-background);
color: var(--danger-foreground);
}
.success {
background: var(--success-background);
color: var(--success-foreground);
}
.warning {
background: var(--warning-background);
color: var(--warning-foreground);
}
// Accent colors (for highlights, badges, etc.)
.accent {
background: var(--accent-primary); // robin blue
background: var(--accent-forest); // green
background: var(--accent-cherry); // red
background: var(--accent-amber); // yellow
}
```
**Token hierarchy:**
- Primitive tokens (`--bg-*`, `--text-*`, etc.) have fixed values across themes.
- Semantic tokens (L1/L2/L3, `--primary-*`, `--danger-*`, etc.) automatically swap based on theme.
- L1 = base/root layer
- L2 = elevated surfaces (cards, panels)
- L3 = nested/inset elements
## Overriding @signozhq/ui Components
Components expose CSS variables for customization.
You can ensure they exist by looking at ./node_modules/@signozhq/ui/dist.
Never write a override without confirm it exists.
Override via:
### Method 1: CSS Variables (Preferred)
Each component exposes `--<component>-<property>` variables:
```scss
// Override Button
.customButton {
--button-background: var(--success-background);
--button-border-radius: var(--radius-2);
--button-padding: var(--spacing-4) var(--spacing-8);
--button-font-size: var(--periscope-font-size-base);
}
// Override Input
.customInput {
--input-height: 2.5rem;
--input-border-color: var(--l2-border);
--input-padding: var(--spacing-2) var(--spacing-6);
--input-placeholder-color: var(--l3-foreground);
}
// Override nested parts
.customInput {
--input-prefix-padding: 0 var(--spacing-4) 0 var(--spacing-6);
--input-suffix-color: var(--accent-primary);
}
```
### Method 2: Data Attributes
Components use data attributes for variants/states. Target them for state-specific overrides:
```scss
// Target variant
.wrapper :global([data-variant="outlined"]) {
--button-border-color: var(--accent-primary);
}
// Target size
.wrapper :global([data-size="sm"]) {
--button-font-size: var(--periscope-font-size-small);
}
// Target color
.wrapper :global([data-color="destructive"]) {
--button-background: var(--danger-background);
}
// Target state (Radix patterns)
.popover :global([data-state="open"]) {
opacity: 1;
}
.tooltip :global([data-side="top"]) {
margin-bottom: var(--spacing-2);
}
```
### Common Component CSS Variables
**Button:**
- `--button-background`, `--button-border-radius`, `--button-padding`
- `--button-font-size`, `--button-height`, `--button-gap`
- `--button-hover-background`, `--button-disabled-opacity`
**Input:**
- `--input-height`, `--input-border-color`, `--input-background`
- `--input-padding`, `--input-font-size`, `--input-placeholder-color`
- `--input-focus-outline-color`, `--input-hover-border-color`
- `--input-prefix-*`, `--input-suffix-*` for adornments
**General pattern:** `--<component>-<property>` or `--<component>-<state>-<property>`
## Good Patterns
### Structure
```scss
// Flat, descriptive, component-scoped
.alertHistory { }
.alertHistoryHeader { }
.alertHistoryContent { }
.alertHistoryFooter { }
// State modifiers as separate classes
.alertHistory { }
.alertHistoryLoading { }
.alertHistoryEmpty { }
.alertHistoryError { }
```
### Composition
```scss
// GOOD: Composing styles
.baseButton {
padding: var(--spacing-2) var(--spacing-4);
border-radius: var(--radius-2);
}
.primaryButton {
composes: baseButton;
background: var(--primary-background);
}
```
### Pseudo Elements
```scss
.button {
// States
&:hover { opacity: 0.9; }
&:focus { outline: 2px solid var(--ring); outline-offset: 2px; }
&:disabled { opacity: 0.5; cursor: not-allowed; }
// Pseudo elements
&::before { content: ''; }
&::after { content: ''; }
}
```
### Media Queries
```scss
.container {
display: flex;
flex-direction: column;
@media (min-width: 768px) {
flex-direction: row;
}
}
```
### Keyframes (Local Scoping)
Without `:local()`, keyframe names are global and can clash across modules:
```scss
// BAD: Global keyframe - can conflict with other modules
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
// GOOD: Locally scoped keyframe
:local(@keyframes fadeIn) {
from { opacity: 0; }
to { opacity: 1; }
}
.modal {
animation: fadeIn 200ms ease;
}
```
## JS Import Patterns
```tsx
// GOOD
import styles from './Component.module.scss';
<div className={styles.container}>
<span className={styles.title}>Title</span>
</div>
// GOOD: Conditional classes
<div className={`${styles.button} ${isActive ? styles.buttonActive : ''}`}>
// GOOD: With clsx/classnames
<div className={clsx(styles.button, { [styles.buttonActive]: isActive })}>
// BAD: Bracket access (may be undefined)
<div className={styles['button-active']}> // undefined if CSS has .button-active
// BAD: String interpolation for class names
<div className={`${styles.button}-active`}> // won't work
// BAD: Dynamic class lookup - can't be statically analyzed
const cls = styles[`variant${props.type}`]; // Vite can't tree-shake or type-check
// GOOD: Explicit map for dynamic variants
const variantMap = {
primary: styles.variantPrimary,
secondary: styles.variantSecondary,
ghost: styles.variantGhost,
};
const cls = variantMap[props.type];
```
## Lint Rules
### JS/TS (oxlint)
| Rule | Severity | Catches |
|------|----------|---------|
| `signoz/no-css-module-bracket-access` | warn | `styles['kebab-case']`, dynamic access |
### CSS/SCSS (stylelint)
| Rule | Severity | Catches |
|------|----------|---------|
| `local/no-deep-nesting` | warning | class nesting >3 levels (pseudo-classes/elements and parent-reference selectors `&.foo`, `&#bar` not counted; configurable via `maxDepth` secondary option) |
| `local/no-id-selectors` | error | `#id` selectors |
| `local/no-bare-element-selectors` | error | root-level `div`, `span` etc |
| `local/prefer-css-variables` | warning | hardcoded colors |
| `local/class-name-pattern` | warning | kebab-case, snake_case, PascalCase |
Run: `pnpm lint:styles` to check CSS modules.

View File

@@ -45,8 +45,8 @@
"@dnd-kit/utilities": "3.2.2",
"@grafana/data": "^11.6.14",
"@monaco-editor/react": "^4.7.0",
"@sentry/react": "10.57.0",
"@sentry/vite-plugin": "5.3.0",
"@sentry/react": "8.41.0",
"@sentry/vite-plugin": "2.22.6",
"@signozhq/design-tokens": "2.1.4",
"@signozhq/icons": "0.4.0",
"@signozhq/resizable": "0.0.2",
@@ -192,9 +192,9 @@
"lint-staged": "^17.0.4",
"msw": "1.3.2",
"orval": "8.9.1",
"oxfmt": "0.54.0",
"oxlint": "1.69.0",
"oxlint-tsgolint": "0.23.0",
"oxfmt": "0.47.0",
"oxlint": "1.62.0",
"oxlint-tsgolint": "0.22.1",
"postcss": "8.5.14",
"postcss-scss": "4.0.9",
"react-resizable": "3.0.4",

View File

@@ -1,144 +0,0 @@
/**
* Rule: no-css-module-bracket-access
*
* Prevents bracket access on CSS module imports that may fail with camelCaseOnly config.
*
* With Vite's `localsConvention: 'camelCaseOnly'`, kebab-case class names are
* converted to camelCase and the original key is NOT exported.
*
* This rule catches patterns like:
* styles['my-class'] // BAD - undefined if CSS has .my-class
* styles['myClass'] // OK but prefer dot notation
* styles.myClass // GOOD
*
* Catches:
* - Bracket access with kebab-case strings (always fails)
* - Bracket access with any string literal (warn - prefer dot notation)
* - Dynamic bracket access (warn - risky)
*/
const CSS_MODULE_IMPORT_NAMES = new Set([
'styles',
'classes',
'css',
'classNames',
]);
function looksLikeCssModuleImport(name) {
// Common patterns: styles, componentStyles, alertHistoryStyles
return (
CSS_MODULE_IMPORT_NAMES.has(name) ||
name.endsWith('Styles') ||
name.endsWith('Classes') ||
name.endsWith('Css')
);
}
function isKebabCase(str) {
return str.includes('-');
}
function isSnakeCase(str) {
return str.includes('_');
}
export default {
create(context) {
return {
MemberExpression(node) {
// Only check bracket notation: styles['...']
if (!node.computed) {
return;
}
const object = node.object;
if (object.type !== 'Identifier') {
return;
}
// Check if this looks like a CSS module import
if (!looksLikeCssModuleImport(object.name)) {
return;
}
const property = node.property;
// Dynamic access: styles[variable]
if (property.type === 'Identifier') {
context.report({
node,
message: `Dynamic CSS module access '${object.name}[${property.name}]' is risky. With 'camelCaseOnly' config, kebab-case keys don't exist. Use dot notation or verify the key exists.`,
});
return;
}
// Template literal: styles[\`...\`]
if (property.type === 'TemplateLiteral') {
context.report({
node,
message: `Template literal CSS module access is risky. With 'camelCaseOnly' config, kebab-case keys don't exist. Prefer dot notation.`,
});
return;
}
// Numeric / boolean / null literal: styles[0]. Not a class lookup; ignore.
if (property.type === 'Literal' && typeof property.value !== 'string') {
return;
}
// String literal: styles['...']
if (property.type === 'Literal' && typeof property.value === 'string') {
const className = property.value;
// Kebab-case will definitely fail
if (isKebabCase(className)) {
context.report({
node,
message: `CSS module class '${className}' uses kebab-case which won't work with 'camelCaseOnly' config. Use '${object.name}.${toCamelCase(className)}' instead.`,
});
return;
}
// Snake_case is suspicious
if (isSnakeCase(className)) {
context.report({
node,
message: `CSS module class '${className}' uses snake_case which may not work as expected. Prefer camelCase: '${object.name}.${toCamelCase(className)}'.`,
});
return;
}
// Valid camelCase but using bracket notation - prefer dot
if (/^[a-z][a-zA-Z0-9]*$/.test(className)) {
context.report({
node,
message: `Prefer dot notation: '${object.name}.${className}' instead of '${object.name}['${className}']'.`,
});
}
return;
}
// Catch-all for other dynamic expressions:
// styles['prefix' + suffix] (BinaryExpression)
// styles[isActive && 'foo'] (LogicalExpression)
// styles[isActive ? 'a' : 'b'] (ConditionalExpression)
// styles[fn()] (CallExpression)
context.report({
node,
message: `Dynamic CSS module access on '${object.name}' is risky. With 'camelCaseOnly' config, kebab-case keys don't exist. Use dot notation or verify each key resolves to an exported camelCase class.`,
});
},
};
},
};
function toCamelCase(str) {
return str
.split(/[-_]/)
.map((part, i) =>
i === 0
? part.toLowerCase()
: part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(),
)
.join('');
}

View File

@@ -11,7 +11,6 @@ import noUnsupportedAssetPattern from './rules/no-unsupported-asset-pattern.mjs'
import noRawAbsolutePath from './rules/no-raw-absolute-path.mjs';
import noAntdComponents from './rules/no-antd-components.mjs';
import noSignozhqUiBarrel from './rules/no-signozhq-ui-barrel.mjs';
import noCssModuleBracketAccess from './rules/no-css-module-bracket-access.mjs';
export default {
meta: {
@@ -24,6 +23,5 @@ export default {
'no-raw-absolute-path': noRawAbsolutePath,
'no-antd-components': noAntdComponents,
'no-signozhq-ui-barrel': noSignozhqUiBarrel,
'no-css-module-bracket-access': noCssModuleBracketAccess,
},
};

696
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -64,17 +64,10 @@ export const TraceDetail = Loadable(
),
);
export const TraceDetailOldRedirect = Loadable(
() =>
import(
/* webpackChunkName: "TraceDetailOldRedirect" */ 'pages/TraceDetailOldRedirect/index'
),
);
export const TraceDetailV3 = Loadable(
() =>
import(
/* webpackChunkName: "TraceDetailV3 Page" */ 'pages/TraceDetailsV3/index'
/* webpackChunkName: "TraceDetailV3 Page" */ 'pages/TraceDetailV3Page/index'
),
);

View File

@@ -47,7 +47,7 @@ import {
SomethingWentWrong,
StatusPage,
SupportPage,
TraceDetailOldRedirect,
TraceDetail,
TraceDetailV3,
TraceFilter,
TracesExplorer,
@@ -139,11 +139,13 @@ const routes: AppRoutes[] = [
exact: true,
key: 'LOGS_SAVE_VIEWS',
},
// Legacy /trace-old/:id redirects to the current /trace/:id view.
// V3 trace details is gated until release: /trace serves V2 (public),
// /trace-old serves V3 (URL-only access). Flip the two `component`
// values back to release V3.
{
path: ROUTES.TRACE_DETAIL_OLD,
exact: true,
component: TraceDetailOldRedirect,
component: TraceDetail,
isPrivate: true,
key: 'TRACE_DETAIL_OLD',
},

View File

@@ -55,9 +55,6 @@ import type {
ThreadDetailResponseDTO,
ThreadListResponseDTO,
ThreadSummaryDTO,
ChipDTO,
ChipsResponseDTO,
PageTypeDTO,
ToolCallEventDTO,
ToolResultEventDTO,
} from './sigNozAIAssistantAPI.schemas';
@@ -544,19 +541,3 @@ export async function submitFeedback(
comment: comment ?? null,
});
}
// ---------------------------------------------------------------------------
// Contextual empty-state chips
// GET /api/v1/assistant/empty-state/chips?page_type=… → { chips }
// ---------------------------------------------------------------------------
export async function getEmptyStateChips(
pageType: PageTypeDTO,
signal?: AbortSignal,
): Promise<ChipDTO[]> {
const response = await AIAssistantInstance.get<ChipsResponseDTO>(
'/empty-state/chips',
{ params: { page_type: pageType }, signal },
);
return response.data.chips;
}

View File

@@ -26,7 +26,6 @@ import type {
CancelApiV1AssistantCancelPostHeaders,
CancelRequestDTO,
CancelResponseDTO,
ChipsResponseDTO,
ClarifyApiV1AssistantClarifyPostHeaders,
ClarifyRequestDTO,
ClarifyResponseDTO,
@@ -40,11 +39,8 @@ import type {
ErrorResponseDTO,
FeedbackRequestDTO,
FeedbackResponseDTO,
GetChipsApiV1AssistantEmptyStateChipsGetHeaders,
GetChipsApiV1AssistantEmptyStateChipsGetParams,
GetThreadApiV1AssistantThreadsThreadIdGetHeaders,
GetThreadApiV1AssistantThreadsThreadIdGetPathParameters,
GetUsageApiV1AssistantUsageGetHeaders,
HTTPValidationErrorDTO,
HealthResponseDTO,
ListThreadsApiV1AssistantThreadsGetHeaders,
@@ -69,89 +65,93 @@ import type {
UpdateThreadApiV1AssistantThreadsThreadIdPatchHeaders,
UpdateThreadApiV1AssistantThreadsThreadIdPatchPathParameters,
UpdateThreadRequestDTO,
UsageResponseDTO,
} from './sigNozAIAssistantAPI.schemas';
import { GeneratedAPIInstance } from '../generatedAPIInstance';
import {
GeneratedAPIInstance,
getGeneratedAPIQueryKeyHeaders,
} from '../generatedAPIInstance';
import type { ErrorType, BodyType } from '../generatedAPIInstance';
/**
* @summary Healthz
* @summary Health
*/
export const healthzHealthzGet = (signal?: AbortSignal) => {
export const healthHealthGet = (signal?: AbortSignal) => {
return GeneratedAPIInstance<HealthResponseDTO>({
url: `/healthz`,
url: `/health`,
method: 'GET',
signal,
});
};
export const getHealthzHealthzGetQueryKey = () => {
return [`/healthz`] as const;
export const getHealthHealthGetQueryKey = () => {
return [`/health`] as const;
};
export const getHealthzHealthzGetQueryOptions = <
TData = Awaited<ReturnType<typeof healthzHealthzGet>>,
export const getHealthHealthGetQueryOptions = <
TData = Awaited<ReturnType<typeof healthHealthGet>>,
TError = ErrorType<unknown>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof healthzHealthzGet>>,
Awaited<ReturnType<typeof healthHealthGet>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getHealthzHealthzGetQueryKey();
const queryKey = queryOptions?.queryKey ?? getHealthHealthGetQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof healthzHealthzGet>>
> = ({ signal }) => healthzHealthzGet(signal);
const queryFn: QueryFunction<Awaited<ReturnType<typeof healthHealthGet>>> = ({
signal,
}) => healthHealthGet(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof healthzHealthzGet>>,
Awaited<ReturnType<typeof healthHealthGet>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type HealthzHealthzGetQueryResult = NonNullable<
Awaited<ReturnType<typeof healthzHealthzGet>>
export type HealthHealthGetQueryResult = NonNullable<
Awaited<ReturnType<typeof healthHealthGet>>
>;
export type HealthzHealthzGetQueryError = ErrorType<unknown>;
export type HealthHealthGetQueryError = ErrorType<unknown>;
/**
* @summary Healthz
* @summary Health
*/
export function useHealthzHealthzGet<
TData = Awaited<ReturnType<typeof healthzHealthzGet>>,
export function useHealthHealthGet<
TData = Awaited<ReturnType<typeof healthHealthGet>>,
TError = ErrorType<unknown>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof healthzHealthzGet>>,
Awaited<ReturnType<typeof healthHealthGet>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getHealthzHealthzGetQueryOptions(options);
const queryOptions = getHealthHealthGetQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Healthz
* @summary Health
*/
export const invalidateHealthzHealthzGet = async (
export const invalidateHealthHealthGet = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getHealthzHealthzGetQueryKey() },
{ queryKey: getHealthHealthGetQueryKey() },
options,
);
@@ -159,82 +159,84 @@ export const invalidateHealthzHealthzGet = async (
};
/**
* @summary Readyz
* @summary Ready
*/
export const readyzReadyzGet = (signal?: AbortSignal) => {
export const readyReadyGet = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ReadinessResponseDTO>({
url: `/readyz`,
url: `/ready`,
method: 'GET',
signal,
});
};
export const getReadyzReadyzGetQueryKey = () => {
return [`/readyz`] as const;
export const getReadyReadyGetQueryKey = () => {
return [`/ready`] as const;
};
export const getReadyzReadyzGetQueryOptions = <
TData = Awaited<ReturnType<typeof readyzReadyzGet>>,
export const getReadyReadyGetQueryOptions = <
TData = Awaited<ReturnType<typeof readyReadyGet>>,
TError = ErrorType<ReadinessResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof readyzReadyzGet>>,
Awaited<ReturnType<typeof readyReadyGet>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getReadyzReadyzGetQueryKey();
const queryKey = queryOptions?.queryKey ?? getReadyReadyGetQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof readyzReadyzGet>>> = ({
const queryFn: QueryFunction<Awaited<ReturnType<typeof readyReadyGet>>> = ({
signal,
}) => readyzReadyzGet(signal);
}) => readyReadyGet(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof readyzReadyzGet>>,
Awaited<ReturnType<typeof readyReadyGet>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ReadyzReadyzGetQueryResult = NonNullable<
Awaited<ReturnType<typeof readyzReadyzGet>>
export type ReadyReadyGetQueryResult = NonNullable<
Awaited<ReturnType<typeof readyReadyGet>>
>;
export type ReadyzReadyzGetQueryError = ErrorType<ReadinessResponseDTO>;
export type ReadyReadyGetQueryError = ErrorType<ReadinessResponseDTO>;
/**
* @summary Readyz
* @summary Ready
*/
export function useReadyzReadyzGet<
TData = Awaited<ReturnType<typeof readyzReadyzGet>>,
export function useReadyReadyGet<
TData = Awaited<ReturnType<typeof readyReadyGet>>,
TError = ErrorType<ReadinessResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof readyzReadyzGet>>,
Awaited<ReturnType<typeof readyReadyGet>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getReadyzReadyzGetQueryOptions(options);
const queryOptions = getReadyReadyGetQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Readyz
* @summary Ready
*/
export const invalidateReadyzReadyzGet = async (
export const invalidateReadyReadyGet = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getReadyzReadyzGetQueryKey() },
{ queryKey: getReadyReadyGetQueryKey() },
options,
);
@@ -245,7 +247,7 @@ export const invalidateReadyzReadyzGet = async (
* @summary Create a new thread
*/
export const createThreadApiV1AssistantThreadsPost = (
createThreadApiV1AssistantThreadsPostBody?: BodyType<CreateThreadApiV1AssistantThreadsPostBody>,
createThreadApiV1AssistantThreadsPostBody: BodyType<CreateThreadApiV1AssistantThreadsPostBody>,
headers?: CreateThreadApiV1AssistantThreadsPostHeaders,
signal?: AbortSignal,
) => {
@@ -266,7 +268,7 @@ export const getCreateThreadApiV1AssistantThreadsPostMutationOptions = <
Awaited<ReturnType<typeof createThreadApiV1AssistantThreadsPost>>,
TError,
{
data?: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
data: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
headers?: CreateThreadApiV1AssistantThreadsPostHeaders;
},
TContext
@@ -275,7 +277,7 @@ export const getCreateThreadApiV1AssistantThreadsPostMutationOptions = <
Awaited<ReturnType<typeof createThreadApiV1AssistantThreadsPost>>,
TError,
{
data?: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
data: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
headers?: CreateThreadApiV1AssistantThreadsPostHeaders;
},
TContext
@@ -292,7 +294,7 @@ export const getCreateThreadApiV1AssistantThreadsPostMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createThreadApiV1AssistantThreadsPost>>,
{
data?: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
data: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
headers?: CreateThreadApiV1AssistantThreadsPostHeaders;
}
> = (props) => {
@@ -308,8 +310,7 @@ export type CreateThreadApiV1AssistantThreadsPostMutationResult = NonNullable<
Awaited<ReturnType<typeof createThreadApiV1AssistantThreadsPost>>
>;
export type CreateThreadApiV1AssistantThreadsPostMutationBody =
| BodyType<CreateThreadApiV1AssistantThreadsPostBody>
| undefined;
BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
export type CreateThreadApiV1AssistantThreadsPostMutationError = ErrorType<
ErrorResponseDTO | HTTPValidationErrorDTO
>;
@@ -325,7 +326,7 @@ export const useCreateThreadApiV1AssistantThreadsPost = <
Awaited<ReturnType<typeof createThreadApiV1AssistantThreadsPost>>,
TError,
{
data?: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
data: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
headers?: CreateThreadApiV1AssistantThreadsPostHeaders;
},
TContext
@@ -334,14 +335,15 @@ export const useCreateThreadApiV1AssistantThreadsPost = <
Awaited<ReturnType<typeof createThreadApiV1AssistantThreadsPost>>,
TError,
{
data?: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
data: BodyType<CreateThreadApiV1AssistantThreadsPostBody>;
headers?: CreateThreadApiV1AssistantThreadsPostHeaders;
},
TContext
> => {
return useMutation(
getCreateThreadApiV1AssistantThreadsPostMutationOptions(options),
);
const mutationOptions =
getCreateThreadApiV1AssistantThreadsPostMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Cursor-based pagination, sorted by updatedAt desc. Use `archived=true|false|all` to filter.
@@ -363,8 +365,13 @@ export const listThreadsApiV1AssistantThreadsGet = (
export const getListThreadsApiV1AssistantThreadsGetQueryKey = (
params?: ListThreadsApiV1AssistantThreadsGetParams,
headers?: ListThreadsApiV1AssistantThreadsGetHeaders,
) => {
return [`/api/v1/assistant/threads`, ...(params ? [params] : [])] as const;
return [
`/api/v1/assistant/threads`,
...(params ? [params] : []),
...getGeneratedAPIQueryKeyHeaders(headers),
] as const;
};
export const getListThreadsApiV1AssistantThreadsGetQueryOptions = <
@@ -385,7 +392,7 @@ export const getListThreadsApiV1AssistantThreadsGetQueryOptions = <
const queryKey =
queryOptions?.queryKey ??
getListThreadsApiV1AssistantThreadsGetQueryKey(params);
getListThreadsApiV1AssistantThreadsGetQueryKey(params, headers);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listThreadsApiV1AssistantThreadsGet>>
@@ -434,7 +441,9 @@ export function useListThreadsApiV1AssistantThreadsGet<
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
query.queryKey = queryOptions.queryKey;
return query;
}
/**
@@ -447,7 +456,7 @@ export const invalidateListThreadsApiV1AssistantThreadsGet = async (
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListThreadsApiV1AssistantThreadsGetQueryKey(params) },
{ queryKey: getListThreadsApiV1AssistantThreadsGetQueryKey(params, headers) },
options,
);
@@ -471,10 +480,14 @@ export const getThreadApiV1AssistantThreadsThreadIdGet = (
});
};
export const getGetThreadApiV1AssistantThreadsThreadIdGetQueryKey = ({
threadId,
}: GetThreadApiV1AssistantThreadsThreadIdGetPathParameters) => {
return [`/api/v1/assistant/threads/${threadId}`] as const;
export const getGetThreadApiV1AssistantThreadsThreadIdGetQueryKey = (
{ threadId }: GetThreadApiV1AssistantThreadsThreadIdGetPathParameters,
headers?: GetThreadApiV1AssistantThreadsThreadIdGetHeaders,
) => {
return [
`/api/v1/assistant/threads/${threadId}`,
...getGeneratedAPIQueryKeyHeaders(headers),
] as const;
};
export const getGetThreadApiV1AssistantThreadsThreadIdGetQueryOptions = <
@@ -495,7 +508,7 @@ export const getGetThreadApiV1AssistantThreadsThreadIdGetQueryOptions = <
const queryKey =
queryOptions?.queryKey ??
getGetThreadApiV1AssistantThreadsThreadIdGetQueryKey({ threadId });
getGetThreadApiV1AssistantThreadsThreadIdGetQueryKey({ threadId }, headers);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getThreadApiV1AssistantThreadsThreadIdGet>>
@@ -549,7 +562,9 @@ export function useGetThreadApiV1AssistantThreadsThreadIdGet<
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
query.queryKey = queryOptions.queryKey;
return query;
}
/**
@@ -563,7 +578,10 @@ export const invalidateGetThreadApiV1AssistantThreadsThreadIdGet = async (
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{
queryKey: getGetThreadApiV1AssistantThreadsThreadIdGetQueryKey({ threadId }),
queryKey: getGetThreadApiV1AssistantThreadsThreadIdGetQueryKey(
{ threadId },
headers,
),
},
options,
);
@@ -578,14 +596,12 @@ export const updateThreadApiV1AssistantThreadsThreadIdPatch = (
{ threadId }: UpdateThreadApiV1AssistantThreadsThreadIdPatchPathParameters,
updateThreadRequestDTO: BodyType<UpdateThreadRequestDTO>,
headers?: UpdateThreadApiV1AssistantThreadsThreadIdPatchHeaders,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ThreadSummaryDTO>({
url: `/api/v1/assistant/threads/${threadId}`,
method: 'PATCH',
headers: { 'Content-Type': 'application/json', ...headers },
data: updateThreadRequestDTO,
signal,
});
};
@@ -679,9 +695,10 @@ export const useUpdateThreadApiV1AssistantThreadsThreadIdPatch = <
},
TContext
> => {
return useMutation(
getUpdateThreadApiV1AssistantThreadsThreadIdPatchMutationOptions(options),
);
const mutationOptions =
getUpdateThreadApiV1AssistantThreadsThreadIdPatchMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Persists the user message, creates an execution (state: queued), kicks off the agent loop asynchronously, and returns immediately. Open `GET /executions/{executionId}/events` for the SSE stream.
@@ -808,11 +825,12 @@ export const useCreateMessageApiV1AssistantThreadsThreadIdMessagesPost = <
},
TContext
> => {
return useMutation(
const mutationOptions =
getCreateMessageApiV1AssistantThreadsThreadIdMessagesPostMutationOptions(
options,
),
);
);
return useMutation(mutationOptions);
};
/**
* Clean-slate regeneration. Starts a fresh execution with conversation history up to (excluding) the original assistant response.
@@ -943,11 +961,12 @@ export const useRegenerateMessageApiV1AssistantMessagesMessageIdRegeneratePost =
},
TContext
> => {
return useMutation(
const mutationOptions =
getRegenerateMessageApiV1AssistantMessagesMessageIdRegeneratePostMutationOptions(
options,
),
);
);
return useMutation(mutationOptions);
};
/**
* Triggers a replay execution that runs the stored tool call with exact params. Returns a new executionId — open SSE for that execution.
@@ -1047,9 +1066,10 @@ export const useApproveApiV1AssistantApprovePost = <
},
TContext
> => {
return useMutation(
getApproveApiV1AssistantApprovePostMutationOptions(options),
);
const mutationOptions =
getApproveApiV1AssistantApprovePostMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Marks the approval as rejected. The execution completes with no tool execution.
@@ -1149,7 +1169,10 @@ export const useRejectApiV1AssistantRejectPost = <
},
TContext
> => {
return useMutation(getRejectApiV1AssistantRejectPostMutationOptions(options));
const mutationOptions =
getRejectApiV1AssistantRejectPostMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Provides structured answers to a clarification request. Persists the answers as a user transcript message, emits `user_message` as the first replayable event on the new execution stream, and resumes the agent with the answers as tool results.
@@ -1249,9 +1272,10 @@ export const useClarifyApiV1AssistantClarifyPost = <
},
TContext
> => {
return useMutation(
getClarifyApiV1AssistantClarifyPostMutationOptions(options),
);
const mutationOptions =
getClarifyApiV1AssistantClarifyPostMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Cooperative cancel. The agent loop finishes its current step, emits a truncated message if streaming, and transitions to canceled.
@@ -1351,7 +1375,10 @@ export const useCancelApiV1AssistantCancelPost = <
},
TContext
> => {
return useMutation(getCancelApiV1AssistantCancelPostMutationOptions(options));
const mutationOptions =
getCancelApiV1AssistantCancelPostMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Deletes the resource that was created by the assistant.
@@ -1450,7 +1477,9 @@ export const useUndoApiV1AssistantUndoPost = <
},
TContext
> => {
return useMutation(getUndoApiV1AssistantUndoPostMutationOptions(options));
const mutationOptions = getUndoApiV1AssistantUndoPostMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Rolls back the resource to its pre-change snapshot.
@@ -1550,7 +1579,10 @@ export const useRevertApiV1AssistantRevertPost = <
},
TContext
> => {
return useMutation(getRevertApiV1AssistantRevertPostMutationOptions(options));
const mutationOptions =
getRevertApiV1AssistantRevertPostMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Recreates the resource from its pre-delete snapshot.
@@ -1650,9 +1682,10 @@ export const useRestoreApiV1AssistantRestorePost = <
},
TContext
> => {
return useMutation(
getRestoreApiV1AssistantRestorePostMutationOptions(options),
);
const mutationOptions =
getRestoreApiV1AssistantRestorePostMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* @summary Submit feedback on an assistant message
@@ -1778,221 +1811,10 @@ export const useSubmitFeedbackApiV1AssistantMessagesMessageIdFeedbackPost = <
},
TContext
> => {
return useMutation(
const mutationOptions =
getSubmitFeedbackApiV1AssistantMessagesMessageIdFeedbackPostMutationOptions(
options,
),
);
};
/**
* @summary Current rate-limit usage for the authenticated user + org
*/
export const getUsageApiV1AssistantUsageGet = (
headers?: GetUsageApiV1AssistantUsageGetHeaders,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<UsageResponseDTO>({
url: `/api/v1/assistant/usage`,
method: 'GET',
headers,
signal,
});
};
export const getGetUsageApiV1AssistantUsageGetQueryKey = () => {
return [`/api/v1/assistant/usage`] as const;
};
export const getGetUsageApiV1AssistantUsageGetQueryOptions = <
TData = Awaited<ReturnType<typeof getUsageApiV1AssistantUsageGet>>,
TError = ErrorType<ErrorResponseDTO | HTTPValidationErrorDTO>,
>(
headers?: GetUsageApiV1AssistantUsageGetHeaders,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getUsageApiV1AssistantUsageGet>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetUsageApiV1AssistantUsageGetQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getUsageApiV1AssistantUsageGet>>
> = ({ signal }) => getUsageApiV1AssistantUsageGet(headers, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getUsageApiV1AssistantUsageGet>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetUsageApiV1AssistantUsageGetQueryResult = NonNullable<
Awaited<ReturnType<typeof getUsageApiV1AssistantUsageGet>>
>;
export type GetUsageApiV1AssistantUsageGetQueryError = ErrorType<
ErrorResponseDTO | HTTPValidationErrorDTO
>;
/**
* @summary Current rate-limit usage for the authenticated user + org
*/
export function useGetUsageApiV1AssistantUsageGet<
TData = Awaited<ReturnType<typeof getUsageApiV1AssistantUsageGet>>,
TError = ErrorType<ErrorResponseDTO | HTTPValidationErrorDTO>,
>(
headers?: GetUsageApiV1AssistantUsageGetHeaders,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getUsageApiV1AssistantUsageGet>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetUsageApiV1AssistantUsageGetQueryOptions(
headers,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Current rate-limit usage for the authenticated user + org
*/
export const invalidateGetUsageApiV1AssistantUsageGet = async (
queryClient: QueryClient,
headers?: GetUsageApiV1AssistantUsageGetHeaders,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetUsageApiV1AssistantUsageGetQueryKey() },
options,
);
return queryClient;
};
/**
* @summary Contextual empty-state chips
*/
export const getChipsApiV1AssistantEmptyStateChipsGet = (
params: GetChipsApiV1AssistantEmptyStateChipsGetParams,
headers?: GetChipsApiV1AssistantEmptyStateChipsGetHeaders,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ChipsResponseDTO>({
url: `/api/v1/assistant/empty-state/chips`,
method: 'GET',
headers,
params,
signal,
});
};
export const getGetChipsApiV1AssistantEmptyStateChipsGetQueryKey = (
params?: GetChipsApiV1AssistantEmptyStateChipsGetParams,
) => {
return [
`/api/v1/assistant/empty-state/chips`,
...(params ? [params] : []),
] as const;
};
export const getGetChipsApiV1AssistantEmptyStateChipsGetQueryOptions = <
TData = Awaited<ReturnType<typeof getChipsApiV1AssistantEmptyStateChipsGet>>,
TError = ErrorType<ErrorResponseDTO | HTTPValidationErrorDTO>,
>(
params: GetChipsApiV1AssistantEmptyStateChipsGetParams,
headers?: GetChipsApiV1AssistantEmptyStateChipsGetHeaders,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getChipsApiV1AssistantEmptyStateChipsGet>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetChipsApiV1AssistantEmptyStateChipsGetQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getChipsApiV1AssistantEmptyStateChipsGet>>
> = ({ signal }) =>
getChipsApiV1AssistantEmptyStateChipsGet(params, headers, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getChipsApiV1AssistantEmptyStateChipsGet>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetChipsApiV1AssistantEmptyStateChipsGetQueryResult = NonNullable<
Awaited<ReturnType<typeof getChipsApiV1AssistantEmptyStateChipsGet>>
>;
export type GetChipsApiV1AssistantEmptyStateChipsGetQueryError = ErrorType<
ErrorResponseDTO | HTTPValidationErrorDTO
>;
/**
* @summary Contextual empty-state chips
*/
export function useGetChipsApiV1AssistantEmptyStateChipsGet<
TData = Awaited<ReturnType<typeof getChipsApiV1AssistantEmptyStateChipsGet>>,
TError = ErrorType<ErrorResponseDTO | HTTPValidationErrorDTO>,
>(
params: GetChipsApiV1AssistantEmptyStateChipsGetParams,
headers?: GetChipsApiV1AssistantEmptyStateChipsGetHeaders,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getChipsApiV1AssistantEmptyStateChipsGet>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetChipsApiV1AssistantEmptyStateChipsGetQueryOptions(
params,
headers,
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Contextual empty-state chips
*/
export const invalidateGetChipsApiV1AssistantEmptyStateChipsGet = async (
queryClient: QueryClient,
params: GetChipsApiV1AssistantEmptyStateChipsGetParams,
headers?: GetChipsApiV1AssistantEmptyStateChipsGetHeaders,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetChipsApiV1AssistantEmptyStateChipsGetQueryKey(params) },
options,
);
return queryClient;
);
return useMutation(mutationOptions);
};

View File

@@ -18,20 +18,15 @@ import type {
} from 'react-query';
import type {
CloneDashboardV2201,
CloneDashboardV2PathParameters,
CreateDashboardV2201,
CreateDashboardView201,
CreatePublicDashboard201,
CreatePublicDashboardPathParameters,
DashboardtypesPatchableDashboardV2DTO,
DashboardtypesPostableDashboardV2DTO,
DashboardtypesPostableDashboardViewDTO,
DashboardtypesPostablePublicDashboardDTO,
DashboardtypesUpdatableDashboardV2DTO,
DashboardtypesUpdatablePublicDashboardDTO,
DeleteDashboardV2PathParameters,
DeleteDashboardViewPathParameters,
DeletePublicDashboardPathParameters,
GetDashboardV2200,
GetDashboardV2PathParameters,
@@ -41,7 +36,6 @@ import type {
GetPublicDashboardPathParameters,
GetPublicDashboardWidgetQueryRange200,
GetPublicDashboardWidgetQueryRangePathParameters,
ListDashboardViews200,
ListDashboardsForUserV2200,
ListDashboardsForUserV2Params,
ListDashboardsV2200,
@@ -55,8 +49,6 @@ import type {
UnpinDashboardV2PathParameters,
UpdateDashboardV2200,
UpdateDashboardV2PathParameters,
UpdateDashboardView200,
UpdateDashboardViewPathParameters,
UpdatePublicDashboardPathParameters,
} from '../sigNoz.schemas';
@@ -71,7 +63,7 @@ export const deletePublicDashboard = (
{ id }: DeletePublicDashboardPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
return GeneratedAPIInstance<string>({
url: `/api/v1/dashboards/${id}/public`,
method: 'DELETE',
signal,
@@ -354,7 +346,7 @@ export const updatePublicDashboard = (
dashboardtypesUpdatablePublicDashboardDTO?: BodyType<DashboardtypesUpdatablePublicDashboardDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
return GeneratedAPIInstance<string>({
url: `/api/v1/dashboards/${id}/public`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
@@ -656,354 +648,6 @@ export const invalidateGetPublicDashboardWidgetQueryRange = async (
return queryClient;
};
/**
* Returns every saved view in the calling user's org. Saved views are shared org-wide.
* @summary List dashboard saved views
*/
export const listDashboardViews = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListDashboardViews200>({
url: `/api/v2/dashboard_views`,
method: 'GET',
signal,
});
};
export const getListDashboardViewsQueryKey = () => {
return [`/api/v2/dashboard_views`] as const;
};
export const getListDashboardViewsQueryOptions = <
TData = Awaited<ReturnType<typeof listDashboardViews>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listDashboardViews>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListDashboardViewsQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listDashboardViews>>
> = ({ signal }) => listDashboardViews(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listDashboardViews>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListDashboardViewsQueryResult = NonNullable<
Awaited<ReturnType<typeof listDashboardViews>>
>;
export type ListDashboardViewsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List dashboard saved views
*/
export function useListDashboardViews<
TData = Awaited<ReturnType<typeof listDashboardViews>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listDashboardViews>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListDashboardViewsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List dashboard saved views
*/
export const invalidateListDashboardViews = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListDashboardViewsQueryKey() },
options,
);
return queryClient;
};
/**
* Persists the calling user's dashboard listing state (query, sort, order) as a named, reusable view shared across the org.
* @summary Create dashboard saved view
*/
export const createDashboardView = (
dashboardtypesPostableDashboardViewDTO?: BodyType<DashboardtypesPostableDashboardViewDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateDashboardView201>({
url: `/api/v2/dashboard_views`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: dashboardtypesPostableDashboardViewDTO,
signal,
});
};
export const getCreateDashboardViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDashboardView>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createDashboardView>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> },
TContext
> => {
const mutationKey = ['createDashboardView'];
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 createDashboardView>>,
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> }
> = (props) => {
const { data } = props ?? {};
return createDashboardView(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateDashboardViewMutationResult = NonNullable<
Awaited<ReturnType<typeof createDashboardView>>
>;
export type CreateDashboardViewMutationBody =
| BodyType<DashboardtypesPostableDashboardViewDTO>
| undefined;
export type CreateDashboardViewMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create dashboard saved view
*/
export const useCreateDashboardView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDashboardView>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createDashboardView>>,
TError,
{ data?: BodyType<DashboardtypesPostableDashboardViewDTO> },
TContext
> => {
return useMutation(getCreateDashboardViewMutationOptions(options));
};
/**
* Removes a saved view. Saved views are shared org-wide. Deleting a non-existent view returns 404.
* @summary Delete dashboard saved view
*/
export const deleteDashboardView = (
{ id }: DeleteDashboardViewPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/dashboard_views/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteDashboardViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteDashboardView>>,
TError,
{ pathParams: DeleteDashboardViewPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteDashboardView>>,
TError,
{ pathParams: DeleteDashboardViewPathParameters },
TContext
> => {
const mutationKey = ['deleteDashboardView'];
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 deleteDashboardView>>,
{ pathParams: DeleteDashboardViewPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteDashboardView(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteDashboardViewMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteDashboardView>>
>;
export type DeleteDashboardViewMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete dashboard saved view
*/
export const useDeleteDashboardView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteDashboardView>>,
TError,
{ pathParams: DeleteDashboardViewPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteDashboardView>>,
TError,
{ pathParams: DeleteDashboardViewPathParameters },
TContext
> => {
return useMutation(getDeleteDashboardViewMutationOptions(options));
};
/**
* Replaces a saved view's name and data. Saved views are shared org-wide.
* @summary Update dashboard saved view
*/
export const updateDashboardView = (
{ id }: UpdateDashboardViewPathParameters,
dashboardtypesPostableDashboardViewDTO?: BodyType<DashboardtypesPostableDashboardViewDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<UpdateDashboardView200>({
url: `/api/v2/dashboard_views/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: dashboardtypesPostableDashboardViewDTO,
signal,
});
};
export const getUpdateDashboardViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateDashboardView>>,
TError,
{
pathParams: UpdateDashboardViewPathParameters;
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateDashboardView>>,
TError,
{
pathParams: UpdateDashboardViewPathParameters;
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
},
TContext
> => {
const mutationKey = ['updateDashboardView'];
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 updateDashboardView>>,
{
pathParams: UpdateDashboardViewPathParameters;
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateDashboardView(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateDashboardViewMutationResult = NonNullable<
Awaited<ReturnType<typeof updateDashboardView>>
>;
export type UpdateDashboardViewMutationBody =
| BodyType<DashboardtypesPostableDashboardViewDTO>
| undefined;
export type UpdateDashboardViewMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update dashboard saved view
*/
export const useUpdateDashboardView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateDashboardView>>,
TError,
{
pathParams: UpdateDashboardViewPathParameters;
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateDashboardView>>,
TError,
{
pathParams: UpdateDashboardViewPathParameters;
data?: BodyType<DashboardtypesPostableDashboardViewDTO>;
},
TContext
> => {
return useMutation(getUpdateDashboardViewMutationOptions(options));
};
/**
* Returns a page of v2-shape dashboards for the org. This is the pure, user-independent list — it carries no pin state. Use ListDashboardsForUserV2 for the personalized, pin-aware list. Supports a filter DSL (`query`), sort (`updated_at`/`created_at`/`name`), order (`asc`/`desc`), and offset-based pagination (`limit`/`offset`).
* @summary List dashboards (v2)
@@ -1192,7 +836,7 @@ export const deleteDashboardV2 = (
{ id }: DeleteDashboardV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
return GeneratedAPIInstance<string>({
url: `/api/v2/dashboards/${id}`,
method: 'DELETE',
signal,
@@ -1562,85 +1206,6 @@ export const useUpdateDashboardV2 = <
> => {
return useMutation(getUpdateDashboardV2MutationOptions(options));
};
/**
* This endpoint clones an existing v2-shape dashboard. User and integration dashboards can be cloned; system dashboards are rejected. The clone keeps the source's display name, panels, and tags, but gets a freshly generated unique internal name and is always created as an unlocked user dashboard owned by the caller.
* @summary Clone dashboard (v2)
*/
export const cloneDashboardV2 = (
{ id }: CloneDashboardV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CloneDashboardV2201>({
url: `/api/v2/dashboards/${id}/clone`,
method: 'POST',
signal,
});
};
export const getCloneDashboardV2MutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof cloneDashboardV2>>,
TError,
{ pathParams: CloneDashboardV2PathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof cloneDashboardV2>>,
TError,
{ pathParams: CloneDashboardV2PathParameters },
TContext
> => {
const mutationKey = ['cloneDashboardV2'];
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 cloneDashboardV2>>,
{ pathParams: CloneDashboardV2PathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return cloneDashboardV2(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type CloneDashboardV2MutationResult = NonNullable<
Awaited<ReturnType<typeof cloneDashboardV2>>
>;
export type CloneDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Clone dashboard (v2)
*/
export const useCloneDashboardV2 = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof cloneDashboardV2>>,
TError,
{ pathParams: CloneDashboardV2PathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof cloneDashboardV2>>,
TError,
{ pathParams: CloneDashboardV2PathParameters },
TContext
> => {
return useMutation(getCloneDashboardV2MutationOptions(options));
};
/**
* This endpoint unlocks a v2-shape dashboard. Only the dashboard's creator or an org admin may lock or unlock.
* @summary Unlock dashboard (v2)
@@ -1649,7 +1214,7 @@ export const unlockDashboardV2 = (
{ id }: UnlockDashboardV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
return GeneratedAPIInstance<string>({
url: `/api/v2/dashboards/${id}/lock`,
method: 'DELETE',
signal,
@@ -1728,7 +1293,7 @@ export const lockDashboardV2 = (
{ id }: LockDashboardV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
return GeneratedAPIInstance<string>({
url: `/api/v2/dashboards/${id}/lock`,
method: 'PUT',
signal,
@@ -1906,7 +1471,7 @@ export const unpinDashboardV2 = (
{ id }: UnpinDashboardV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
return GeneratedAPIInstance<string>({
url: `/api/v2/users/me/dashboards/${id}/pins`,
method: 'DELETE',
signal,
@@ -1985,7 +1550,7 @@ export const pinDashboardV2 = (
{ id }: PinDashboardV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
return GeneratedAPIInstance<string>({
url: `/api/v2/users/me/dashboards/${id}/pins`,
method: 'PUT',
signal,

View File

@@ -37,7 +37,7 @@ export const handleExportRawDataPOST = (
params?: HandleExportRawDataPOSTParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
return GeneratedAPIInstance<string>({
url: `/api/v1/export_raw_data`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },

View File

@@ -680,7 +680,7 @@ export const updateMetricMetadata = (
metricsexplorertypesUpdateMetricMetadataRequestDTO?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
return GeneratedAPIInstance<string>({
url: `/api/v2/metrics/${metricName}/metadata`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },

View File

@@ -203,7 +203,7 @@ export const deleteRole = (
{ id }: DeleteRolePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
return GeneratedAPIInstance<string>({
url: `/api/v1/roles/${id}`,
method: 'DELETE',
signal,
@@ -372,7 +372,7 @@ export const patchRole = (
authtypesPatchableRoleDTO?: BodyType<AuthtypesPatchableRoleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
return GeneratedAPIInstance<string>({
url: `/api/v1/roles/${id}`,
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
@@ -572,7 +572,7 @@ export const patchObjects = (
coretypesPatchableObjectsDTO?: BodyType<CoretypesPatchableObjectsDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
return GeneratedAPIInstance<string>({
url: `/api/v1/roles/${id}/relations/${relation}/objects`,
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },

View File

@@ -222,7 +222,7 @@ export const deleteServiceAccount = (
{ id }: DeleteServiceAccountPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
return GeneratedAPIInstance<string>({
url: `/api/v1/service_accounts/${id}`,
method: 'DELETE',
signal,
@@ -405,7 +405,7 @@ export const updateServiceAccount = (
serviceaccounttypesPostableServiceAccountDTO?: BodyType<ServiceaccounttypesPostableServiceAccountDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
return GeneratedAPIInstance<string>({
url: `/api/v1/service_accounts/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
@@ -707,7 +707,7 @@ export const revokeServiceAccountKey = (
{ id, fid }: RevokeServiceAccountKeyPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
return GeneratedAPIInstance<string>({
url: `/api/v1/service_accounts/${id}/keys/${fid}`,
method: 'DELETE',
signal,
@@ -788,7 +788,7 @@ export const updateServiceAccountKey = (
serviceaccounttypesUpdatableFactorAPIKeyDTO?: BodyType<ServiceaccounttypesUpdatableFactorAPIKeyDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
return GeneratedAPIInstance<string>({
url: `/api/v1/service_accounts/${id}/keys/${fid}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
@@ -1090,7 +1090,7 @@ export const deleteServiceAccountRole = (
{ id, rid }: DeleteServiceAccountRolePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
return GeneratedAPIInstance<string>({
url: `/api/v1/service_accounts/${id}/roles/${rid}`,
method: 'DELETE',
signal,
@@ -1254,7 +1254,7 @@ export const updateMyServiceAccount = (
serviceaccounttypesPostableServiceAccountDTO?: BodyType<ServiceaccounttypesPostableServiceAccountDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
return GeneratedAPIInstance<string>({
url: `/api/v1/service_accounts/me`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },

View File

@@ -2143,10 +2143,6 @@ export interface ErrorsResponseerroradditionalDTO {
* @type string
*/
message?: string;
/**
* @type array
*/
suggestions?: string[];
}
export interface ErrorsResponseretryjsonDTO {
@@ -2162,6 +2158,10 @@ export interface ErrorsJSONDTO {
* @type array
*/
errors?: ErrorsResponseerroradditionalDTO[];
/**
* @type array
*/
invalidReferences?: string[];
/**
* @type string
*/
@@ -2645,14 +2645,6 @@ export enum CloudintegrationtypesServiceIDDTO {
appservice = 'appservice',
containerapp = 'containerapp',
aks = 'aks',
sqldatabase = 'sqldatabase',
sqldatabasemi = 'sqldatabasemi',
mysqlflexibleserver = 'mysqlflexibleserver',
postgresqlflexibleserver = 'postgresqlflexibleserver',
mongodb = 'mongodb',
cosmosdb = 'cosmosdb',
cassandradb = 'cassandradb',
redis = 'redis',
}
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
/**
@@ -4633,54 +4625,6 @@ export interface DashboardtypesDashboardSpecDTO {
variables: DashboardtypesVariableDTO[];
}
export enum DashboardtypesListOrderDTO {
asc = 'asc',
desc = 'desc',
}
export enum DashboardtypesListSortDTO {
updated_at = 'updated_at',
created_at = 'created_at',
name = 'name',
}
export interface DashboardtypesDashboardViewDataDTO {
order?: DashboardtypesListOrderDTO;
/**
* @type string
*/
query?: string;
sort?: DashboardtypesListSortDTO;
/**
* @type string
*/
version: string;
}
export interface DashboardtypesDashboardViewDTO {
/**
* @type string
* @format date-time
*/
createdAt?: string;
data: DashboardtypesDashboardViewDataDTO;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
orgId: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
}
export enum DashboardtypesDatasourcePluginKindDTO {
'signoz/Datasource' = 'signoz/Datasource',
}
@@ -4792,6 +4736,15 @@ export interface DashboardtypesJSONPatchOperationDTO {
value?: unknown;
}
export enum DashboardtypesListOrderDTO {
asc = 'asc',
desc = 'desc',
}
export enum DashboardtypesListSortDTO {
updated_at = 'updated_at',
created_at = 'created_at',
name = 'name',
}
export interface DashboardtypesListedDashboardV2SpecDTO {
display?: DashboardtypesDisplayDTO;
}
@@ -4934,13 +4887,6 @@ export interface DashboardtypesListableDashboardV2DTO {
total: number;
}
export interface DashboardtypesListableDashboardViewDTO {
/**
* @type array
*/
views: DashboardtypesDashboardViewDTO[];
}
export enum DashboardtypesPanelPluginKindDTO {
'signoz/TimeSeriesPanel' = 'signoz/TimeSeriesPanel',
'signoz/BarChartPanel' = 'signoz/BarChartPanel',
@@ -4992,14 +4938,6 @@ export interface DashboardtypesPostableDashboardV2DTO {
tags: TagtypesPostableTagDTO[] | null;
}
export interface DashboardtypesPostableDashboardViewDTO {
data: DashboardtypesDashboardViewDataDTO;
/**
* @type string
*/
name: string;
}
export interface DashboardtypesPostablePublicDashboardDTO {
/**
* @type string
@@ -9798,19 +9736,6 @@ export type UpdateSpanMapperPathParameters = {
groupId: string;
mapperId: string;
};
export type GetStats200Data = { [key: string]: unknown };
export type GetStats200 = {
/**
* @type object
*/
data: GetStats200Data;
/**
* @type string
*/
status: string;
};
export type GetTraceAggregationsPathParameters = {
traceID: string;
};
@@ -9891,36 +9816,6 @@ export type GetUserPreference200 = {
export type UpdateUserPreferencePathParameters = {
name: string;
};
export type ListDashboardViews200 = {
data: DashboardtypesListableDashboardViewDTO;
/**
* @type string
*/
status: string;
};
export type CreateDashboardView201 = {
data: DashboardtypesDashboardViewDTO;
/**
* @type string
*/
status: string;
};
export type DeleteDashboardViewPathParameters = {
id: string;
};
export type UpdateDashboardViewPathParameters = {
id: string;
};
export type UpdateDashboardView200 = {
data: DashboardtypesDashboardViewDTO;
/**
* @type string
*/
status: string;
};
export type ListDashboardsV2Params = {
/**
* @type string
@@ -9999,17 +9894,6 @@ export type UpdateDashboardV2200 = {
status: string;
};
export type CloneDashboardV2PathParameters = {
id: string;
};
export type CloneDashboardV2201 = {
data: DashboardtypesGettableDashboardV2DTO;
/**
* @type string
*/
status: string;
};
export type UnlockDashboardV2PathParameters = {
id: string;
};

View File

@@ -1,96 +0,0 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
*/
import { useQuery } from 'react-query';
import type {
InvalidateOptions,
QueryClient,
QueryFunction,
QueryKey,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type { GetStats200, RenderErrorResponseDTO } from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType } from '../../../generatedAPIInstance';
/**
* This endpoint returns the collected stats for the organization
* @summary Get stats
*/
export const getStats = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetStats200>({
url: `/api/v1/stats`,
method: 'GET',
signal,
});
};
export const getGetStatsQueryKey = () => {
return [`/api/v1/stats`] as const;
};
export const getGetStatsQueryOptions = <
TData = Awaited<ReturnType<typeof getStats>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getStats>>, TError, TData>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetStatsQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getStats>>> = ({
signal,
}) => getStats(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getStats>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetStatsQueryResult = NonNullable<
Awaited<ReturnType<typeof getStats>>
>;
export type GetStatsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get stats
*/
export function useGetStats<
TData = Awaited<ReturnType<typeof getStats>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getStats>>, TError, TData>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetStatsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get stats
*/
export const invalidateGetStats = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetStatsQueryKey() },
options,
);
return queryClient;
};

View File

@@ -38,8 +38,8 @@ export enum LOCALSTORAGE {
DISSMISSED_COST_METER_INFO = 'DISMISSED_COST_METER_INFO',
DISMISSED_API_KEYS_DEPRECATION_BANNER = 'DISMISSED_API_KEYS_DEPRECATION_BANNER',
TRACE_DETAILS_SPAN_DETAILS_POSITION = 'TRACE_DETAILS_SPAN_DETAILS_POSITION',
LICENSE_KEY_CALLOUT_DISMISSED = 'LICENSE_KEY_CALLOUT_DISMISSED',
TRACE_DETAILS_PREFER_OLD_VIEW = 'TRACE_DETAILS_PREFER_OLD_VIEW',
LICENSE_KEY_CALLOUT_DISMISSED = 'LICENSE_KEY_CALLOUT_DISMISSED',
DASHBOARD_PREFERENCES = 'DASHBOARD_PREFERENCES',
ACTIVE_SIGNOZ_INSTANCE_URL = 'ACTIVE_SIGNOZ_INSTANCE_URL',
DASHBOARDS_LIST_VISIBLE_COLUMNS = 'DASHBOARDS_LIST_VISIBLE_COLUMNS',

View File

@@ -113,7 +113,4 @@ export const REACT_QUERY_KEY = {
// Fields Selector Query Keys
GET_FIELDS_SELECTOR_SUGGESTIONS: 'GET_FIELDS_SELECTOR_SUGGESTIONS',
// AI Assistant Query Keys
AI_ASSISTANT_EMPTY_STATE_CHIPS: 'AI_ASSISTANT_EMPTY_STATE_CHIPS',
} as const;

View File

@@ -63,50 +63,6 @@ describe('getAutoContexts', () => {
]);
});
it('resolves alert list tabs on /alerts', () => {
expect(getAutoContexts(ROUTES.LIST_ALL_ALERT, '')).toStrictEqual([
{
source: 'auto',
type: 'alert',
resourceId: null,
metadata: { page: 'alert_list' },
},
]);
expect(
getAutoContexts(ROUTES.LIST_ALL_ALERT, '?tab=AlertRules'),
).toStrictEqual([
{
source: 'auto',
type: 'alert',
resourceId: null,
metadata: { page: 'alert_list' },
},
]);
expect(
getAutoContexts(ROUTES.LIST_ALL_ALERT, '?tab=TriggeredAlerts'),
).toStrictEqual([
{
source: 'auto',
type: 'alert',
resourceId: null,
metadata: { page: 'alerts_triggered' },
},
]);
expect(
getAutoContexts(ROUTES.LIST_ALL_ALERT, '?tab=Configuration'),
).toStrictEqual([
{
source: 'auto',
type: 'alert',
resourceId: null,
metadata: { page: 'alert_list' },
},
]);
});
it('returns dashboard detail context on dashboard page', () => {
const dashboardId = 'dash-123';
const pathname = ROUTES.DASHBOARD.replace(':dashboardId', dashboardId);
@@ -130,21 +86,4 @@ describe('getAutoContexts', () => {
expect(contexts).toStrictEqual([]);
});
it('emits no auto-context on /home (no attachable resource)', () => {
expect(getAutoContexts(ROUTES.HOME, '')).toStrictEqual([]);
});
it('emits no auto-context on infrastructure monitoring routes', () => {
expect(
getAutoContexts(ROUTES.INFRASTRUCTURE_MONITORING_BASE, ''),
).toStrictEqual([]);
expect(
getAutoContexts(
ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
'?selectedItem=host-1',
),
).toStrictEqual([]);
});
});

View File

@@ -1,87 +0,0 @@
import { PageTypeDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { resolvePageType } from '../resolvePageType';
describe('resolvePageType', () => {
it('returns other for the standalone assistant surface', () => {
expect(
resolvePageType('/services', '', { isStandaloneAssistant: true }),
).toBe(PageTypeDTO.other);
});
it('returns dashboard_detail on a dashboard page', () => {
const pathname = ROUTES.DASHBOARD.replace(':dashboardId', 'dash-123');
expect(resolvePageType(pathname, '')).toBe(PageTypeDTO.dashboard_detail);
});
it('returns alerts_triggered on alert history without ruleId', () => {
expect(resolvePageType(ROUTES.ALERT_HISTORY, '')).toBe(
PageTypeDTO.alerts_triggered,
);
});
it('resolves alert list tabs on /alerts', () => {
expect(resolvePageType(ROUTES.LIST_ALL_ALERT, '')).toBe(
PageTypeDTO.alert_list,
);
expect(resolvePageType(ROUTES.LIST_ALL_ALERT, '?tab=AlertRules')).toBe(
PageTypeDTO.alert_list,
);
expect(resolvePageType(ROUTES.LIST_ALL_ALERT, '?tab=TriggeredAlerts')).toBe(
PageTypeDTO.alerts_triggered,
);
expect(resolvePageType(ROUTES.LIST_ALL_ALERT, '?tab=Configuration')).toBe(
PageTypeDTO.alert_list,
);
});
it('returns log_detail when logs explorer has activeLogId', () => {
const search = `?${QueryParams.activeLogId}=log-1`;
expect(resolvePageType(ROUTES.LOGS_EXPLORER, search)).toBe(
PageTypeDTO.log_detail,
);
});
it('returns other for unmapped routes', () => {
expect(resolvePageType(ROUTES.ALERT_OVERVIEW, '')).toBe(PageTypeDTO.other);
});
it('returns other for the app root route (no contextual mapping)', () => {
expect(resolvePageType(ROUTES.HOME_PAGE, '')).toBe(PageTypeDTO.other);
});
it('returns homepage on /home', () => {
expect(resolvePageType(ROUTES.HOME, '')).toBe(PageTypeDTO.homepage);
});
it('returns infra_entity_detail on infrastructure monitoring routes', () => {
expect(resolvePageType(ROUTES.INFRASTRUCTURE_MONITORING_BASE, '')).toBe(
PageTypeDTO.infra_entity_detail,
);
expect(resolvePageType(ROUTES.INFRASTRUCTURE_MONITORING_HOSTS, '')).toBe(
PageTypeDTO.infra_entity_detail,
);
expect(resolvePageType(ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES, '')).toBe(
PageTypeDTO.infra_entity_detail,
);
});
it('returns metrics_explorer on all metrics explorer routes', () => {
expect(resolvePageType(ROUTES.METRICS_EXPLORER_BASE, '')).toBe(
PageTypeDTO.metrics_explorer,
);
expect(resolvePageType(ROUTES.METRICS_EXPLORER, '')).toBe(
PageTypeDTO.metrics_explorer,
);
expect(resolvePageType(ROUTES.METRICS_EXPLORER_EXPLORER, '')).toBe(
PageTypeDTO.metrics_explorer,
);
expect(resolvePageType(ROUTES.METRICS_EXPLORER_VIEWS, '')).toBe(
PageTypeDTO.metrics_explorer,
);
});
});

View File

@@ -47,7 +47,14 @@ import { AIAssistantEvents, SuggestedPromptCategory } from '../../events';
import { useAIAssistantAnalyticsContext } from '../../hooks/useAIAssistantAnalyticsContext';
import { useAIAssistantStore } from '../../store/useAIAssistantStore';
import { openSavedViewByKey } from './utils/openSavedView';
import {
getPanelTypeForRequestType,
requestTypeFromActionQuery,
} from './utils/applyFilterPanelType';
import {
buildExplorerNavigationUrl,
openSavedViewByKey,
} from './utils/openSavedView';
import {
isSavedViewOpenAction,
resolveOpenResourceType,
@@ -323,48 +330,41 @@ function withDerivedFilterExpressions(query: Query): Query {
* the new URL on mount.
*/
function applyFilter(action: MessageActionDTO, deps: ApplyFilterDeps): void {
// eslint-disable-next-line no-console
console.log('[apply_filter] enter', {
signal: action.signal,
query: action.query,
pathname: deps.pathname,
});
if (!action.signal || !action.query) {
// eslint-disable-next-line no-console
console.warn('[apply_filter] bail: missing signal or query', action);
return;
}
const urlQuery = toUrlCompositeQuery(action.query as Record<string, unknown>);
if (!urlQuery) {
// eslint-disable-next-line no-console
console.warn(
'[apply_filter] bail: toUrlCompositeQuery returned null — agent payload shape unrecognized',
action.query,
);
return;
}
// `requestType` lives on the request envelope, which `toUrlCompositeQuery`
// drops — read it off the raw action query and translate it into the
// explorer panel type so a grouped/aggregated query opens as a table/graph
// instead of the default raw-log List view.
const panelType = getPanelTypeForRequestType(
requestTypeFromActionQuery(action.query as Record<string, unknown>),
);
const normalized = withDerivedFilterExpressions(urlQuery as unknown as Query);
// eslint-disable-next-line no-console
console.log('[apply_filter] normalized', normalized);
if (signalMatchesPathname(action.signal, deps.pathname)) {
// eslint-disable-next-line no-console
console.log('[apply_filter] on-page → handleSetQueryData + redirect');
normalized.builder.queryData.forEach((q, i) => {
deps.handleSetQueryData(i, q);
});
deps.redirectWithQueryBuilderData(normalized);
deps.redirectWithQueryBuilderData(normalized, {
[QueryParams.panelTypes]: panelType,
});
return;
}
const base = explorerRouteForSignal(action.signal);
if (!base) {
// eslint-disable-next-line no-console
console.warn('[apply_filter] bail: no route for signal', action.signal);
return;
}
// eslint-disable-next-line no-console
console.log('[apply_filter] off-page → history.push', base);
const encoded = encodeURIComponent(JSON.stringify(normalized));
deps.history.push(`${base}?${QueryParams.compositeQuery}=${encoded}`);
// Reuse the saved-view URL builder so the encoding (double-encoded
// compositeQuery + JSON-stringified panelTypes) matches what the explorer's
// URL parser expects — see useGetCompositeQueryParam / useGetPanelTypesQueryParam.
const url = buildExplorerNavigationUrl(base, normalized, {
[QueryParams.panelTypes]: panelType,
});
deps.history.push(url);
}
/** Picks the right rollback API call for a given action kind. */

View File

@@ -0,0 +1,75 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
getPanelTypeForRequestType,
requestTypeFromActionQuery,
} from '../applyFilterPanelType';
describe('getPanelTypeForRequestType', () => {
it('maps scalar (grouped aggregation) to the Table view', () => {
expect(getPanelTypeForRequestType('scalar')).toBe(PANEL_TYPES.TABLE);
});
it('maps time_series to the Time Series (graph) view', () => {
expect(getPanelTypeForRequestType('time_series')).toBe(
PANEL_TYPES.TIME_SERIES,
);
});
it('maps distribution (aggregation) to the Table view', () => {
expect(getPanelTypeForRequestType('distribution')).toBe(PANEL_TYPES.TABLE);
});
it('maps raw to the List view', () => {
expect(getPanelTypeForRequestType('raw')).toBe(PANEL_TYPES.LIST);
});
it.each([undefined, null, '', 'trace', 'nonsense', 42, {}])(
'defaults to the List view for raw/unknown/missing requestType (%p)',
(value) => {
expect(getPanelTypeForRequestType(value)).toBe(PANEL_TYPES.LIST);
},
);
});
describe('requestTypeFromActionQuery', () => {
it('reads the top-level requestType envelope field', () => {
expect(
requestTypeFromActionQuery({
requestType: 'scalar',
schemaVersion: 'v5',
compositeQuery: { queries: [] },
}),
).toBe('scalar');
});
it('returns undefined when the field or query is absent', () => {
expect(requestTypeFromActionQuery({})).toBeUndefined();
expect(requestTypeFromActionQuery(null)).toBeUndefined();
expect(requestTypeFromActionQuery(undefined)).toBeUndefined();
});
it('composes with getPanelTypeForRequestType for the reported bug payload', () => {
// The "log count by service" apply_filter payload from issue #304 follow-up:
// scalar + groupBy(service.name) must open the Table view, not List.
const query = {
requestType: 'scalar',
schemaVersion: 'v5',
compositeQuery: {
queries: [
{
type: 'builder_query',
spec: {
signal: 'logs',
aggregations: [{ expression: 'count()' }],
groupBy: [{ name: 'service.name' }],
},
},
],
},
};
expect(getPanelTypeForRequestType(requestTypeFromActionQuery(query))).toBe(
PANEL_TYPES.TABLE,
);
});
});

View File

@@ -221,6 +221,20 @@ describe('buildExplorerNavigationUrl', () => {
expect(url).toContain(`${QueryParams.compositeQuery}=`);
expect(url).toContain(`${QueryParams.viewKey}=`);
});
// Regression guard for the apply_filter view bug: the panel type must land
// on the URL JSON-encoded the way `useGetPanelTypesQueryParam` reads it
// (`JSON.parse` of the param), i.e. as the quoted string `"table"` ->
// `panelTypes=%22table%22`. Without this the explorer falls back to LIST.
it('JSON-encodes panelTypes so the explorer opens the right view', () => {
const url = buildExplorerNavigationUrl(
ROUTES.LOGS_EXPLORER,
{ queryType: 'builder' } as never,
{ [QueryParams.panelTypes]: PANEL_TYPES.TABLE },
);
expect(url).toContain(`${QueryParams.panelTypes}=%22table%22`);
});
});
describe('openSavedView', () => {

View File

@@ -0,0 +1,46 @@
import { REQUEST_TYPES } from 'api/v5/queryRange/constants';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { RequestType } from 'types/api/v5/queryRange';
/**
* Maps an apply_filter `query.requestType` to the explorer panel type so the
* Explorer opens in the view the query implies:
*
* - `scalar` -> Table (grouped aggregation, e.g. "count by service")
* - `distribution` -> Table (aggregation; Logs/Traces have no histogram view)
* - `time_series` -> Time Series (graph)
* - `raw` / other -> List (raw rows) [default]
*
* `trace` and the empty request type fall through to the List default on
* purpose — they are raw, ungrouped result sets.
*
* The agent emits `requestType` on the request envelope of `action.query`. It
* must be read off the raw action query *before* `toUrlCompositeQuery` maps the
* inner `compositeQuery` (that mapper keeps only the builder queries and drops
* the envelope). Without an explicit `panelTypes` URL param the Explorer falls
* back to `PANEL_TYPES.LIST` (see `useGetPanelTypesQueryParam`), so a grouped
* "count by service" query renders as a raw log list instead of a table.
*/
const REQUEST_TYPE_TO_PANEL_TYPE: Partial<Record<RequestType, PANEL_TYPES>> = {
[REQUEST_TYPES.SCALAR]: PANEL_TYPES.TABLE,
[REQUEST_TYPES.DISTRIBUTION]: PANEL_TYPES.TABLE,
[REQUEST_TYPES.TIME_SERIES]: PANEL_TYPES.TIME_SERIES,
[REQUEST_TYPES.RAW]: PANEL_TYPES.LIST,
};
export function getPanelTypeForRequestType(requestType: unknown): PANEL_TYPES {
if (typeof requestType === 'string') {
const mapped = REQUEST_TYPE_TO_PANEL_TYPE[requestType as RequestType];
if (mapped) {
return mapped;
}
}
return PANEL_TYPES.LIST;
}
/** Reads the `requestType` envelope field off a raw apply_filter query payload. */
export function requestTypeFromActionQuery(
query: Record<string, unknown> | null | undefined,
): unknown {
return query?.requestType;
}

View File

@@ -45,7 +45,7 @@
line-height: 1.45;
}
.suggestions {
.emptySuggestions {
display: flex;
flex-direction: column;
gap: 6px;
@@ -53,8 +53,11 @@
max-width: 360px;
}
.suggestion {
width: 100%;
.emptyChip {
display: flex;
align-items: center;
justify-content: flex-start !important;
gap: 8px;
padding: 10px 14px;
border: 1px solid var(--l2-border);
border-radius: var(--radius-2);
@@ -63,16 +66,21 @@
font-size: 12.5px;
text-align: left;
cursor: pointer;
transition:
background 0.15s ease,
border-color 0.15s ease,
color 0.15s ease;
transition: all 0.15s ease;
line-height: 1.35;
overflow-wrap: anywhere;
&:hover {
background: var(--l2-background);
border-color: var(--l3-border);
color: var(--l1-foreground);
}
svg {
flex-shrink: 0;
color: var(--l3-foreground);
}
&:hover svg {
color: var(--accent-primary);
}
}

View File

@@ -1,5 +1,13 @@
import { useCallback, useEffect, useRef } from 'react';
import { Button } from '@signozhq/ui/button';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import {
Activity,
TriangleAlert,
ChartBar,
Search,
Zap,
} from '@signozhq/icons';
import Noz from 'components/Noz/Noz';
import logEvent from 'api/common/logEvent';
@@ -12,7 +20,29 @@ import MessageBubble from '../MessageBubble';
import StreamingMessage from '../StreamingMessage';
import styles from './VirtualizedMessages.module.scss';
import { useEmptyStateChips } from './useEmptyStateChips';
const SUGGESTIONS = [
{
icon: TriangleAlert,
text: 'Show me the top errors in the last hour',
},
{
icon: Activity,
text: 'What services have the highest latency?',
},
{
icon: ChartBar,
text: 'Give me an overview of system health',
},
{
icon: Search,
text: 'Find slow database queries',
},
{
icon: Zap,
text: 'Which endpoints have the most 5xx errors?',
},
];
const EMPTY_EVENTS: StreamingEventItem[] = [];
@@ -143,10 +173,8 @@ export default function VirtualizedMessages({
const showStreamingSlot =
isStreaming || Boolean(pendingApproval) || Boolean(pendingClarification);
const isEmptyState = messages.length === 0 && !showStreamingSlot;
const { chips: emptyStateChips } = useEmptyStateChips(isEmptyState);
if (isEmptyState) {
if (messages.length === 0 && !showStreamingSlot) {
return (
<div className={styles.empty}>
<div className={`${styles.emptyIcon} noz-wave`}>
@@ -156,22 +184,24 @@ export default function VirtualizedMessages({
<p className={styles.emptySubtitle}>
Ask questions about your traces, logs, metrics, and infrastructure.
</p>
<div className={styles.suggestions}>
{emptyStateChips.map((chip) => (
<div
key={chip.id}
className={styles.suggestion}
<div className={styles.emptySuggestions}>
{SUGGESTIONS.map((s) => (
<Button
key={s.text}
variant="outlined"
color="secondary"
className={styles.emptyChip}
onClick={(): void => {
void logEvent(AIAssistantEvents.SuggestedPromptClicked, {
promptId: chip.id,
promptId: s.text,
category: SuggestedPromptCategory.EmptyState,
});
onSendSuggestedPrompt(chip.text);
onSendSuggestedPrompt(s.text);
}}
data-testid={`empty-state-chip-${chip.id}`}
prefix={<s.icon size={14} />}
>
{chip.text}
</div>
{s.text}
</Button>
))}
</div>
</div>

View File

@@ -1,25 +0,0 @@
import type { ChipDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
/** Static empty-state chips used when the contextual chips API is unavailable. */
export const EMPTY_STATE_CHIPS_FALLBACK: ChipDTO[] = [
{
id: 'top_errors_last_hour',
text: 'Show me the top errors in the last hour',
},
{
id: 'highest_latency_services',
text: 'What services have the highest latency?',
},
{
id: 'system_health_overview',
text: 'Give me an overview of system health',
},
{
id: 'slow_database_queries',
text: 'Find slow database queries',
},
{
id: 'endpoints_5xx_errors',
text: 'Which endpoints have the most 5xx errors?',
},
];

View File

@@ -1,33 +0,0 @@
import { useMemo } from 'react';
import { useQuery } from 'react-query';
import { getEmptyStateChips } from 'api/ai-assistant/chat';
import type { ChipDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useResolvePageType } from 'hooks/aiAssistant/useResolvePageType';
import { EMPTY_STATE_CHIPS_FALLBACK } from './emptyStateChipsFallback';
interface UseEmptyStateChipsResult {
chips: ChipDTO[];
}
export function useEmptyStateChips(enabled: boolean): UseEmptyStateChipsResult {
const pageType = useResolvePageType();
const { data, isError } = useQuery(
[REACT_QUERY_KEY.AI_ASSISTANT_EMPTY_STATE_CHIPS, pageType],
({ signal }) => getEmptyStateChips(pageType, signal),
{ enabled },
);
const chips = useMemo(() => {
if (isError) {
return EMPTY_STATE_CHIPS_FALLBACK;
}
return data ?? [];
}, [data, isError]);
return { chips };
}

View File

@@ -1,7 +1,6 @@
import type { MessageContext } from 'api/ai-assistant/chat';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { AlertListTabs } from 'pages/AlertList/types';
import { matchPath } from 'react-router-dom';
/**
@@ -165,18 +164,13 @@ export function getAutoContexts(
];
}
// Alerts index — `/alerts` with tab query param (defaults to Alert Rules).
if (matchPath(pathname, { path: ROUTES.LIST_ALL_ALERT, exact: true })) {
const page = resolveAlertsIndexPage(params.get(QueryParams.tab));
return [
{
source: 'auto',
type: 'alert',
resourceId: null,
metadata: {
page,
...(page === 'alerts_triggered' ? sharedMetadata : {}),
},
metadata: { page: 'alert_list' },
},
];
}
@@ -282,9 +276,8 @@ export function getAutoContexts(
// ── Metrics ───────────────────────────────────────────────────────────────
// Metrics explorer — `/metrics-explorer` and sub-routes (summary, explorer, views).
if (
matchPath(pathname, { path: ROUTES.METRICS_EXPLORER_BASE, exact: false })
matchPath(pathname, { path: ROUTES.METRICS_EXPLORER_EXPLORER, exact: false })
) {
return [
{
@@ -299,25 +292,9 @@ export function getAutoContexts(
];
}
// NOTE: Homepage (`/home`) and infrastructure monitoring
// (`/infrastructure-monitoring/*`) intentionally emit no auto-context here.
// They have no resource that maps to `MessageContextDTOType`, so attaching
// a chip would misrepresent the page (e.g. a bogus "metrics_explorer"
// context). Their `page_type` for empty-state chips is resolved directly
// from the route in `resolvePageType`.
return [];
}
type AlertsIndexPage = 'alert_list' | 'alerts_triggered';
function resolveAlertsIndexPage(tab: string | null): AlertsIndexPage {
if (tab === AlertListTabs.TRIGGERED_ALERTS) {
return 'alerts_triggered';
}
return 'alert_list';
}
/**
* Pulls metadata fields that any page may carry in its query string —
* `timeRange`, `query`, saved-view selectors, dashboard variables. Each

View File

@@ -1,74 +0,0 @@
import { PageTypeDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { matchPath } from 'react-router-dom';
import { getAutoContexts } from './getAutoContexts';
const PAGE_METADATA_TO_DTO: Record<string, PageTypeDTO> = {
dashboard_detail: PageTypeDTO.dashboard_detail,
dashboard_list: PageTypeDTO.dashboard_list,
panel_edit: PageTypeDTO.panel_edit,
panel_fullscreen: PageTypeDTO.panel_fullscreen,
logs_explorer: PageTypeDTO.logs_explorer,
trace_detail: PageTypeDTO.trace_detail,
traces_explorer: PageTypeDTO.traces_explorer,
metrics_explorer: PageTypeDTO.metrics_explorer,
service_detail: PageTypeDTO.service_detail,
services_list: PageTypeDTO.services_list,
alert_edit: PageTypeDTO.alert_edit,
alert_list: PageTypeDTO.alert_list,
alert_new: PageTypeDTO.alert_new,
alerts_triggered: PageTypeDTO.alerts_triggered,
};
interface ResolvePageTypeOptions {
/** Standalone `/ai-assistant` surface — no underlying observability page. */
isStandaloneAssistant?: boolean;
}
/**
* Maps the current URL (and assistant surface) to the backend `page_type`
* enum used by contextual empty-state chips.
*/
export function resolvePageType(
pathname: string,
search: string,
options?: ResolvePageTypeOptions,
): PageTypeDTO {
if (options?.isStandaloneAssistant) {
return PageTypeDTO.other;
}
// Pseudo-pages with no attachable resource: resolved straight from the
// route. They deliberately emit no auto-context chip (see `getAutoContexts`),
// so they can't be derived from `metadata.page` like the pages below.
if (matchPath(pathname, { path: ROUTES.HOME, exact: true })) {
return PageTypeDTO.homepage;
}
if (
matchPath(pathname, {
path: ROUTES.INFRASTRUCTURE_MONITORING_BASE,
exact: false,
})
) {
return PageTypeDTO.infra_entity_detail;
}
const contexts = getAutoContexts(pathname, search);
const page = contexts[0]?.metadata?.page;
if (typeof page === 'string') {
if (page === 'logs_explorer') {
const activeLogId = new URLSearchParams(search).get(QueryParams.activeLogId);
if (activeLogId) {
return PageTypeDTO.log_detail;
}
}
const mapped = PAGE_METADATA_TO_DTO[page];
if (mapped) {
return mapped;
}
}
return PageTypeDTO.other;
}

View File

@@ -63,6 +63,5 @@
flex: 0 0 auto;
min-height: 0;
min-width: 0;
padding-left: 12px;
padding-bottom: 12px;
padding: 8px;
}

View File

@@ -16,7 +16,7 @@ import PieArc from './PieArc';
import PieCenterLabel from './PieCenterLabel';
import styles from './Pie.module.scss';
import { PieTooltipData } from './types';
import { getDonutGeometry, getFillColor } from './utils';
import { getFillColor } from './utils';
/**
* Donut chart rendered with @visx. Splits its area into chart + legend with the
@@ -78,12 +78,16 @@ export default function Pie({
[containerWidth, containerHeight, position, data],
);
// Donut geometry derived from the allocated chart box, sized to leave room
// for the external leader labels (see getDonutGeometry).
const { size, radius, innerRadius } = useMemo(
() => getDonutGeometry(width, height),
[width, height],
);
// Donut geometry derived from the allocated chart box.
const { size, radius, innerRadius } = useMemo(() => {
const nextSize = Math.min(width, height);
const nextRadius = nextSize * 0.35;
return {
size: nextSize,
radius: nextRadius,
innerRadius: nextRadius * 0.6,
};
}, [width, height]);
const totalValue = useMemo(
() => visibleData.reduce((sum, slice) => sum + slice.value, 0),

View File

@@ -1,40 +1,11 @@
import {
getArcGeometry,
getDonutGeometry,
getFillColor,
getScaledFontSize,
lightenColor,
} from '../utils';
describe('Pie utils', () => {
describe('getDonutGeometry', () => {
it('keeps the label anchor inside the box (reserves room for leader labels)', () => {
const { radius } = getDonutGeometry(400, 300);
const half = Math.min(400, 300) / 2; // 150
// The label anchor sits at radius * 1.3 and must stay within the box
// half-extent so labels are not clipped.
expect(radius * 1.3).toBeLessThanOrEqual(half);
// And it should use the available room (anchor = half - 22 allowance).
expect(radius * 1.3).toBeCloseTo(half - 22);
});
it('derives size and inner radius from the outer radius', () => {
const { size, radius, innerRadius } = getDonutGeometry(300, 300);
expect(size).toBeCloseTo(radius * 2);
expect(innerRadius).toBeCloseTo(radius * 0.6);
});
it('sizes off the smaller dimension so it fits both axes', () => {
expect(getDonutGeometry(1000, 200)).toStrictEqual(
getDonutGeometry(200, 1000),
);
});
it('never returns a negative radius for a box too small for labels', () => {
expect(getDonutGeometry(20, 20).radius).toBe(0);
});
});
describe('getScaledFontSize', () => {
it('returns the base size for empty text', () => {
expect(getScaledFontSize({ text: '', baseSize: 30, innerRadius: 100 })).toBe(

View File

@@ -10,16 +10,6 @@ export interface ScaledFontSizeArgs {
innerRadius: number;
}
/** Donut sizing for a given chart box: the outer/inner radii and the square it spans. */
export interface DonutGeometry {
/** Outer diameter — feeds the visx Pie width/height and the render guard. */
size: number;
/** Outer radius of the donut ring. */
radius: number;
/** Inner radius (the hole) — also bounds the centre-total font. */
innerRadius: number;
}
export interface ArcGeometry {
/** Outer point where the leader label sits. */
labelX: number;

View File

@@ -3,37 +3,7 @@
* so the renderer stays declarative (per the one-component-per-file rule).
*/
import {
ArcGeometry,
DonutGeometry,
ParsedRgb,
ScaledFontSizeArgs,
} from './types';
// Leader-line + two-line label/value drawn outside the donut. `getArcGeometry`
// anchors the label at `radius * LABEL_RADIUS_RATIO`; `LABEL_TEXT_ALLOWANCE` is
// the px reserved beyond that anchor for the (10px, two-line) text so it never
// clips against the SVG edge.
const LABEL_RADIUS_RATIO = 1.3;
const LABEL_TEXT_ALLOWANCE = 22;
const INNER_RADIUS_RATIO = 0.6;
/**
* Sizes the donut to fit inside a `width × height` box *with room for the
* external leader labels*. The label anchor sits at `radius * 1.3`, so we solve
* the outer radius back from the box's half-extent minus the text allowance —
* guaranteeing the labels stay inside the SVG instead of being clipped (V1 used
* a flat `0.35 * min(w,h)`, which left too little margin on small panels).
*/
export function getDonutGeometry(width: number, height: number): DonutGeometry {
const half = Math.min(width, height) / 2;
const radius = Math.max(0, (half - LABEL_TEXT_ALLOWANCE) / LABEL_RADIUS_RATIO);
return {
size: radius * 2,
radius,
innerRadius: radius * INNER_RADIUS_RATIO,
};
}
import { ArcGeometry, ParsedRgb, ScaledFontSizeArgs } from './types';
/**
* Shrinks the centre-total font as the text gets longer so it never overflows
@@ -67,7 +37,7 @@ export function getArcGeometry(
radius: number,
): ArcGeometry {
const angle = (startAngle + endAngle) / 2;
const labelRadius = radius * LABEL_RADIUS_RATIO;
const labelRadius = radius * 1.3;
const lineEndRadius = radius * 1.1;
return {
labelX: Math.sin(angle) * labelRadius,

View File

@@ -1,79 +0,0 @@
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { calculateChartDimensions } from '../utils';
const labels = (count: number, length = 20): string[] =>
Array.from({ length: count }, (_, i) =>
`label-${i}`.padEnd(length, 'x').slice(0, length),
);
describe('calculateChartDimensions', () => {
it('returns all zeros when the container has no space', () => {
expect(
calculateChartDimensions({
containerWidth: 0,
containerHeight: 300,
legendConfig: { position: LegendPosition.BOTTOM },
seriesLabels: labels(3),
}),
).toStrictEqual({
width: 0,
height: 0,
legendWidth: 0,
legendHeight: 0,
averageLegendWidth: 0,
});
});
it('RIGHT: reserves a side column capped at 30% of the width and keeps full height', () => {
const dims = calculateChartDimensions({
containerWidth: 1000,
containerHeight: 400,
legendConfig: { position: LegendPosition.RIGHT },
seriesLabels: labels(10, 40),
});
// 40-char labels approximate to 336px, capped at min(240, 30% of 1000).
expect(dims.legendWidth).toBe(240);
expect(dims.width).toBe(760);
expect(dims.height).toBe(400);
expect(dims.legendHeight).toBe(400);
});
it('BOTTOM: a single row of items reserves one legend row', () => {
const dims = calculateChartDimensions({
containerWidth: 1000,
containerHeight: 500,
legendConfig: { position: LegendPosition.BOTTOM },
seriesLabels: labels(3),
});
// One row = line height (28) + padding (12).
expect(dims.legendHeight).toBe(40);
expect(dims.height).toBe(460);
expect(dims.legendWidth).toBe(1000);
});
it('BOTTOM: many items cap at two rows on a tall container', () => {
const dims = calculateChartDimensions({
containerWidth: 1000,
containerHeight: 500,
legendConfig: { position: LegendPosition.BOTTOM },
seriesLabels: labels(40),
});
// Two rows = 2 * 40 - 12 (no trailing padding) = 68, under the 80px cap.
expect(dims.legendHeight).toBe(68);
expect(dims.height).toBe(432);
});
it('BOTTOM: on a short container the legend never takes more than 30% of the height', () => {
const dims = calculateChartDimensions({
containerWidth: 1000,
containerHeight: 160,
legendConfig: { position: LegendPosition.BOTTOM },
seriesLabels: labels(40),
});
// Without the height-relative cap the legend would take 68px of a 160px
// panel and the chart (pie especially) collapses to a sliver.
expect(dims.legendHeight).toBe(48); // 30% of 160
expect(dims.height).toBe(112);
});
});

View File

@@ -116,15 +116,7 @@ export function calculateChartDimensions({
? legendRowCount * legendRowHeight - LEGEND_PADDING
: legendRowHeight;
// Cap at two rows / 80px, and never more than 30% of the container height
// (the doc above always promised the %-cap; without it, short grid panels
// hand most of their area to the legend and the chart — the pie donut
// especially — collapses to a sliver). 30% mirrors the RIGHT-legend width cap.
const maxAllowedLegendHeight = Math.min(
2 * legendRowHeight,
80,
Math.floor(containerHeight * 0.3),
);
const maxAllowedLegendHeight = Math.min(2 * legendRowHeight, 80);
const bottomLegendHeight = Math.min(
idealBottomLegendHeight,

View File

@@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { Skeleton } from 'antd';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { FeatureKeys } from 'constants/features';
import { PANEL_TYPES } from 'constants/queryBuilder';
@@ -123,14 +124,24 @@ function ServiceOverview({
/>
<Card data-testid="service_latency">
<GraphContainer>
<Graph
onDragSelect={onDragSelect}
widget={latencyWidget}
onClickHandler={handleGraphClick('Service')}
isQueryEnabled={isQueryEnabled}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
{topLevelOperationsIsLoading && (
<Skeleton
style={{
height: '100%',
padding: '16px',
}}
/>
)}
{!topLevelOperationsIsLoading && (
<Graph
onDragSelect={onDragSelect}
widget={latencyWidget}
onClickHandler={handleGraphClick('Service')}
isQueryEnabled={isQueryEnabled}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
)}
</GraphContainer>
</Card>
</>

View File

@@ -1,3 +1,4 @@
import { Skeleton } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import axios from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
@@ -28,14 +29,24 @@ function TopLevelOperation({
</Typography>
) : (
<GraphContainer>
<Graph
widget={widget}
onClickHandler={handleGraphClick(opName)}
onDragSelect={onDragSelect}
isQueryEnabled={!topLevelOperationsIsLoading}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
{topLevelOperationsIsLoading && (
<Skeleton
style={{
height: '100%',
padding: '16px',
}}
/>
)}
{!topLevelOperationsIsLoading && (
<Graph
widget={widget}
onClickHandler={handleGraphClick(opName)}
onDragSelect={onDragSelect}
isQueryEnabled={!topLevelOperationsIsLoading}
version={ENTITY_VERSION_V4}
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
/>
)}
</GraphContainer>
)}
</Card>

View File

@@ -823,8 +823,8 @@ function NewWidget({
<div className="right-header">
<HeaderRightSection
enableAnnouncements={false}
enableShare={false}
enableFeedback={false}
enableShare
enableFeedback
/>
{showSwitchToViewModeButton && (
<Button

View File

@@ -68,6 +68,10 @@
border-left: unset;
border-radius: 0px 4px 4px 0px;
}
.new-view-btn {
margin-left: 8px;
}
}
.second-row {

View File

@@ -1,48 +0,0 @@
import { renderHook } from '@testing-library/react';
import { PageTypeDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import type { AIAssistantVariant } from 'container/AIAssistant/VariantContext';
import ROUTES from 'constants/routes';
import { useResolvePageType } from '../useResolvePageType';
const mockUseLocation = jest.fn();
const mockUseVariant = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): unknown => mockUseLocation(),
}));
jest.mock('container/AIAssistant/VariantContext', () => ({
useVariant: (): unknown => mockUseVariant(),
}));
function setup(
pathname: string,
search: string,
variant: AIAssistantVariant,
): PageTypeDTO {
mockUseLocation.mockReturnValue({ pathname, search });
mockUseVariant.mockReturnValue(variant);
return renderHook(() => useResolvePageType()).result.current;
}
describe('useResolvePageType', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('returns other for the standalone "page" assistant surface', () => {
const pathname = ROUTES.DASHBOARD.replace(':dashboardId', 'dash-123');
expect(setup(pathname, '', 'page')).toBe(PageTypeDTO.other);
});
it('resolves the underlying page type for embedded variants', () => {
const pathname = ROUTES.DASHBOARD.replace(':dashboardId', 'dash-123');
expect(setup(pathname, '', 'panel')).toBe(PageTypeDTO.dashboard_detail);
expect(setup(pathname, '', 'modal')).toBe(PageTypeDTO.dashboard_detail);
});
});

View File

@@ -1,23 +0,0 @@
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { PageTypeDTO } from 'api/ai-assistant/sigNozAIAssistantAPI.schemas';
import { resolvePageType } from 'container/AIAssistant/resolvePageType';
import { useVariant } from 'container/AIAssistant/VariantContext';
/**
* React hook wrapper around `resolvePageType` that derives the current
* `page_type` from the active location and assistant variant.
*/
export function useResolvePageType(): PageTypeDTO {
const location = useLocation();
const variant = useVariant();
return useMemo(
() =>
resolvePageType(location.pathname, location.search, {
isStandaloneAssistant: variant === 'page',
}),
[location.pathname, location.search, variant],
);
}

View File

@@ -44,6 +44,7 @@
auto-fill,
minmax(var(--legend-average-width, 240px), 1fr)
);
row-gap: 4px;
column-gap: 12px;
}

View File

@@ -20,7 +20,6 @@ import APIError from 'types/api/error';
import DashboardActions from './DashboardActions/DashboardActions';
import DashboardInfo from './DashboardInfo/DashboardInfo';
import { useEditableTitle } from './DashboardInfo/useEditableTitle';
import VariablesBar from '../VariablesBar/VariablesBar';
import styles from './DashboardPageToolbar.module.scss';
@@ -138,8 +137,6 @@ function DashboardPageToolbar(props: DashboardPageToolbarProps): JSX.Element {
onOpenRename={startEdit}
/>
</div>
<VariablesBar dashboard={dashboard} />
</section>
);
}

View File

@@ -1,15 +0,0 @@
// Publish tab — "status strip" direction (Claude Design: Publish Drawer Final).
// Fills the drawer height so the actions anchor a footer instead of floating.
.publishTab {
display: flex;
flex-direction: column;
height: 100%;
min-height: 100%;
}
.content {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
}

View File

@@ -1,12 +0,0 @@
.footer {
position: sticky;
z-index: 1;
flex: none;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
margin-top: 10px;
padding-top: 14px;
border-top: 1px solid var(--l2-border);
}

View File

@@ -1,71 +0,0 @@
import { Globe, RefreshCw, Trash } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import styles from './PublicDashboardActions.module.scss';
interface PublicDashboardActionsProps {
isPublic: boolean;
disabled: boolean;
isPublishing: boolean;
isUpdating: boolean;
isUnpublishing: boolean;
onPublish: () => void;
onUpdate: () => void;
onUnpublish: () => void;
}
function PublicDashboardActions({
isPublic,
disabled,
isPublishing,
isUpdating,
isUnpublishing,
onPublish,
onUpdate,
onUnpublish,
}: PublicDashboardActionsProps): JSX.Element {
return (
<div className={styles.footer}>
{isPublic ? (
<>
<Button
variant="outlined"
color="destructive"
disabled={disabled}
loading={isUnpublishing}
prefix={<Trash size={15} />}
testId="public-dashboard-unpublish"
onClick={onUnpublish}
>
Unpublish Dashboard
</Button>
<Button
variant="solid"
color="primary"
disabled={disabled}
loading={isUpdating}
prefix={<RefreshCw size={15} />}
testId="public-dashboard-update"
onClick={onUpdate}
>
Update Dashboard
</Button>
</>
) : (
<Button
variant="solid"
color="primary"
disabled={disabled}
loading={isPublishing}
prefix={<Globe size={15} />}
testId="public-dashboard-publish"
onClick={onPublish}
>
Publish Dashboard
</Button>
)}
</div>
);
}
export default PublicDashboardActions;

View File

@@ -1,19 +0,0 @@
.hint {
display: flex;
align-items: flex-start;
gap: 8px;
padding-top: 2px;
color: var(--l3-foreground);
}
.hintIcon {
flex: none;
margin-top: 1px;
color: var(--l3-foreground);
}
.hintText {
color: var(--l3-foreground);
font-size: 12px;
line-height: 1.5;
}

View File

@@ -1,17 +0,0 @@
import { Info } from '@signozhq/icons';
import { Typography } from '@signozhq/ui/typography';
import styles from './PublicDashboardHint.module.scss';
function PublicDashboardHint(): JSX.Element {
return (
<div className={styles.hint}>
<Info size={14} className={styles.hintIcon} />
<Typography.Text className={styles.hintText}>
Dashboard variables aren&apos;t supported on public links.
</Typography.Text>
</div>
);
}
export default PublicDashboardHint;

View File

@@ -1,34 +0,0 @@
.switchRow {
display: flex;
align-items: center;
}
.fieldGroup {
display: flex;
flex-direction: column;
gap: 8px;
// Render the (non-portaled) dropdown above the drawer.
[data-radix-popper-content-wrapper] {
z-index: 1100 !important;
}
// Radix sets --radix-select-trigger-width on the content element (the wrapper's
// child), so match it there to make the dropdown take the input's width.
// SelectSimple exposes no content className, hence the descendant selector.
[data-radix-popper-content-wrapper] > * {
width: var(--radix-select-trigger-width);
min-width: var(--radix-select-trigger-width);
}
}
.fieldLabel {
color: var(--l2-foreground);
font-size: 12px;
font-weight: 500;
line-height: 1;
}
.timeRangeSelect {
width: 100%;
}

View File

@@ -1,55 +0,0 @@
import { SelectSimple } from '@signozhq/ui/select';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import { RelativeDurationOptions } from 'container/TopNav/DateTimeSelectionV2/constants';
import styles from './PublicDashboardSettingsForm.module.scss';
interface PublicDashboardSettingsFormProps {
timeRangeEnabled: boolean;
defaultTimeRange: string;
disabled: boolean;
onTimeRangeEnabledChange: (value: boolean) => void;
onDefaultTimeRangeChange: (value: string) => void;
}
function PublicDashboardSettingsForm({
timeRangeEnabled,
defaultTimeRange,
disabled,
onTimeRangeEnabledChange,
onDefaultTimeRangeChange,
}: PublicDashboardSettingsFormProps): JSX.Element {
return (
<>
<div className={styles.switchRow}>
<Switch
testId="public-dashboard-time-range-toggle"
value={timeRangeEnabled}
disabled={disabled}
onChange={onTimeRangeEnabledChange}
>
Enable time range
</Switch>
</div>
<div className={styles.fieldGroup}>
<Typography.Text className={styles.fieldLabel}>
Default time range
</Typography.Text>
<SelectSimple
className={styles.timeRangeSelect}
testId="public-dashboard-default-time-range"
placeholder="Select default time range"
items={RelativeDurationOptions}
value={defaultTimeRange}
disabled={disabled}
withPortal={false}
onChange={(value): void => onDefaultTimeRangeChange(value as string)}
/>
</div>
</>
);
}
export default PublicDashboardSettingsForm;

View File

@@ -1,67 +0,0 @@
.statusStrip {
display: flex;
align-items: center;
gap: 13px;
padding: 14px 16px;
border-radius: 8px;
border: 1px solid var(--l2-border);
background: var(--l1-background);
}
.statusStripLive {
border-color: var(--callout-primary-border);
background: var(--callout-primary-background);
}
.statusMedallion {
display: flex;
align-items: center;
justify-content: center;
flex: none;
width: 38px;
height: 38px;
border-radius: 6px;
border: 1px solid var(--l2-border);
background: var(--l3-background);
color: var(--l2-foreground);
}
.statusMedallionLive {
border-color: var(--callout-primary-border);
background: var(--callout-primary-background);
color: var(--callout-primary-icon);
}
.statusBody {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.statusTitle {
color: var(--l1-foreground);
font-size: 14px;
font-weight: 600;
line-height: 1.2;
}
.statusSubtitle {
margin-top: 2px;
color: var(--l3-foreground);
font-size: 13px;
line-height: 1.35;
}
.statusSubtitleLive {
color: var(--l2-foreground);
}
.statusBadgeDot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 6px;
background: currentColor;
}

View File

@@ -1,50 +0,0 @@
import { Globe, LockKeyhole } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import styles from './PublicDashboardStatus.module.scss';
interface PublicDashboardStatusProps {
isPublic: boolean;
}
function PublicDashboardStatus({
isPublic,
}: PublicDashboardStatusProps): JSX.Element {
return (
<div
className={cx(styles.statusStrip, { [styles.statusStripLive]: isPublic })}
>
<span
className={cx(styles.statusMedallion, {
[styles.statusMedallionLive]: isPublic,
})}
>
{isPublic ? <Globe size={18} /> : <LockKeyhole size={18} />}
</span>
<div className={styles.statusBody}>
<Typography.Text className={styles.statusTitle}>
{isPublic ? 'This dashboard is live' : 'This dashboard is private'}
</Typography.Text>
<Typography.Text
className={cx(styles.statusSubtitle, {
[styles.statusSubtitleLive]: isPublic,
})}
>
{isPublic
? 'Anyone with the link can view it — no account needed.'
: 'Publish it to share a read-only view with anyone who has the link.'}
</Typography.Text>
</div>
<Badge variant="outline" color={isPublic ? 'robin' : 'secondary'}>
<span className={styles.statusBadgeDot} />
{isPublic ? 'Public' : 'Private'}
</Badge>
</div>
);
}
export default PublicDashboardStatus;

View File

@@ -1,69 +0,0 @@
.fieldGroup {
display: flex;
flex-direction: column;
gap: 8px;
}
.fieldLabel {
color: var(--l2-foreground);
font-size: 12px;
font-weight: 500;
line-height: 1;
}
.linkPlaceholder {
display: flex;
align-items: center;
gap: 9px;
height: 40px;
padding: 0 12px;
border-radius: 6px;
border: 1px dashed var(--l2-border);
background: var(--l1-background);
color: var(--l3-foreground);
}
.linkPlaceholderIcon {
flex: none;
color: var(--l3-foreground);
}
.linkPlaceholderText {
color: var(--l3-foreground);
font-size: 13px;
line-height: 1;
}
.linkField {
display: flex;
align-items: center;
gap: 2px;
height: 40px;
padding: 0 5px 0 12px;
border-radius: 6px;
border: 1px solid var(--l2-border);
background: var(--l1-background);
&:hover {
border-color: var(--l3-border);
}
}
.linkUrl {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--l2-foreground);
font-family: var(--font-mono, 'Geist Mono'), monospace;
font-size: 13px;
line-height: 1;
}
.linkDivider {
width: 1px;
height: 20px;
margin: 0 4px;
background: var(--l2-border);
}

View File

@@ -1,59 +0,0 @@
import { Copy, ExternalLink, Link2 } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import styles from './PublicDashboardUrl.module.scss';
interface PublicDashboardUrlProps {
isPublic: boolean;
url: string;
onCopy: () => void;
onOpen: () => void;
}
function PublicDashboardUrl({
isPublic,
url,
onCopy,
onOpen,
}: PublicDashboardUrlProps): JSX.Element {
return (
<div className={styles.fieldGroup}>
<Typography.Text className={styles.fieldLabel}>Public link</Typography.Text>
{isPublic ? (
<div className={styles.linkField}>
<Typography.Text className={styles.linkUrl}>{url}</Typography.Text>
<span className={styles.linkDivider} />
<Button
variant="ghost"
size="icon"
aria-label="Copy link"
testId="public-dashboard-copy-url"
onClick={onCopy}
>
<Copy size={15} />
</Button>
<Button
variant="ghost"
size="icon"
aria-label="Open link"
testId="public-dashboard-open-url"
onClick={onOpen}
>
<ExternalLink size={15} />
</Button>
</div>
) : (
<div className={styles.linkPlaceholder}>
<Link2 size={15} className={styles.linkPlaceholderIcon} />
<Typography.Text className={styles.linkPlaceholderText}>
Your shareable link will appear here once published
</Typography.Text>
</div>
)}
</div>
);
}
export default PublicDashboardUrl;

View File

@@ -1,76 +0,0 @@
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
import PublicDashboardActions from './PublicDashboardActions/PublicDashboardActions';
import PublicDashboardHint from './PublicDashboardHint/PublicDashboardHint';
import PublicDashboardSettingsForm from './PublicDashboardSettingsForm/PublicDashboardSettingsForm';
import PublicDashboardStatus from './PublicDashboardStatus/PublicDashboardStatus';
import PublicDashboardUrl from './PublicDashboardUrl/PublicDashboardUrl';
import { usePublicDashboard } from './usePublicDashboard';
import styles from './PublicDashboard.module.scss';
interface PublicDashboardSettingsProps {
dashboard: DashboardtypesGettableDashboardV2DTO;
}
function PublicDashboardSettings({
dashboard,
}: PublicDashboardSettingsProps): JSX.Element {
const {
isPublic,
isAdmin,
isLoading,
isPublishing,
isUpdating,
isUnpublishing,
timeRangeEnabled,
defaultTimeRange,
publicUrl,
setTimeRangeEnabled,
setDefaultTimeRange,
onPublish,
onUpdate,
onUnpublish,
onCopyUrl,
onOpenUrl,
} = usePublicDashboard(dashboard.id);
const controlsDisabled = isLoading || !isAdmin;
return (
<div className={styles.publishTab}>
<div className={styles.content}>
<PublicDashboardStatus isPublic={isPublic} />
<PublicDashboardUrl
isPublic={isPublic}
url={publicUrl}
onCopy={onCopyUrl}
onOpen={onOpenUrl}
/>
<PublicDashboardSettingsForm
timeRangeEnabled={timeRangeEnabled}
defaultTimeRange={defaultTimeRange}
disabled={controlsDisabled}
onTimeRangeEnabledChange={setTimeRangeEnabled}
onDefaultTimeRangeChange={setDefaultTimeRange}
/>
</div>
<PublicDashboardHint />
<PublicDashboardActions
isPublic={isPublic}
disabled={controlsDisabled}
isPublishing={isPublishing}
isUpdating={isUpdating}
isUnpublishing={isUnpublishing}
onPublish={onPublish}
onUpdate={onUpdate}
onUnpublish={onUnpublish}
/>
</div>
);
}
export default PublicDashboardSettings;

View File

@@ -1,192 +0,0 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueryClient } from 'react-query';
import { useCopyToClipboard } from 'react-use';
import { toast } from '@signozhq/ui/sonner';
import {
invalidateGetPublicDashboard,
useCreatePublicDashboard,
useDeletePublicDashboard,
useUpdatePublicDashboard,
} from 'api/generated/services/dashboard';
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { USER_ROLES } from 'types/roles';
import { getAbsoluteUrl } from 'utils/basePath';
import { openInNewTab } from 'utils/navigation';
import { usePublicDashboardMeta } from './usePublicDashboardMeta';
export interface UsePublicDashboardReturn {
isPublic: boolean;
isAdmin: boolean;
isLoading: boolean;
isPublishing: boolean;
isUpdating: boolean;
isUnpublishing: boolean;
timeRangeEnabled: boolean;
defaultTimeRange: string;
publicUrl: string;
setTimeRangeEnabled: (value: boolean) => void;
setDefaultTimeRange: (value: string) => void;
onPublish: () => void;
onUpdate: () => void;
onUnpublish: () => void;
onCopyUrl: () => void;
onOpenUrl: () => void;
}
/**
* Encapsulates the public-dashboard query, the create/update/delete mutations and the
* local form state for the V2 publish settings section. Targets the same
* `/dashboards/{id}/public` endpoint as V1 via the generated client.
*/
export function usePublicDashboard(
dashboardId: string,
): UsePublicDashboardReturn {
const queryClient = useQueryClient();
const { showErrorModal } = useErrorModal();
const { user } = useAppContext();
const isAdmin = user?.role === USER_ROLES.ADMIN;
const [, copyToClipboard] = useCopyToClipboard();
const [timeRangeEnabled, setTimeRangeEnabled] = useState<boolean>(true);
const [defaultTimeRange, setDefaultTimeRange] =
useState<string>(DEFAULT_TIME_RANGE);
// Read the shared public-meta cache — the GET is owned globally (toolbar), so the
// drawer reuses it rather than issuing its own request.
const {
publicMeta,
isPublic,
isLoading: isLoadingMeta,
isFetching,
error,
refetch,
} = usePublicDashboardMeta(dashboardId);
// Seed form state from the server config when published.
useEffect(() => {
if (publicMeta) {
setTimeRangeEnabled(publicMeta.timeRangeEnabled ?? false);
setDefaultTimeRange(publicMeta.defaultTimeRange || DEFAULT_TIME_RANGE);
}
}, [publicMeta]);
// A 404 (dashboard not published) surfaces as an error — reset to defaults.
useEffect(() => {
if (error) {
setTimeRangeEnabled(true);
setDefaultTimeRange(DEFAULT_TIME_RANGE);
}
}, [error]);
const publicUrl = useMemo(
() => getAbsoluteUrl(publicMeta?.publicPath ?? ''),
[publicMeta?.publicPath],
);
const handleError = useCallback(
(err: unknown): void => {
showErrorModal(err as APIError);
},
[showErrorModal],
);
const handleSuccess = useCallback(
(message: string): void => {
toast.success(message);
void invalidateGetPublicDashboard(queryClient, { id: dashboardId });
refetch();
},
[queryClient, dashboardId, refetch],
);
const { mutate: createPublicDashboard, isLoading: isPublishing } =
useCreatePublicDashboard({
mutation: {
onSuccess: () => handleSuccess('Dashboard published successfully'),
onError: handleError,
},
});
const { mutate: updatePublicDashboard, isLoading: isUpdating } =
useUpdatePublicDashboard({
mutation: {
onSuccess: () => handleSuccess('Public dashboard updated successfully'),
onError: handleError,
},
});
const { mutate: deletePublicDashboard, isLoading: isUnpublishing } =
useDeletePublicDashboard({
mutation: {
onSuccess: () => handleSuccess('Dashboard unpublished successfully'),
onError: handleError,
},
});
const onPublish = useCallback((): void => {
if (!dashboardId) {
return;
}
createPublicDashboard({
pathParams: { id: dashboardId },
data: { timeRangeEnabled, defaultTimeRange },
});
}, [createPublicDashboard, dashboardId, timeRangeEnabled, defaultTimeRange]);
const onUpdate = useCallback((): void => {
if (!dashboardId) {
return;
}
updatePublicDashboard({
pathParams: { id: dashboardId },
data: { timeRangeEnabled, defaultTimeRange },
});
}, [updatePublicDashboard, dashboardId, timeRangeEnabled, defaultTimeRange]);
const onUnpublish = useCallback((): void => {
if (!dashboardId) {
return;
}
deletePublicDashboard({ pathParams: { id: dashboardId } });
}, [deletePublicDashboard, dashboardId]);
const onCopyUrl = useCallback((): void => {
if (!publicUrl) {
return;
}
copyToClipboard(publicUrl);
toast.success('Copied public dashboard URL successfully');
}, [copyToClipboard, publicUrl]);
const onOpenUrl = useCallback((): void => {
if (publicUrl) {
openInNewTab(publicUrl);
}
}, [publicUrl]);
const isLoading =
isLoadingMeta || isFetching || isPublishing || isUpdating || isUnpublishing;
return {
isPublic,
isAdmin,
isLoading,
isPublishing,
isUpdating,
isUnpublishing,
timeRangeEnabled,
defaultTimeRange,
publicUrl,
setTimeRangeEnabled,
setDefaultTimeRange,
onPublish,
onUpdate,
onUnpublish,
onCopyUrl,
onOpenUrl,
};
}

View File

@@ -1,65 +0,0 @@
import { useMemo } from 'react';
import { useGetPublicDashboard } from 'api/generated/services/dashboard';
import type { DashboardtypesGettablePublicDasbhboardDTO } from 'api/generated/services/sigNoz.schemas';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
export interface UsePublicDashboardMetaReturn {
publicMeta: DashboardtypesGettablePublicDasbhboardDTO | undefined;
isPublic: boolean;
isLoading: boolean;
isFetching: boolean;
error: unknown;
refetch: () => void;
}
// How long a fetched result stays fresh before a natural trigger may refresh it.
const PUBLIC_META_STALE_TIME = 5 * 60 * 1000;
/**
* Single source of truth for a dashboard's public-sharing meta. Keyed by dashboard
* id via the generated query, so the GET happens once globally (the toolbar mounts it
* with the dashboard) and every other caller — the publish settings drawer — reads the
* same cache instead of issuing its own request. A mutation that invalidates
* getGetPublicDashboardQueryKey refreshes all consumers at once.
*
* Only fetched on cloud / enterprise tenants, where public dashboards are available.
*/
export function usePublicDashboardMeta(
dashboardId: string,
): UsePublicDashboardMetaReturn {
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
const enabled = !!dashboardId && (isCloudUser || isEnterpriseSelfHostedUser);
const { data, isLoading, isFetching, error, refetch } = useGetPublicDashboard(
{ id: dashboardId },
{
query: {
enabled,
retry: false,
// refetchOnMount: false stops opening the drawer / switching to the Publish
// tab from refiring the GET — it reuses the toolbar's cached result. A finite
// staleTime still lets it refresh naturally once the data ages, and mutations
// invalidate the key to refresh the published state immediately.
staleTime: PUBLIC_META_STALE_TIME,
refetchOnMount: false,
},
},
);
// react-query retains the last successful `data` after a refetch errors (e.g. the
// 404 once a dashboard is unpublished), so gate on the error to reflect the
// private state.
const publicMeta = error ? undefined : data?.data;
return useMemo(
() => ({
publicMeta,
isPublic: !!publicMeta?.publicPath,
isLoading,
isFetching,
error,
refetch,
}),
[publicMeta, isLoading, isFetching, error, refetch],
);
}

View File

@@ -1,34 +1,24 @@
import { useEffect, useMemo, useState } from 'react';
import { Info } from '@signozhq/icons';
import { SelectSimple } from '@signozhq/ui/select';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
// eslint-disable-next-line signoz/no-antd-components -- fixed-option signal picker
// eslint-disable-next-line signoz/no-antd-components -- searchable async select: no @signozhq/ui equivalent
import { Select } from 'antd';
import { CustomSelect } from 'components/NewSelect';
import TextToolTip from 'components/TextToolTip';
import { useGetFieldKeys } from 'hooks/dynamicVariables/useGetFieldKeys';
import { useGetFieldValues } from 'hooks/dynamicVariables/useGetFieldValues';
import useDebounce from 'hooks/useDebounce';
import { isRetryableError } from 'utils/errorUtils';
import {
DYNAMIC_SIGNAL_LABEL,
DYNAMIC_SIGNALS,
type DynamicSignalOption,
signalForApi,
} from '../variableFormModel';
import { TELEMETRY_SIGNALS, type TelemetrySignal } from '../variableModel';
import styles from './VariableForm.module.scss';
interface DynamicVariableFieldsProps {
attribute: string;
signal: DynamicSignalOption;
signal: TelemetrySignal;
onChange: (patch: {
dynamicAttribute?: string;
dynamicSignal?: DynamicSignalOption;
dynamicSignal?: TelemetrySignal;
}) => void;
onPreview: (values: (string | number)[]) => void;
/** Inline error shown under the attribute field (e.g. duplicate attribute). */
attributeError?: string;
}
/** Dynamic-variable body: telemetry signal + field, whose live values preview. */
@@ -37,24 +27,18 @@ function DynamicVariableFields({
signal,
onChange,
onPreview,
attributeError,
}: DynamicVariableFieldsProps): JSX.Element {
const [search, setSearch] = useState('');
const debouncedSearch = useDebounce(search, 300);
const apiSignal = signalForApi(signal);
const {
data: keyData,
isLoading,
error,
refetch,
} = useGetFieldKeys({
signal: apiSignal,
const { data: keyData, isLoading } = useGetFieldKeys({
signal,
name: debouncedSearch || undefined,
});
// `keys` is a Record keyed BY field name; the field names are the map keys.
// CustomSelect filters the supplied options locally as the user types.
// When the API reports the list is `complete`, search filters locally.
const isComplete = keyData?.data?.complete === true;
const options = useMemo(
() =>
Object.keys(keyData?.data?.keys ?? {}).map((name) => ({
@@ -65,7 +49,7 @@ function DynamicVariableFields({
);
const { data: valueData } = useGetFieldValues({
signal: apiSignal,
signal,
name: attribute,
enabled: !!attribute,
});
@@ -78,60 +62,40 @@ function DynamicVariableFields({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [valueData]);
const errorMessage = error ? (error as Error).message || null : null;
return (
<>
<div className={cx(styles.row, styles.sortSection)}>
<div className={cx(styles.labelContainer, styles.sourceLabel)}>
<div className={styles.labelContainer}>
<Typography.Text className={styles.label}>Source</Typography.Text>
<TextToolTip
text="By default, this searches across logs, traces, and metrics, which can be slow. Selecting a single source improves performance. Many fields share the same values across different signals (for example, `k8s.pod.name` is identical in logs, traces and metrics) making one source enough. Only use `All telemetry` when you need fields that have different values in different signal types."
useFilledIcon={false}
outlinedIcon={<Info size={14} />}
/>
</div>
<Select
<SelectSimple
className={styles.sortSelect}
popupMatchSelectWidth={false}
value={signal}
options={DYNAMIC_SIGNALS.map((s) => ({
label: DYNAMIC_SIGNAL_LABEL[s],
value: s,
}))}
items={TELEMETRY_SIGNALS.map((s) => ({ label: s, value: s }))}
onChange={(value): void =>
onChange({ dynamicSignal: value as DynamicSignalOption })
onChange({ dynamicSignal: value as TelemetrySignal })
}
data-testid="variable-signal-select"
testId="variable-signal-select"
/>
</div>
<div className={cx(styles.row, styles.sortSection)}>
<div className={styles.labelContainer}>
<Typography.Text className={styles.label}>Attribute</Typography.Text>
</div>
<CustomSelect
<Select
className={styles.searchSelect}
showSearch
value={attribute || undefined}
placeholder="Select a telemetry field"
loading={isLoading}
options={options}
filterOption={isComplete}
onSearch={setSearch}
onChange={(value): void => onChange({ dynamicAttribute: value as string })}
noDataMessage="No fields found"
errorMessage={errorMessage}
onRetry={(): void => {
void refetch();
}}
showRetryButton={error ? isRetryableError(error) : true}
options={options}
notFoundContent={isLoading ? 'Loading…' : 'No fields found'}
data-testid="variable-field-select"
/>
</div>
{attributeError ? (
<Typography.Text className={styles.errorText}>
{attributeError}
</Typography.Text>
) : null}
</>
);
}

View File

@@ -1,139 +0,0 @@
import { Badge } from '@signozhq/ui/badge';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
// eslint-disable-next-line signoz/no-antd-components -- fixed-option sort picker
import { Select } from 'antd';
import { CustomSelect } from 'components/NewSelect';
import {
VARIABLE_SORT_LABEL,
VARIABLE_SORTS,
type VariableFormModel,
type VariableSort,
} from '../variableFormModel';
import styles from './VariableForm.module.scss';
interface ListVariableFieldsProps {
model: VariableFormModel;
onChange: (patch: Partial<VariableFormModel>) => void;
previewValues: (string | number)[];
previewError: string | null;
defaultValue: string;
onDefaultValueChange: (value: string) => void;
/** Whether the "ALL values" toggle applies to this type (QUERY / CUSTOM). */
showAllOptionField: boolean;
}
/**
* Rows shared by the list-style variables (Query / Custom / Dynamic): the value
* preview, sort, multi-select / ALL toggles and the default-value picker.
*/
function ListVariableFields({
model,
onChange,
previewValues,
previewError,
defaultValue,
onDefaultValueChange,
showAllOptionField,
}: ListVariableFieldsProps): JSX.Element {
return (
<>
<div className={cx(styles.row, styles.previewSection)}>
<Typography.Text className={styles.previewLabel}>
Preview of Values
</Typography.Text>
<div className={styles.previewValues}>
{previewError ? (
<Typography.Text className={styles.previewError}>
{previewError}
</Typography.Text>
) : (
previewValues.map((value, idx) => (
<Badge
// eslint-disable-next-line react/no-array-index-key -- preview values are display-only and may contain duplicates
key={`${value}-${idx}`}
color="vanilla"
>
{value.toString()}
</Badge>
))
)}
</div>
</div>
<div className={cx(styles.row, styles.sortSection)}>
<div className={styles.labelContainer}>
<Typography.Text className={styles.label}>Sort Values</Typography.Text>
</div>
<Select
className={styles.sortSelect}
popupMatchSelectWidth={false}
value={model.sort}
options={VARIABLE_SORTS.map((sort) => ({
label: VARIABLE_SORT_LABEL[sort],
value: sort,
}))}
onChange={(value): void => onChange({ sort: value as VariableSort })}
data-testid="variable-sort-select"
/>
</div>
<div className={cx(styles.row, styles.multiSection)}>
<Typography.Text className={styles.rowLabel}>
Enable multiple values to be checked
</Typography.Text>
<Switch
value={model.multiSelect}
onChange={(checked): void =>
onChange({
multiSelect: checked,
showAllOption: checked ? model.showAllOption : false,
})
}
testId="variable-multi-switch"
/>
</div>
{model.multiSelect && showAllOptionField ? (
<div className={cx(styles.row, styles.allOptionSection)}>
<Typography.Text className={styles.rowLabel}>
Include an option for ALL values
</Typography.Text>
<Switch
value={model.showAllOption}
onChange={(checked): void => onChange({ showAllOption: checked })}
testId="variable-all-switch"
/>
</div>
) : null}
<div className={cx(styles.row, styles.defaultValueSection)}>
<div className={styles.labelContainer}>
<Typography.Text className={styles.label}>Default Value</Typography.Text>
<Typography.Text className={styles.defaultValueDesc}>
{model.type === 'QUERY'
? 'Click Test Run Query to see the values or add custom value'
: 'Select a value from the preview values or add custom value'}
</Typography.Text>
</div>
<CustomSelect
className={styles.searchSelect}
showSearch
allowClear
placeholder="Select a default value"
value={defaultValue || undefined}
onChange={(value): void => onDefaultValueChange((value as string) ?? '')}
options={previewValues.map((value) => ({
label: value.toString(),
value: value.toString(),
}))}
data-testid="variable-default-select"
/>
</div>
</>
);
}
export default ListVariableFields;

View File

@@ -3,14 +3,14 @@ import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
import Editor from 'components/Editor';
import type { PayloadVariables } from 'types/api/dashboard/variables/query';
import sortValues from 'lib/dashboardVariables/sortVariableValues';
import type { VariableSort } from '../variableModel';
import styles from './VariableForm.module.scss';
interface QueryVariableFieldsProps {
queryValue: string;
/** Sibling variable selections, so dependent `$vars` in the query resolve. */
variables: PayloadVariables;
sort: VariableSort;
onChange: (queryValue: string) => void;
onPreview: (values: (string | number)[]) => void;
onError: (message: string | null) => void;
@@ -19,7 +19,7 @@ interface QueryVariableFieldsProps {
/** Query-variable body: SQL editor + "Test Run Query" that previews the values. */
function QueryVariableFields({
queryValue,
variables,
sort,
onChange,
onPreview,
onError,
@@ -30,21 +30,20 @@ function QueryVariableFields({
setIsRunning(true);
onError(null);
try {
const res = await dashboardVariablesQuery({ query: queryValue, variables });
const res = await dashboardVariablesQuery({
query: queryValue,
variables: {},
});
if (res.statusCode === 200 && res.payload) {
onPreview(res.payload.variableValues ?? []);
onPreview(
sortValues(res.payload.variableValues ?? [], sort) as (string | number)[],
);
} else {
onError(res.error || 'Failed to run query');
onPreview([]);
}
} catch (err) {
// `dashboardVariablesQuery` throws `{ message, details: { error } }`.
const detail = (err as { details?: { error?: string } }).details?.error;
const message =
detail && detail.includes('Syntax error:')
? 'Please make sure query is valid and dependent variables are selected'
: detail || (err as Error).message || 'Failed to run query';
onError(message);
onError((err as Error).message || 'Failed to run query');
onPreview([]);
} finally {
setIsRunning(false);

View File

@@ -5,8 +5,22 @@
.container {
display: flex;
flex-direction: column;
border: 1px solid var(--l1-border);
border-radius: 3px;
border: 1px solid var(--l2-border);
}
.allVariables {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-bottom: 1px solid var(--l1-border);
}
.allVariablesBtn {
--button-height: 24px;
--button-padding: 0;
color: var(--muted-foreground);
}
.content {
@@ -28,12 +42,6 @@
width: 200px;
}
.sourceLabel {
display: flex;
align-items: center;
gap: 6px;
}
.label {
color: var(--l2-foreground);
font-family: Inter;
@@ -51,7 +59,7 @@
.textarea,
.defaultInput {
padding: 6px 6px 6px 8px;
border: 1px solid var(--l2-border);
border: 1px solid var(--l1-border);
border-radius: 2px;
background: var(--l3-background);
}
@@ -70,89 +78,48 @@
color: var(--bg-amber-500);
}
/* Variable type — Tabs root composing the picker row + per-type body panels. */
/* Variable type segmented group */
.typeSection {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 40px;
margin-bottom: 0;
}
/* Picker row (label left, tabs right); the bottom divider separates type from
config. Single line — the tab row scrolls (never wraps) when narrow. */
.typePicker {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--l2-border);
@media (max-width: 1440px) {
flex-wrap: wrap;
}
}
/* Active tab panel — reset the Tabs default padding; body rows handle spacing. */
.typePanel {
padding: 0 !important;
}
.typeContent {
display: flex;
flex-direction: column;
gap: 20px;
}
.typeLabelContainer {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
width: auto;
}
/* Horizontal scroll so the tab row never wraps to a second line. The scrollbar
is hidden — the row stays a single crisp line and scrolls only when narrow. */
.typeTabsScroll {
justify-self: flex-end;
--tab-list-wrapper-secondary-padding-left: 0;
}
/* Connected segmented control, mirroring Overview's SegmentedControl: no outer
padding, segments divided by 1px borders, active segment filled + bold. */
.typeTabs {
display: inline-flex;
flex-wrap: nowrap;
width: max-content;
gap: 0;
padding: 0;
border: 1px solid var(--l2-border);
border-radius: 2px;
background: transparent;
}
.typeTab {
display: inline-flex;
.typeBtnGroup {
display: grid;
grid-template-columns: repeat(4, max-content);
height: 32px;
flex-shrink: 0;
border: 1px solid var(--l1-border);
border-radius: 2px;
background: var(--l2-background);
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.1);
}
.typeBtn {
--button-height: 32px;
display: flex;
align-items: center;
gap: 6px;
min-height: 24px;
padding: 6px 14px;
white-space: nowrap;
justify-content: center;
gap: 4px;
min-width: 114px;
border-radius: 0;
color: var(--l2-foreground);
&:not(:last-child) {
border-right: 1px solid var(--l2-border);
& + & {
border-left: 1px solid var(--l1-border);
}
}
&[data-state='active'] {
color: var(--l1-foreground);
font-weight: 500;
// override the Tabs component's default (transparent) active background.
background: var(--l3-background) !important;
}
.typeBtnSelected {
background: var(--l1-border);
color: var(--l1-foreground);
}
.betaTag {
@@ -171,7 +138,7 @@
.editorWrap {
height: 240px;
overflow: hidden;
border: 1px solid var(--l2-border);
border: 1px solid var(--l1-border);
border-radius: 2px;
}
@@ -187,7 +154,7 @@
.customSection :global(.custom-collapse) {
width: 100%;
border: 1px solid var(--l2-border);
border: 1px solid var(--l1-border);
border-radius: 3px 3px 0 0;
:global(.ant-collapse-item) {
@@ -241,7 +208,7 @@
min-height: 88px;
margin-bottom: 0;
padding-bottom: 8px;
border: 1px solid var(--l2-border);
border: 1px solid var(--l1-border);
border-radius: 3px;
}
@@ -304,9 +271,13 @@
letter-spacing: -0.07px;
}
.sortSelect {
width: 192px;
}
.defaultValueSection {
display: flex;
justify-content: space-between;
display: grid;
grid-template-columns: max-content 1fr;
gap: 1rem;
align-items: center;
margin-bottom: 0;
@@ -326,21 +297,14 @@
letter-spacing: -0.06px;
}
/* All variable selects (Source / Attribute / Sort / Default Value) share width
and a consistent --l2-border outline. */
.sortSelect,
.searchSelect {
width: 240px;
flex-shrink: 0;
:global(.ant-select-selector) {
border-color: var(--l2-border) !important;
}
width: 100%;
}
.actionButtons {
width: 100%;
/* Footer */
.footer {
display: flex;
justify-content: flex-end;
gap: 1rem;
margin-top: 12px;
}

View File

@@ -1,199 +1,350 @@
import { Check, X } from '@signozhq/icons';
import { useEffect, useState } from 'react';
import { ArrowLeft, Check, X } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { TabsContent, TabsRoot } from '@signozhq/ui/tabs';
import { SelectSimple } from '@signozhq/ui/select';
import { Switch } from '@signozhq/ui/switch';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
// eslint-disable-next-line signoz/no-antd-components -- TextArea/Collapse: no @signozhq/ui equivalent
import { Collapse, Input as AntdInput } from 'antd';
// eslint-disable-next-line signoz/no-antd-components -- TextArea/Collapse/searchable Select: no @signozhq/ui equivalent
import { Collapse, Input as AntdInput, Select } from 'antd';
import { commaValuesParser } from 'lib/dashboardVariables/customCommaValuesParser';
import sortValues from 'lib/dashboardVariables/sortVariableValues';
import type { VariableType } from '../variableFormModel';
import {
VARIABLE_SORTS,
type VariableFormModel,
type VariableSort,
type VariableType,
} from '../variableModel';
import DynamicVariableFields from './DynamicVariableFields';
import ListVariableFields from './ListVariableFields';
import QueryVariableFields from './QueryVariableFields';
import { useVariableForm } from './useVariableForm';
import VariableTypeTabs from './VariableTypeTabs';
import VariableTypeSelector from './VariableTypeSelector';
import styles from './VariableForm.module.scss';
import BackToAllVariables from '../components/BackToAllVariables/BackToAllVariables';
import { VariableFormProps } from '../types';
import VariableInfoForm from '../components/VariableInfoForm/VariableInfoForm';
const SORT_LABEL: Record<VariableSort, string> = {
DISABLED: 'Disabled',
ASC: 'Ascending',
DESC: 'Descending',
};
function getNameError(name: string, existingNames: string[]): string | null {
if (name === '') {
return 'Variable name is required';
}
if (/\s/.test(name)) {
return 'Variable name cannot contain whitespaces';
}
if (existingNames.includes(name)) {
return 'Variable name already exists';
}
return null;
}
interface VariableFormProps {
initial: VariableFormModel;
/** Names of the other variables, for uniqueness validation. */
existingNames: string[];
isSaving: boolean;
onClose: () => void;
onSave: (model: VariableFormModel) => void;
}
/**
* In-drawer variable editor reproducing the V1 VariableItem layout, built on
* @signozhq components (antd kept only for the monaco editor, TextArea, Collapse
* and searchable selects). Master→detail: renders in place of the list. Form
* state/handlers live in {@link useVariableForm}; the shared list-type rows in
* {@link ListVariableFields}.
* and searchable selects). Master→detail: renders in place of the list.
*/
function VariableForm({
initial,
siblings,
isNew,
existingNames,
isSaving,
onClose,
onSave,
}: VariableFormProps): JSX.Element {
const {
model,
set,
onNameChange,
selectType,
onCustomChange,
onDynamicChange,
setRawPreview,
previewValues,
previewError,
setPreviewError,
defaultValue,
setDefaultValue,
visibleNameError,
nameError,
attributeError,
cycleError,
isListType,
showAllOptionField,
payloadVariables,
handleSave,
} = useVariableForm({ initial, siblings, isNew, onSave });
const [model, setModel] = useState<VariableFormModel>(initial);
const [previewValues, setPreviewValues] = useState<(string | number)[]>([]);
const [previewError, setPreviewError] = useState<string | null>(null);
const [defaultValue, setDefaultValue] = useState<string>(
((initial.defaultValue as { value?: string })?.value ?? '') as string,
);
// Shared list rows (preview/sort/multi/default) for the list-type variables;
// rendered as a sibling inside each list-type panel. Only the active panel
// mounts (Tabs unmounts the rest), so reusing one element is safe.
const listFields = isListType ? (
<ListVariableFields
model={model}
onChange={set}
previewValues={previewValues}
previewError={previewError}
defaultValue={defaultValue}
onDefaultValueChange={setDefaultValue}
showAllOptionField={showAllOptionField}
/>
) : null;
useEffect(() => {
setModel(initial);
setPreviewValues([]);
setPreviewError(null);
setDefaultValue(
((initial.defaultValue as { value?: string })?.value ?? '') as string,
);
}, [initial]);
const set = (patch: Partial<VariableFormModel>): void =>
setModel((prev) => ({ ...prev, ...patch }));
const selectType = (type: VariableType): void => {
set({ type });
setPreviewValues([]);
setPreviewError(null);
};
const onCustomChange = (value: string): void => {
set({ customValue: value });
setPreviewValues(
sortValues(commaValuesParser(value), model.sort) as (string | number)[],
);
};
const trimmedName = model.name.trim();
const nameError = getNameError(trimmedName, existingNames);
const isListType =
model.type === 'QUERY' || model.type === 'CUSTOM' || model.type === 'DYNAMIC';
const showAllOptionField = model.type === 'QUERY' || model.type === 'CUSTOM';
const handleSave = (): void => {
onSave({
...model,
name: trimmedName,
defaultValue: defaultValue ? { value: defaultValue } : undefined,
});
};
return (
<div className={styles.container}>
<BackToAllVariables onClose={onClose} />
<>
<div className={styles.container}>
<div className={styles.allVariables}>
<Button
variant="ghost"
color="secondary"
className={styles.allVariablesBtn}
prefix={<ArrowLeft size={14} />}
onClick={onClose}
testId="variable-form-back"
>
All variables
</Button>
</div>
<div className={styles.content}>
<VariableInfoForm
title={model.name}
description={model.description}
onTitleChange={onNameChange}
onDescriptionChange={(value): void => set({ description: value })}
visibleNameError={visibleNameError}
/>
<div className={styles.content}>
{/* Name */}
<div className={cx(styles.row, styles.column)}>
<Typography.Text className={styles.label}>Name</Typography.Text>
<Input
className={styles.input}
value={model.name}
placeholder="Unique name of the variable"
onChange={(e): void => set({ name: e.target.value })}
testId="variable-name-input"
/>
{nameError ? (
<Typography.Text className={styles.errorText}>
{nameError}
</Typography.Text>
) : null}
</div>
<TabsRoot
className={styles.typeSection}
value={model.type}
onValueChange={(next): void => selectType(next as VariableType)}
>
<VariableTypeTabs />
{/* Description */}
<div className={cx(styles.row, styles.column)}>
<Typography.Text className={styles.label}>Description</Typography.Text>
<AntdInput.TextArea
className={styles.textarea}
value={model.description}
placeholder="Enter a description for the variable"
rows={3}
onChange={(e): void => set({ description: e.target.value })}
data-testid="variable-description-input"
/>
</div>
<TabsContent value="DYNAMIC" className={styles.typePanel}>
<div className={styles.typeContent}>
<DynamicVariableFields
attribute={model.dynamicAttribute}
signal={model.dynamicSignal}
onChange={onDynamicChange}
onPreview={setRawPreview}
attributeError={attributeError}
{/* Variable Type */}
<VariableTypeSelector value={model.type} onChange={selectType} />
{/* Type-specific body */}
{model.type === 'DYNAMIC' ? (
<DynamicVariableFields
attribute={model.dynamicAttribute}
signal={model.dynamicSignal}
onChange={(patch): void => set(patch)}
onPreview={setPreviewValues}
/>
) : null}
{model.type === 'QUERY' ? (
<QueryVariableFields
queryValue={model.queryValue}
sort={model.sort}
onChange={(queryValue): void => set({ queryValue })}
onPreview={setPreviewValues}
onError={setPreviewError}
/>
) : null}
{model.type === 'CUSTOM' ? (
<div className={cx(styles.row, styles.customSection)}>
<Collapse
collapsible="header"
rootClassName="custom-collapse"
defaultActiveKey={['1']}
items={[
{
key: '1',
label: 'Options',
children: (
<AntdInput.TextArea
value={model.customValue}
placeholder="Enter options separated by commas."
rootClassName="comma-input"
onChange={(e): void => onCustomChange(e.target.value)}
data-testid="variable-custom-input"
/>
),
},
]}
/>
{listFields}
</div>
</TabsContent>
) : null}
<TabsContent value="QUERY" className={styles.typePanel}>
<div className={styles.typeContent}>
<QueryVariableFields
queryValue={model.queryValue}
variables={payloadVariables}
onChange={(queryValue): void => set({ queryValue })}
onPreview={setRawPreview}
onError={setPreviewError}
{model.type === 'TEXT' ? (
<div className={cx(styles.row, styles.textboxSection)}>
<div className={styles.labelContainer}>
<Typography.Text className={styles.label}>
Default Value
</Typography.Text>
</div>
<Input
className={styles.defaultInput}
value={model.textValue}
placeholder="Enter a default value (if any)..."
onChange={(e): void => set({ textValue: e.target.value })}
testId="variable-text-input"
/>
{listFields}
</div>
</TabsContent>
) : null}
<TabsContent value="CUSTOM" className={styles.typePanel}>
<div className={styles.typeContent}>
<div className={cx(styles.row, styles.customSection)}>
<Collapse
collapsible="header"
rootClassName="custom-collapse"
defaultActiveKey={['1']}
items={[
{
key: '1',
label: 'Options',
children: (
<AntdInput.TextArea
value={model.customValue}
placeholder="Enter options separated by commas."
rootClassName="comma-input"
onChange={(e): void => onCustomChange(e.target.value)}
data-testid="variable-custom-input"
/>
),
},
]}
{/* Shared rows for list-type variables */}
{isListType ? (
<>
<div className={cx(styles.row, styles.previewSection)}>
<Typography.Text className={styles.previewLabel}>
Preview of Values
</Typography.Text>
<div className={styles.previewValues}>
{previewError ? (
<Typography.Text className={styles.previewError}>
{previewError}
</Typography.Text>
) : (
previewValues.map((value, idx) => (
<Badge
// eslint-disable-next-line react/no-array-index-key -- preview values are display-only and may contain duplicates
key={`${value}-${idx}`}
color="vanilla"
>
{value.toString()}
</Badge>
))
)}
</div>
</div>
<div className={cx(styles.row, styles.sortSection)}>
<div className={styles.labelContainer}>
<Typography.Text className={styles.label}>Sort Values</Typography.Text>
</div>
<SelectSimple
className={styles.sortSelect}
value={model.sort}
items={VARIABLE_SORTS.map((sort) => ({
label: SORT_LABEL[sort],
value: sort,
}))}
onChange={(value): void => set({ sort: value as VariableSort })}
testId="variable-sort-select"
/>
</div>
{listFields}
</div>
</TabsContent>
<TabsContent value="TEXT" className={styles.typePanel}>
<div className={styles.typeContent}>
<div className={cx(styles.row, styles.textboxSection)}>
<div className={cx(styles.row, styles.multiSection)}>
<Typography.Text className={styles.rowLabel}>
Enable multiple values to be checked
</Typography.Text>
<Switch
value={model.multiSelect}
onChange={(checked): void => {
set({
multiSelect: checked,
showAllOption: checked ? model.showAllOption : false,
});
}}
testId="variable-multi-switch"
/>
</div>
{model.multiSelect && showAllOptionField ? (
<div className={cx(styles.row, styles.allOptionSection)}>
<Typography.Text className={styles.rowLabel}>
Include an option for ALL values
</Typography.Text>
<Switch
value={model.showAllOption}
onChange={(checked): void => set({ showAllOption: checked })}
testId="variable-all-switch"
/>
</div>
) : null}
<div className={cx(styles.row, styles.defaultValueSection)}>
<div className={styles.labelContainer}>
<Typography.Text className={styles.label}>
Default Value
</Typography.Text>
<Typography.Text className={styles.defaultValueDesc}>
{model.type === 'QUERY'
? 'Click Test Run Query to see the values or add custom value'
: 'Select a value from the preview values or add custom value'}
</Typography.Text>
</div>
<Input
className={styles.defaultInput}
value={model.textValue}
placeholder="Enter a default value (if any)..."
onChange={(e): void => set({ textValue: e.target.value })}
testId="variable-text-input"
<Select
className={styles.searchSelect}
showSearch
allowClear
placeholder="Select a default value"
value={defaultValue || undefined}
onChange={(value): void => setDefaultValue(value ?? '')}
options={previewValues.map((value) => ({
label: value.toString(),
value: value.toString(),
}))}
data-testid="variable-default-select"
/>
</div>
</div>
</TabsContent>
</TabsRoot>
{cycleError ? (
<Typography.Text className={styles.errorText}>
{cycleError}
</Typography.Text>
) : null}
<div className={styles.actionButtons}>
<Button
variant="outlined"
color="secondary"
prefix={<X size={14} />}
onClick={onClose}
>
Discard
</Button>
<Button
variant="solid"
color="primary"
prefix={<Check size={14} />}
disabled={!!nameError || !!attributeError}
loading={isSaving}
onClick={handleSave}
testId="variable-save"
>
Save Variable
</Button>
</>
) : null}
</div>
</div>
</div>
<div className={styles.footer}>
<Button
variant="solid"
color="secondary"
prefix={<X size={14} />}
onClick={onClose}
>
Discard
</Button>
<Button
variant="solid"
color="primary"
prefix={<Check size={14} />}
disabled={!!nameError}
loading={isSaving}
onClick={handleSave}
testId="variable-save"
>
Save Variable
</Button>
</div>
</>
);
}

View File

@@ -0,0 +1,99 @@
import {
ClipboardType,
DatabaseZap,
Info,
LayoutList,
Pyramid,
} from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import TextToolTip from 'components/TextToolTip';
import type { VariableType } from '../variableModel';
import styles from './VariableForm.module.scss';
interface VariableTypeSelectorProps {
value: VariableType;
onChange: (type: VariableType) => void;
}
/** The segmented Dynamic / Textbox / Custom / Query type picker. */
function VariableTypeSelector({
value,
onChange,
}: VariableTypeSelectorProps): JSX.Element {
return (
<div className={cx(styles.row, styles.typeSection)}>
<div className={styles.typeLabelContainer}>
<Typography.Text className={styles.label}>Variable Type</Typography.Text>
<TextToolTip
text="Learn more about supported variable types"
url="https://signoz.io/docs/userguide/manage-variables/#supported-variable-types"
urlText="here"
useFilledIcon={false}
outlinedIcon={<Info size={14} />}
/>
</div>
<div className={styles.typeBtnGroup}>
<Button
variant="ghost"
color="secondary"
prefix={<Pyramid size={14} />}
className={cx(styles.typeBtn, {
[styles.typeBtnSelected]: value === 'DYNAMIC',
})}
onClick={(): void => onChange('DYNAMIC')}
testId="variable-type-dynamic"
>
Dynamic
<Badge color="robin" className={styles.betaTag}>
Beta
</Badge>
</Button>
<Button
variant="ghost"
color="secondary"
prefix={<ClipboardType size={14} />}
className={cx(styles.typeBtn, {
[styles.typeBtnSelected]: value === 'TEXT',
})}
onClick={(): void => onChange('TEXT')}
testId="variable-type-textbox"
>
Textbox
</Button>
<Button
variant="ghost"
color="secondary"
prefix={<LayoutList size={14} />}
className={cx(styles.typeBtn, {
[styles.typeBtnSelected]: value === 'CUSTOM',
})}
onClick={(): void => onChange('CUSTOM')}
testId="variable-type-custom"
>
Custom
</Button>
<Button
variant="ghost"
color="secondary"
prefix={<DatabaseZap size={14} />}
className={cx(styles.typeBtn, {
[styles.typeBtnSelected]: value === 'QUERY',
})}
onClick={(): void => onChange('QUERY')}
testId="variable-type-query"
>
Query
<Badge color="amber" className={styles.betaTag}>
Not Recommended
</Badge>
</Button>
</div>
</div>
);
}
export default VariableTypeSelector;

View File

@@ -1,93 +0,0 @@
import {
ClipboardType,
DatabaseZap,
Info,
LayoutList,
Pyramid,
} from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { TabsList, TabsTrigger } from '@signozhq/ui/tabs';
import { Typography } from '@signozhq/ui/typography';
import TextToolTip from 'components/TextToolTip';
import styles from './VariableForm.module.scss';
/**
* Presentational trigger row for the variable-type tabs (label + segmented
* triggers). Must render inside a `TabsRoot`, which owns the active state and
* change handling; the matching `TabsContent` panels are siblings in the root.
*/
function VariableTypeTabs(): JSX.Element {
return (
<div className={styles.typePicker}>
<div className={styles.typeLabelContainer}>
<Typography.Text className={styles.label}>Variable Type</Typography.Text>
<TextToolTip
text="Learn more about supported variable types"
url="https://signoz.io/docs/userguide/manage-variables/#supported-variable-types"
urlText="here"
useFilledIcon={false}
outlinedIcon={<Info size={14} />}
/>
</div>
<div className={styles.typeTabsScroll}>
<TabsList variant="secondary" className={styles.typeTabs}>
<TabsTrigger
value="DYNAMIC"
className={styles.typeTab}
testId="variable-type-dynamic"
>
<Pyramid size={14} />
Dynamic
<Badge color="robin" className={styles.betaTag}>
Beta
</Badge>
</TabsTrigger>
<TabsTrigger
value="TEXT"
className={styles.typeTab}
testId="variable-type-textbox"
>
<ClipboardType size={14} />
Textbox
</TabsTrigger>
<TabsTrigger
value="CUSTOM"
className={styles.typeTab}
testId="variable-type-custom"
>
<LayoutList size={14} />
Custom
</TabsTrigger>
<TabsTrigger
value="QUERY"
className={styles.typeTab}
testId="variable-type-query"
>
<DatabaseZap size={14} />
Query
<Badge color="amber" className={styles.betaTag}>
Not Recommended
</Badge>
<span
className={styles.betaTag}
onClick={(e): void => e.stopPropagation()}
role="presentation"
>
<TextToolTip
text="Learn why we don't recommend"
url="https://signoz.io/docs/userguide/manage-variables/#why-avoid-clickhouse-query-variables"
urlText="here"
useFilledIcon={false}
outlinedIcon={<Info size={14} />}
/>
</span>
</TabsTrigger>
</TabsList>
</div>
</div>
);
}
export default VariableTypeTabs;

View File

@@ -1,191 +0,0 @@
import { useEffect, useMemo, useState } from 'react';
import { commaValuesParser } from 'lib/dashboardVariables/customCommaValuesParser';
import type { PayloadVariables } from 'types/api/dashboard/variables/query';
import type { VariableSelectionMap } from '../../../VariablesBar/selectionTypes';
import { useDashboardStore } from '../../../store/useDashboardStore';
import { detectVariableCycle } from '../variableDependencies';
import {
sortValuesByOrder,
type VariableFormModel,
type VariableType,
} from '../variableFormModel';
import { getAttributeError, getNameError } from './variableValidation';
// Stable reference so the zustand selector never returns a fresh object (which
// would make useSyncExternalStore loop) when this dashboard has no selections.
const EMPTY_SELECTIONS: VariableSelectionMap = {};
interface UseVariableFormArgs {
initial: VariableFormModel;
siblings: VariableFormModel[];
isNew: boolean;
onSave: (model: VariableFormModel) => void;
}
export interface UseVariableForm {
model: VariableFormModel;
set: (patch: Partial<VariableFormModel>) => void;
onNameChange: (value: string) => void;
selectType: (type: VariableType) => void;
onCustomChange: (value: string) => void;
onDynamicChange: (patch: Partial<VariableFormModel>) => void;
setRawPreview: (values: (string | number)[]) => void;
previewValues: (string | number)[];
previewError: string | null;
setPreviewError: (message: string | null) => void;
defaultValue: string;
setDefaultValue: (value: string) => void;
visibleNameError: string | null;
nameError: string | null;
attributeError: string | undefined;
cycleError: string | null;
isListType: boolean;
showAllOptionField: boolean;
payloadVariables: PayloadVariables;
handleSave: () => void;
}
const readDefaultValue = (model: VariableFormModel): string =>
((model.defaultValue as { value?: string })?.value ?? '') as string;
/** Form state, derivations and handlers for the variable editor. */
export function useVariableForm({
initial,
siblings,
isNew,
onSave,
}: UseVariableFormArgs): UseVariableForm {
const [model, setModel] = useState<VariableFormModel>(initial);
// Raw, unsorted preview; `previewValues` applies the chosen sort so a shown
// preview re-sorts when Sort changes.
const [rawPreview, setRawPreview] = useState<(string | number)[]>([]);
const [previewError, setPreviewError] = useState<string | null>(null);
const [cycleError, setCycleError] = useState<string | null>(null);
// In add mode, mirror the chosen attribute into the name until the user types.
const [nameTouched, setNameTouched] = useState(false);
const [defaultValue, setDefaultValue] = useState<string>(
readDefaultValue(initial),
);
useEffect(() => {
setModel(initial);
setRawPreview([]);
setPreviewError(null);
setCycleError(null);
setNameTouched(false);
setDefaultValue(readDefaultValue(initial));
}, [initial]);
const set = (patch: Partial<VariableFormModel>): void =>
setModel((prev) => ({ ...prev, ...patch }));
const previewValues = useMemo(
() => sortValuesByOrder(rawPreview, model.sort) as (string | number)[],
[rawPreview, model.sort],
);
const existingNames = useMemo(() => siblings.map((v) => v.name), [siblings]);
const existingDynamicAttributes = useMemo(
() =>
siblings
.filter((v) => v.type === 'DYNAMIC' && v.dynamicAttribute)
.map((v) => v.dynamicAttribute),
[siblings],
);
// Sibling selections feed the Query "Test Run" so dependent `$vars` resolve.
const dashboardId = useDashboardStore((s) => s.dashboardId);
const selections = useDashboardStore(
(s) => s.variableValues[dashboardId ?? ''] ?? EMPTY_SELECTIONS,
);
const payloadVariables = useMemo<PayloadVariables>(() => {
const out: PayloadVariables = {};
siblings.forEach((v) => {
if (v.name) {
out[v.name] = selections[v.name]?.value ?? null;
}
});
return out;
}, [siblings, selections]);
const trimmedName = model.name.trim();
const nameError = getNameError(trimmedName, existingNames, initial.name);
// Surface the message only once the field is dirty; Save stays disabled regardless.
const visibleNameError = nameTouched ? nameError : null;
const attributeError = getAttributeError(model, existingDynamicAttributes);
const isListType =
model.type === 'QUERY' || model.type === 'CUSTOM' || model.type === 'DYNAMIC';
const showAllOptionField = model.type === 'QUERY' || model.type === 'CUSTOM';
const onNameChange = (value: string): void => {
setNameTouched(true);
set({ name: value });
};
const selectType = (type: VariableType): void => {
set({ type });
setRawPreview([]);
setPreviewError(null);
};
const onCustomChange = (value: string): void => {
set({ customValue: value });
setRawPreview(commaValuesParser(value));
};
// In add mode, mirror the selected attribute into the name until the user
// edits the name themselves (matches the V1 dynamic-variable behaviour).
const onDynamicChange = (patch: Partial<VariableFormModel>): void => {
if (isNew && !nameTouched && patch.dynamicAttribute) {
set({ ...patch, name: patch.dynamicAttribute });
} else {
set(patch);
}
};
const handleSave = (): void => {
const next: VariableFormModel = {
...model,
name: trimmedName,
defaultValue: defaultValue ? { value: defaultValue } : undefined,
};
const cycle = detectVariableCycle([...siblings, next]);
if (cycle) {
setCycleError(
`Cannot save: circular dependency detected between variables: ${cycle.join(
' → ',
)}`,
);
return;
}
setCycleError(null);
onSave(next);
};
return {
model,
set,
onNameChange,
selectType,
onCustomChange,
onDynamicChange,
setRawPreview,
previewValues,
previewError,
setPreviewError,
defaultValue,
setDefaultValue,
visibleNameError,
nameError,
attributeError,
cycleError,
isListType,
showAllOptionField,
payloadVariables,
handleSave,
};
}

View File

@@ -1,37 +0,0 @@
import type { VariableFormModel } from '../variableFormModel';
/**
* Name validation, mirroring V1: empty / whitespace are rejected, and the name
* set includes self, but keeping your own (original) name is always allowed.
*/
export function getNameError(
name: string,
existingNames: string[],
originalName: string,
): string | null {
if (name === '') {
return 'Variable name is required';
}
if (/\s/.test(name)) {
return 'Variable name cannot contain whitespaces';
}
if (name !== originalName && existingNames.includes(name)) {
return 'Variable name already exists';
}
return null;
}
/** Rejects a dynamic variable reusing an attribute already bound elsewhere. */
export function getAttributeError(
model: VariableFormModel,
existingDynamicAttributes: string[],
): string | undefined {
if (
model.type === 'DYNAMIC' &&
model.dynamicAttribute &&
existingDynamicAttributes.includes(model.dynamicAttribute)
) {
return 'A variable with this attribute key already exists';
}
return undefined;
}

View File

@@ -1,140 +0,0 @@
import type { CSSProperties } from 'react';
import { Check, GripVertical, PenLine, Trash2, X } from '@signozhq/icons';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import type { VariableFormModel } from './variableFormModel';
import styles from './Variables.module.scss';
const TYPE_LABEL: Record<VariableFormModel['type'], string> = {
QUERY: 'Query',
CUSTOM: 'Custom',
TEXT: 'Text',
DYNAMIC: 'Dynamic',
};
interface VariableRowProps {
variable: VariableFormModel;
index: number;
canEdit: boolean;
/** True when this row's delete is awaiting inline confirmation. */
isConfirmingDelete: boolean;
onEdit: (index: number) => void;
onRequestDelete: (index: number) => void;
onConfirmDelete: (index: number) => void;
onCancelDelete: () => void;
}
/** A single draggable variable row (drag handle + meta + inline actions). */
function VariableRow({
variable,
index,
canEdit,
isConfirmingDelete,
onEdit,
onRequestDelete,
onConfirmDelete,
onCancelDelete,
}: VariableRowProps): JSX.Element {
const {
attributes,
listeners,
setNodeRef,
setActivatorNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: variable.name });
const style: CSSProperties = {
transform: CSS.Transform.toString(transform),
transition,
...(isDragging ? { position: 'relative', zIndex: 1, opacity: 0.8 } : {}),
};
return (
<div
ref={setNodeRef}
style={style}
className={styles.row}
data-testid={`variable-row-${variable.name}`}
>
<div className={styles.rowMain}>
{canEdit ? (
<span
ref={setActivatorNodeRef}
className={styles.dragHandle}
aria-label="Reorder variable"
{...attributes}
{...listeners}
>
<GripVertical size={14} />
</span>
) : null}
<Typography.Text className={styles.varName}>
${variable.name}
</Typography.Text>
<span className={styles.typeTag}>{TYPE_LABEL[variable.type]}</span>
{variable.description ? (
<Typography.Text className={styles.varDesc}>
{variable.description}
</Typography.Text>
) : null}
</div>
{canEdit && isConfirmingDelete ? (
<div className={styles.rowActions}>
<Typography.Text className={styles.confirmText}>Delete?</Typography.Text>
<Button
variant="ghost"
color="destructive"
size="icon"
onClick={(): void => onConfirmDelete(index)}
aria-label="Confirm delete"
testId={`variable-delete-confirm-${variable.name}`}
>
<Check size={14} />
</Button>
<Button
variant="ghost"
color="secondary"
size="icon"
onClick={onCancelDelete}
aria-label="Cancel delete"
>
<X size={14} />
</Button>
</div>
) : null}
{canEdit && !isConfirmingDelete ? (
<div className={styles.rowActions}>
<Button
variant="ghost"
color="secondary"
size="icon"
onClick={(): void => onEdit(index)}
aria-label="Edit variable"
testId={`variable-edit-${variable.name}`}
>
<PenLine size={14} />
</Button>
<Button
variant="ghost"
color="secondary"
size="icon"
onClick={(): void => onRequestDelete(index)}
aria-label="Delete variable"
testId={`variable-delete-${variable.name}`}
>
<Trash2 size={14} />
</Button>
</div>
) : null}
</div>
);
}
export default VariableRow;

View File

@@ -2,11 +2,13 @@
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px 16px;
}
.header {
display: flex;
justify-content: flex-end;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
@@ -28,6 +30,14 @@
color: var(--l2-foreground);
}
.empty {
padding: 32px;
text-align: center;
border: 1px dashed var(--l1-border);
border-radius: 4px;
color: var(--l2-foreground);
}
.list {
display: flex;
flex-direction: column;
@@ -52,15 +62,6 @@
min-width: 0;
}
.dragHandle {
display: flex;
flex-shrink: 0;
align-items: center;
color: var(--l3-foreground);
cursor: grab;
touch-action: none;
}
.varName {
font-weight: 500;
color: var(--l1-foreground);

View File

@@ -1,20 +1,24 @@
import type { DragEndEvent } from '@dnd-kit/core';
import {
DndContext,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import {
SortableContext,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
Check,
ChevronDown,
ChevronUp,
PenLine,
Trash2,
X,
} from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import VariableRow from './VariableRow';
import type { VariableFormModel } from './variableFormModel';
import type { VariableFormModel } from './variableModel';
import styles from './Variables.module.scss';
const TYPE_LABEL: Record<VariableFormModel['type'], string> = {
QUERY: 'Query',
CUSTOM: 'Custom',
TEXT: 'Text',
DYNAMIC: 'Dynamic',
};
interface VariablesListProps {
variables: VariableFormModel[];
canEdit: boolean;
@@ -37,48 +41,98 @@ function VariablesList({
onCancelDelete,
onMove,
}: VariablesListProps): JSX.Element {
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 1 } }),
);
const handleDragEnd = ({ active, over }: DragEndEvent): void => {
if (!over || active.id === over.id) {
return;
}
const from = variables.findIndex((v) => v.name === active.id);
const to = variables.findIndex((v) => v.name === over.id);
if (from !== -1 && to !== -1) {
onMove(from, to);
}
};
return (
<DndContext
sensors={sensors}
modifiers={[restrictToVerticalAxis]}
onDragEnd={handleDragEnd}
>
<SortableContext
items={variables.map((v) => v.name)}
strategy={verticalListSortingStrategy}
>
<div className={styles.list} data-testid="variables-list">
{variables.map((variable, index) => (
<VariableRow
key={variable.name || `variable-${index}`}
variable={variable}
index={index}
canEdit={canEdit}
isConfirmingDelete={confirmingIndex === index}
onEdit={onEdit}
onRequestDelete={onRequestDelete}
onConfirmDelete={onConfirmDelete}
onCancelDelete={onCancelDelete}
/>
))}
<div className={styles.list} data-testid="variables-list">
{variables.map((variable, index) => (
<div
className={styles.row}
key={variable.name || `variable-${index}`}
data-testid={`variable-row-${variable.name}`}
>
<div className={styles.rowMain}>
<Typography.Text className={styles.varName}>
${variable.name}
</Typography.Text>
<span className={styles.typeTag}>{TYPE_LABEL[variable.type]}</span>
{variable.description ? (
<Typography.Text className={styles.varDesc}>
{variable.description}
</Typography.Text>
) : null}
</div>
{canEdit && confirmingIndex === index ? (
<div className={styles.rowActions}>
<Typography.Text className={styles.confirmText}>Delete?</Typography.Text>
<Button
variant="ghost"
color="destructive"
size="icon"
onClick={(): void => onConfirmDelete(index)}
aria-label="Confirm delete"
testId={`variable-delete-confirm-${variable.name}`}
>
<Check size={14} />
</Button>
<Button
variant="ghost"
color="secondary"
size="icon"
onClick={onCancelDelete}
aria-label="Cancel delete"
>
<X size={14} />
</Button>
</div>
) : null}
{canEdit && confirmingIndex !== index ? (
<div className={styles.rowActions}>
<Button
variant="ghost"
color="secondary"
size="icon"
disabled={index === 0}
onClick={(): void => onMove(index, index - 1)}
aria-label="Move up"
>
<ChevronUp size={14} />
</Button>
<Button
variant="ghost"
color="secondary"
size="icon"
disabled={index === variables.length - 1}
onClick={(): void => onMove(index, index + 1)}
aria-label="Move down"
>
<ChevronDown size={14} />
</Button>
<Button
variant="ghost"
color="secondary"
size="icon"
onClick={(): void => onEdit(index)}
aria-label="Edit variable"
testId={`variable-edit-${variable.name}`}
>
<PenLine size={14} />
</Button>
<Button
variant="ghost"
color="secondary"
size="icon"
onClick={(): void => onRequestDelete(index)}
aria-label="Delete variable"
testId={`variable-delete-${variable.name}`}
>
<Trash2 size={14} />
</Button>
</div>
) : null}
</div>
</SortableContext>
</DndContext>
))}
</div>
);
}

View File

@@ -1,26 +0,0 @@
import { Plus } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
const AddVariableButton = ({
isEditable,
setIsEditing,
}: {
isEditable: boolean;
setIsEditing: (state: { type: 'new' }) => void;
}): JSX.Element => {
return (
<Button
variant="solid"
color="primary"
prefix={<Plus size={14} />}
size="md"
onClick={(): void => setIsEditing({ type: 'new' })}
testId="add-variable"
disabled={!isEditable}
>
Add variable
</Button>
);
};
export default AddVariableButton;

View File

@@ -1,11 +0,0 @@
.backToAllVariables {
gap: 8px;
padding: 8px;
border-bottom: 1px solid var(--l3-border);
}
.backToAllVariablesButton {
--button-font-size: 14px;
--button-padding: var(--spacing-5) var(--spacing-3);
color: var(--l1-foreground);
}

View File

@@ -1,28 +0,0 @@
import { ArrowLeft } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import styles from './BackToAllVariables.module.scss';
import { VariableFormProps } from '../../types';
const BackToAllVariables = ({
onClose,
}: {
onClose: VariableFormProps['onClose'];
}): JSX.Element => {
return (
<div className={styles.backToAllVariables}>
<Button
variant="ghost"
color="secondary"
className={styles.backToAllVariablesButton}
prefix={<ArrowLeft size={14} />}
onClick={onClose}
testId="variable-form-back"
size="md"
>
All variables
</Button>
</div>
);
};
export default BackToAllVariables;

View File

@@ -1,25 +0,0 @@
.noVariablesCard {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.noVariablesCopy {
display: flex;
flex-direction: column;
gap: 2px;
}
.noVariablesTitle {
color: var(--l1-foreground);
font-size: 14px;
font-weight: 500;
line-height: 20px;
}
.noVariablesInfo {
color: var(--l3-foreground);
font-size: 13px;
line-height: 18px;
}

View File

@@ -1,28 +0,0 @@
import { Typography } from '@signozhq/ui/typography';
import AddVariableButton from '../AddVariableButton';
import { EditingState } from '../../types';
import styles from './NoVariables.module.scss';
const NoVariablesCard = ({
isEditable,
setIsEditing,
}: {
isEditable: boolean;
setIsEditing: React.Dispatch<React.SetStateAction<EditingState | null>>;
}): JSX.Element => {
return (
<div className={styles.noVariablesCard}>
<div className={styles.noVariablesCopy}>
<Typography.Text className={styles.noVariablesTitle}>
No variables yet
</Typography.Text>
<Typography.Text className={styles.noVariablesInfo}>
Create a variable to parameterize your panel queries.
</Typography.Text>
</div>
<AddVariableButton isEditable={isEditable} setIsEditing={setIsEditing} />
</div>
);
};
export default NoVariablesCard;

View File

@@ -1,25 +0,0 @@
.infoItemContainer {
display: flex;
flex-direction: column;
gap: 4px;
}
.infoTitle {
color: var(--l2-foreground);
font-family: Inter;
font-size: 12px;
}
.variableNameInput {
border-radius: 2px;
border: 1px solid var(--l2-border);
&::placeholder {
color: var(--l3-foreground);
}
}
.descriptionTextArea {
border-radius: 2px;
border: 1px solid var(--l2-border);
}

View File

@@ -1,60 +0,0 @@
import { Input } from '@signozhq/ui/input';
import { Typography } from '@signozhq/ui/typography';
// eslint-disable-next-line signoz/no-antd-components -- multiline TextArea has no @signozhq/ui equivalent yet
import { Input as AntdInput } from 'antd';
import styles from './VariableInfoForm.module.scss';
import variableFormStyles from '../../VariableForm/VariableForm.module.scss';
interface VariableInfoFormProps {
title: string;
description: string;
onTitleChange: (value: string) => void;
onDescriptionChange: (value: string) => void;
visibleNameError: string | null;
}
function VariableInfoForm({
title,
description,
onTitleChange,
onDescriptionChange,
visibleNameError,
}: VariableInfoFormProps): JSX.Element {
return (
<>
<div className={styles.infoItemContainer}>
<Typography className={styles.infoTitle}>Name</Typography>
<Input
testId="variable-name"
className={styles.variableNameInput}
value={title}
onChange={(e): void => onTitleChange(e.target.value)}
placeholder="Unique name of the variable"
/>
{visibleNameError ? (
<Typography.Text className={variableFormStyles.errorText}>
<sup>*</sup>&nbsp;
{visibleNameError}
</Typography.Text>
) : null}
</div>
<div className={styles.infoItemContainer}>
<Typography className={styles.infoTitle}>Description</Typography>
<AntdInput.TextArea
className={styles.descriptionTextArea}
value={description}
placeholder="Enter a description for the variable"
data-testid="dashboard-desc"
rows={3}
onChange={(e): void => onDescriptionChange(e.target.value)}
/>
</div>
</>
);
}
export default VariableInfoForm;

View File

@@ -1,75 +1,75 @@
import { useEffect, useMemo, useState } from 'react';
import { Plus } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Typography } from '@signozhq/ui/typography';
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
import cx from 'classnames';
import settingsStyles from '../DashboardSettings.module.scss';
import { useDashboardStore } from '../../store/useDashboardStore';
import { useSaveVariables } from './useSaveVariables';
import { dtoToFormModel } from './variableAdapters';
import {
emptyVariableFormModel,
type VariableFormModel,
} from './variableFormModel';
} from './variableModel';
import VariableForm from './VariableForm/VariableForm';
import VariablesList from './VariablesList';
import styles from './Variables.module.scss';
import AddVariableButton from './components/AddVariableButton';
import NoVariablesCard from './components/NoVariablesCard/NoVariablesCard';
import { EditingState } from './types';
interface VariablesSettingsProps {
dashboard: DashboardtypesGettableDashboardV2DTO;
}
/** `null` index = adding a new variable; a number = editing that row. */
type EditingState = { index: number | null } | null;
function VariablesSettings({ dashboard }: VariablesSettingsProps): JSX.Element {
const isEditable = useDashboardStore((s) => s.isEditable);
const { save, isSaving } = useSaveVariables();
const initialFormModels = useMemo(
() => dashboard.spec.variables.map(dtoToFormModel),
[dashboard.spec.variables],
const initialModels = useMemo(
() => (dashboard.spec?.variables ?? []).map(dtoToFormModel),
[dashboard.spec?.variables],
);
const [variables, setVariables] =
useState<VariableFormModel[]>(initialFormModels);
const [variables, setVariables] = useState<VariableFormModel[]>(initialModels);
// Resync from the dashboard after a save round-trips (refetch bumps updatedAt).
useEffect(() => {
setVariables(initialFormModels);
setVariables(initialModels);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dashboard.updatedAt]);
const [isEditing, setIsEditing] = useState<EditingState>(null);
const [editing, setEditing] = useState<EditingState>(null);
const [confirmDeleteIndex, setConfirmDeleteIndex] = useState<number | null>(
null,
);
const editingFormModel: VariableFormModel | null = useMemo(() => {
if (!isEditing) {
const editingModel: VariableFormModel | null = useMemo(() => {
if (!editing) {
return null;
}
return isEditing.type === 'new'
return editing.index === null
? emptyVariableFormModel()
: variables[isEditing.index];
}, [isEditing, variables]);
: variables[editing.index];
}, [editing, variables]);
const siblings = useMemo(() => {
const self = isEditing?.type === 'edit' ? isEditing.index : null;
return variables.filter((_, i) => i !== self);
}, [variables, isEditing]);
const existingNames = useMemo(() => {
const self = editing?.index ?? null;
return variables.filter((_, i) => i !== self).map((v) => v.name);
}, [variables, editing]);
const persist = (next: VariableFormModel[]): void => {
setVariables(next);
void save(next);
};
const handleFormSave = (Formmodel: VariableFormModel): void => {
const handleFormSave = (model: VariableFormModel): void => {
const next = [...variables];
if (isEditing?.type === 'new') {
next.push(Formmodel);
} else if (isEditing?.type === 'edit') {
next[isEditing.index] = Formmodel;
if (editing?.index == null) {
next.push(model);
} else {
next[editing.index] = model;
}
setIsEditing(null);
setEditing(null);
persist(next);
};
@@ -88,14 +88,14 @@ function VariablesSettings({ dashboard }: VariablesSettingsProps): JSX.Element {
setConfirmDeleteIndex(null);
};
if (editingFormModel) {
// Detail view — edit/new form replaces the list in place (no modal).
if (editingModel) {
return (
<VariableForm
initial={editingFormModel}
siblings={siblings}
isNew={isEditing?.type === 'new'}
initial={editingModel}
existingNames={existingNames}
isSaving={isSaving}
onClose={(): void => setIsEditing(null)}
onClose={(): void => setEditing(null)}
onSave={handleFormSave}
/>
);
@@ -103,25 +103,42 @@ function VariablesSettings({ dashboard }: VariablesSettingsProps): JSX.Element {
// Master view — the variables list.
return (
<div className={cx(styles.container, settingsStyles.settingsCard)}>
<div className={styles.container}>
<div className={styles.header}>
<div className={styles.titleRow}>
<Typography.Text className={styles.title}>Variables</Typography.Text>
<Typography.Text className={styles.subtitle}>
Define variables to parameterize panel queries.
</Typography.Text>
</div>
{isEditable ? (
<Button
variant="solid"
color="primary"
prefix={<Plus size={14} />}
onClick={(): void => setEditing({ index: null })}
testId="add-variable"
>
New variable
</Button>
) : null}
</div>
{variables.length === 0 ? (
<NoVariablesCard isEditable={isEditable} setIsEditing={setIsEditing} />
<div className={styles.empty}>
<Typography.Text>No variables defined yet.</Typography.Text>
</div>
) : (
<>
<div className={styles.header}>
<AddVariableButton isEditable={isEditable} setIsEditing={setIsEditing} />
</div>
<VariablesList
variables={variables}
canEdit={isEditable}
confirmingIndex={confirmDeleteIndex}
onEdit={(index): void => setIsEditing({ type: 'edit', index })}
onRequestDelete={(index): void => setConfirmDeleteIndex(index)}
onConfirmDelete={handleConfirmDelete}
onCancelDelete={(): void => setConfirmDeleteIndex(null)}
onMove={handleMove}
/>
</>
<VariablesList
variables={variables}
canEdit={isEditable}
confirmingIndex={confirmDeleteIndex}
onEdit={(index): void => setEditing({ index })}
onRequestDelete={(index): void => setConfirmDeleteIndex(index)}
onConfirmDelete={handleConfirmDelete}
onCancelDelete={(): void => setConfirmDeleteIndex(null)}
onMove={handleMove}
/>
)}
</div>
);

View File

@@ -1,18 +0,0 @@
import { VariableFormModel } from './variableFormModel';
/** `null` index = adding a new variable; a number = editing that row. */
export type EditingState =
| { type: 'new' }
| { type: 'edit'; index: number }
| null;
export interface VariableFormProps {
initial: VariableFormModel;
/** The other variables (excluding this one), for uniqueness & cycle checks. */
siblings: VariableFormModel[];
/** True when adding a new variable (enables auto-naming from the attribute). */
isNew: boolean;
isSaving: boolean;
onClose: () => void;
onSave: (model: VariableFormModel) => void;
}

View File

@@ -6,7 +6,7 @@ import APIError from 'types/api/error';
import { useDashboardStore } from '../../store/useDashboardStore';
import { formModelToDto } from './variableAdapters';
import type { VariableFormModel } from './variableFormModel';
import type { VariableFormModel } from './variableModel';
import { buildVariablesPatch } from './variablePatchOps';
interface UseSaveVariables {

View File

@@ -4,6 +4,7 @@ import {
DashboardtypesVariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesCustomVariableSpecDTOKind as CustomPluginKind,
DashboardtypesVariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesDynamicVariableSpecDTOKind as DynamicPluginKind,
DashboardtypesVariablePluginVariantGithubComSigNozSignozPkgTypesDashboardtypesQueryVariableSpecDTOKind as QueryPluginKind,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import type {
DashboardtypesListVariableSpecDTO,
@@ -13,24 +14,21 @@ import type {
} from 'api/generated/services/sigNoz.schemas';
import {
DYNAMIC_SIGNAL_ALL,
type DynamicSignalOption,
emptyVariableFormModel,
signalForApi,
VARIABLE_SORT_DISABLED,
PLUGIN_KIND,
type TelemetrySignal,
type VariableFormModel,
type VariableSort,
} from './variableFormModel';
} from './variableModel';
/** DTO envelope → flat form model (for display / editing). */
export function dtoToFormModel(
dto: DashboardtypesVariableDTO,
): VariableFormModel {
const base = emptyVariableFormModel();
const display = dto.spec.display;
const display = dto.spec?.display;
const common: VariableFormModel = {
...base,
// TODO
name: dto.spec?.name ?? display?.name ?? '',
description: display?.description ?? '',
};
@@ -52,7 +50,7 @@ export function dtoToFormModel(
...common,
multiSelect: spec.allowMultiple ?? false,
showAllOption: spec.allowAllValue ?? false,
sort: (spec.sort as VariableSort) ?? VARIABLE_SORT_DISABLED,
sort: (spec.sort as VariableSort) ?? 'DISABLED',
defaultValue: spec.defaultValue,
};
const plugin = spec.plugin;
@@ -69,9 +67,7 @@ export function dtoToFormModel(
...listCommon,
type: 'DYNAMIC',
dynamicAttribute: plugin.spec.name ?? '',
// An omitted wire signal means "all telemetry".
dynamicSignal:
(plugin.spec.signal as DynamicSignalOption) ?? DYNAMIC_SIGNAL_ALL,
dynamicSignal: (plugin.spec.signal as TelemetrySignal) ?? 'traces',
};
}
// Default to Query (also covers a query plugin or a missing/unknown plugin).
@@ -99,7 +95,7 @@ function buildPlugin(
kind: DynamicPluginKind['signoz/DynamicVariable'],
spec: {
name: model.dynamicAttribute,
signal: signalForApi(model.dynamicSignal),
signal: model.dynamicSignal as TelemetrytypesSignalDTO,
},
};
case 'QUERY':
@@ -118,6 +114,7 @@ export function formModelToDto(
const display = {
name: model.name,
description: model.description,
hidden: model.hidden,
};
if (model.type === 'TEXT') {
@@ -138,10 +135,7 @@ export function formModelToDto(
name: model.name,
display,
allowMultiple: model.multiSelect,
// Dynamic variables always expose the aggregate "ALL" entry (matches V1,
// which forced showALLOption true on save); other types respect the toggle.
allowAllValue: model.type === 'DYNAMIC' ? true : model.showAllOption,
// model.sort is already a Perses sort token (`none` / `alphabetical-*`).
allowAllValue: model.showAllOption,
sort: model.sort,
defaultValue: model.defaultValue,
plugin: buildPlugin(model),
@@ -155,3 +149,5 @@ export function variableTypeOf(
): VariableFormModel['type'] {
return dtoToFormModel(dto).type;
}
export { PLUGIN_KIND };

View File

@@ -1,35 +0,0 @@
import {
buildDependencies,
buildDependencyGraph,
} from 'container/DashboardContainer/DashboardVariablesSelection/util';
import type { IDashboardVariable } from 'types/api/dashboard/getAll';
import type { VariableFormModel } from './variableFormModel';
/**
* Detects a circular reference among QUERY variables (a query referencing
* another that, transitively, references it back). Reuses the V1 dependency
* graph helpers, which key off `name` / `type` / `queryValue` only.
*
* Returns the names forming the cycle, or `null` when the set is acyclic.
*/
export function detectVariableCycle(
variables: VariableFormModel[],
): string[] | null {
const asDbVariables = variables
.filter((variable) => variable.name)
.map(
(variable) =>
({
name: variable.name,
type: variable.type,
queryValue: variable.queryValue,
}) as IDashboardVariable,
);
const { hasCycle, cycleNodes } = buildDependencyGraph(
buildDependencies(asDbVariables),
);
return hasCycle ? (cycleNodes ?? []) : null;
}

View File

@@ -1,154 +0,0 @@
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import type { VariableDefaultValueDTO } from 'api/generated/services/sigNoz.schemas';
import { sortBy } from 'lodash-es';
/**
* The four variable types the editor exposes. No generated enum exists for this
* — it's a UI grouping over the wire's envelope + plugin kinds: the TextVariable
* envelope → `TEXT`, and a ListVariable's `DashboardtypesVariablePluginKindDTO`
* (`signoz/QueryVariable` | `signoz/CustomVariable` | `signoz/DynamicVariable`)
* → `QUERY` | `CUSTOM` | `DYNAMIC`. Replace with a generated enum if the backend
* ever exposes a single variable-kind type.
*/
export type VariableType = 'QUERY' | 'CUSTOM' | 'TEXT' | 'DYNAMIC';
/** Telemetry signal — the generated enum (traces / logs / metrics). */
export type TelemetrySignal = TelemetrytypesSignalDTO;
/**
* Signal selected in the dynamic-variable editor. `'all'` is UI-only (the
* generated `TelemetrytypesSignalDTO` has no "all") — it searches across every
* signal and maps to an omitted `signal` on the wire (see {@link signalForApi}).
*/
export const DYNAMIC_SIGNAL_ALL = 'all' as const;
export type DynamicSignalOption = TelemetrySignal | typeof DYNAMIC_SIGNAL_ALL;
/**
* Sort order for list-variable values. The wire (Perses) validates `sort`
* against a fixed method set. There is no generated TS enum for it
* (`DashboardtypesListOrderDTO` is the query-builder order, a different field),
* so we mirror the Perses `Sort` tokens here.
*/
export const VARIABLE_SORT = {
DISABLED: 'none',
ASC: 'alphabetical-asc',
DESC: 'alphabetical-desc',
NUMERICAL_ASC: 'numerical-asc',
NUMERICAL_DESC: 'numerical-desc',
CI_ASC: 'alphabetical-ci-asc',
CI_DESC: 'alphabetical-ci-desc',
} as const;
export type VariableSort = (typeof VARIABLE_SORT)[keyof typeof VARIABLE_SORT];
/** Persisted "no sort" value (Perses `none`). */
export const VARIABLE_SORT_DISABLED: VariableSort = VARIABLE_SORT.DISABLED;
export const VARIABLE_SORTS: VariableSort[] = [
VARIABLE_SORT.DISABLED,
VARIABLE_SORT.ASC,
VARIABLE_SORT.DESC,
VARIABLE_SORT.NUMERICAL_ASC,
VARIABLE_SORT.NUMERICAL_DESC,
VARIABLE_SORT.CI_ASC,
VARIABLE_SORT.CI_DESC,
];
export const VARIABLE_SORT_LABEL: Record<VariableSort, string> = {
[VARIABLE_SORT.DISABLED]: 'Disabled',
[VARIABLE_SORT.ASC]: 'Alphabetical (ascending)',
[VARIABLE_SORT.DESC]: 'Alphabetical (descending)',
[VARIABLE_SORT.NUMERICAL_ASC]: 'Numerical (ascending)',
[VARIABLE_SORT.NUMERICAL_DESC]: 'Numerical (descending)',
[VARIABLE_SORT.CI_ASC]: 'Alphabetical, case-insensitive (ascending)',
[VARIABLE_SORT.CI_DESC]: 'Alphabetical, case-insensitive (descending)',
};
export const DYNAMIC_SIGNALS: DynamicSignalOption[] = [
DYNAMIC_SIGNAL_ALL,
TelemetrytypesSignalDTO.traces,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.metrics,
];
export const DYNAMIC_SIGNAL_LABEL: Record<DynamicSignalOption, string> = {
[DYNAMIC_SIGNAL_ALL]: 'All telemetry',
[TelemetrytypesSignalDTO.traces]: 'Traces',
[TelemetrytypesSignalDTO.logs]: 'Logs',
[TelemetrytypesSignalDTO.metrics]: 'Metrics',
};
/** Maps the editor's signal selection to the wire value (`'all'` → omitted). */
export function signalForApi(
signal: DynamicSignalOption,
): TelemetrySignal | undefined {
return signal === DYNAMIC_SIGNAL_ALL ? undefined : signal;
}
type SortableValues = (string | number | boolean)[];
/** Sorts preview / option values by the variable's chosen order (no-op when disabled). */
export function sortValuesByOrder(
values: SortableValues,
sort: VariableSort,
): SortableValues {
switch (sort) {
case VARIABLE_SORT.ASC:
return sortBy(values);
case VARIABLE_SORT.DESC:
return sortBy(values).reverse();
case VARIABLE_SORT.NUMERICAL_ASC:
return sortBy(values, (value) => Number(value));
case VARIABLE_SORT.NUMERICAL_DESC:
return sortBy(values, (value) => Number(value)).reverse();
case VARIABLE_SORT.CI_ASC:
return sortBy(values, (value) => String(value).toLowerCase());
case VARIABLE_SORT.CI_DESC:
return sortBy(values, (value) => String(value).toLowerCase()).reverse();
default:
return values;
}
}
export interface VariableFormModel {
/** Stable identifier, referenced in queries (e.g. `$name`); must be unique. */
name: string;
description: string;
type: VariableType;
// List-variable common fields (Query / Custom / Dynamic).
multiSelect: boolean;
showAllOption: boolean;
sort: VariableSort;
// Type-specific.
queryValue: string; // QUERY
customValue: string; // CUSTOM
textValue: string; // TEXT
textConstant: boolean; // TEXT
dynamicAttribute: string; // DYNAMIC — the telemetry field name
dynamicSignal: DynamicSignalOption; // DYNAMIC — the telemetry signal
/**
* Runtime-selected default, not editable in the management tab yet; carried
* through edits so saving a definition doesn't clobber it.
*/
defaultValue?: VariableDefaultValueDTO;
}
export function emptyVariableFormModel(): VariableFormModel {
return {
name: '',
description: '',
type: 'DYNAMIC',
multiSelect: false,
showAllOption: false,
sort: VARIABLE_SORT_DISABLED,
queryValue: '',
customValue: '',
textValue: '',
textConstant: false,
dynamicAttribute: '',
dynamicSignal: DYNAMIC_SIGNAL_ALL,
};
}

View File

@@ -0,0 +1,78 @@
import type { VariableDefaultValueDTO } from 'api/generated/services/sigNoz.schemas';
/**
* Flat, UI-friendly representation of a V2 dashboard variable. The wire format
* (`DashboardtypesVariableDTO`) is a nested envelope/plugin union that is awkward
* to bind a form to; `variableAdapters` converts between this model and the DTO.
*/
export type VariableType = 'QUERY' | 'CUSTOM' | 'TEXT' | 'DYNAMIC';
export type VariableSort = 'DISABLED' | 'ASC' | 'DESC';
export type TelemetrySignal = 'traces' | 'logs' | 'metrics';
/** Wire `kind` discriminators (string values of the generated enums). */
export const ENVELOPE_KIND = {
LIST: 'ListVariable',
TEXT: 'TextVariable',
} as const;
export const PLUGIN_KIND = {
QUERY: 'signoz/QueryVariable',
CUSTOM: 'signoz/CustomVariable',
DYNAMIC: 'signoz/DynamicVariable',
} as const;
export const VARIABLE_SORTS: VariableSort[] = ['DISABLED', 'ASC', 'DESC'];
export const TELEMETRY_SIGNALS: TelemetrySignal[] = [
'traces',
'logs',
'metrics',
];
export interface VariableFormModel {
/** Stable identifier, referenced in queries (e.g. `$name`); must be unique. */
name: string;
description: string;
hidden: boolean;
type: VariableType;
// List-variable common fields (Query / Custom / Dynamic).
multiSelect: boolean;
showAllOption: boolean;
sort: VariableSort;
// Type-specific.
queryValue: string; // QUERY
customValue: string; // CUSTOM
textValue: string; // TEXT
textConstant: boolean; // TEXT
dynamicAttribute: string; // DYNAMIC — the telemetry field name
dynamicSignal: TelemetrySignal; // DYNAMIC — the telemetry signal
/**
* Runtime-selected default, not editable in the management tab yet; carried
* through edits so saving a definition doesn't clobber it.
*/
defaultValue?: VariableDefaultValueDTO;
}
export function emptyVariableFormModel(): VariableFormModel {
return {
name: '',
description: '',
hidden: false,
type: 'QUERY',
multiSelect: false,
showAllOption: false,
sort: 'DISABLED',
queryValue: '',
customValue: '',
textValue: '',
textConstant: false,
dynamicAttribute: '',
dynamicSignal: 'traces',
};
}

View File

@@ -11,7 +11,7 @@ import {
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
import Overview from './Overview';
import PublicDashboardSettings from './PublicDashboard';
import { SettingsTabPlaceholder } from './utils';
import VariablesSettings from './Variables';
import { useAppContext } from 'providers/App/App';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
@@ -52,14 +52,15 @@ function DashboardSettings({ dashboard }: DashboardSettingsProps): JSX.Element {
key: TabKeys.VARIABLES,
label: TabKeys.VARIABLES,
children: <VariablesSettings dashboard={dashboard} />,
prefixIcon: <Braces size={14} />,
},
...(enablePublicDashboard
? [
{
key: TabKeys.PUBLISH,
label: TabKeys.PUBLISH,
children: <PublicDashboardSettings dashboard={dashboard} />,
children: (
<SettingsTabPlaceholder message="V2 public dashboard publishing coming next." />
),
disabled: user?.role !== USER_ROLES.ADMIN,
},
]

View File

@@ -0,0 +1,23 @@
import { Empty } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import styles from './DashboardSettings.module.scss';
/**
* TEMPORARY: stand-in for the not-yet-built Variables / Publish settings tabs.
* Will be cleaned up later once those tabs ship their real content.
*/
export function SettingsTabPlaceholder({
message,
}: {
message: string;
}): JSX.Element {
return (
<div className={styles.placeholder}>
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={<Typography.Text>{message}</Typography.Text>}
/>
</div>
);
}

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