Compare commits

...

18 Commits

Author SHA1 Message Date
Nikhil Soni
5027e73076 chore: generate api spec
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 17:06:16 +05:30
Nikhil Soni
c09b3477db feat(savedview): migrate saved_views data to new spec envelope
One-time migration (mirrors 046's tx-based read/transform/write-back
structure): rewrites every existing saved_views.data row from the bare
CompositeQuery blob into the new {schemaVersion, spec} envelope,
best-effort folding legacy extra_data content into
spec.selectedFields/spec.display. Then drops the now-unused extra_data,
category, and tags columns (confirmed via earlier research that
category/tags were never actually populated by any caller).
2026-07-29 17:06:08 +05:30
Nikhil Soni
77afa978a8 feat(savedview): restructure storage around typed schemaVersion+spec
Mirrors the dashboardtypes v2 spec pattern: StorableSavedView.Data is
now a typed SavedViewData{SchemaVersion, Spec} (bun auto-marshals it
like DashboardView.Data), and the canonical GettableSavedView/
PostableSavedView embed it directly, replacing the old flat
CompositeQuery+ExtraData shape. SavedViewSpec adds SelectedFields and
Display{MaxLines,FontSize,Format,Color}, formalizing what the frontend
previously packed into an opaque, backend-unaware extraData JSON
string.

Handler splits into canonical (Create/Get/Update/List, backing
/api/v2/saved_views) and legacy (CreateV1/GetV1/UpdateV1/ListV1,
backing /api/v1/explorer/views) methods; Delete is shared since it has
no request/response body to reshape. The legacy methods decode/encode
v3.SavedView directly and convert to/from the canonical spec via small
converters in implsavedview -- extraData is best-effort parsed into
selectedFields/display on write and re-synthesized on read, so the
still-live frontend keeps working unchanged. CreateV1/UpdateV1 validate
via v3.SavedView.Validate() (no sourcePage enum enforcement), matching
production main's behavior exactly rather than the stricter canonical
validation.

Module stays singular (CreateView/GetView/UpdateView/
GetViewsForFilters/DeleteView) -- only the wire-facing handler differs
per API generation, so the two generations always converge on the same
storage shape, unlike dashboard's v1/v2 split where the DB can hold two
different JSON shapes depending on which API wrote a row.
2026-07-29 17:05:59 +05:30
Nikhil Soni
4cb865d208 revert(v3): restore SavedView type for legacy /api/v1 compat
v3.SavedView was removed from this package earlier in this branch when
the saved-view domain type moved to savedviewtypes. Restoring it here
so /api/v1/explorer/views can decode/encode the exact legacy wire shape
via a thin converter, instead of duplicating an equivalent struct.
2026-07-29 17:05:44 +05:30
Nikhil Soni
ef6ac791e8 chore: generate api spec
Regenerated via: go run cmd/enterprise/*.go generate openapi && cd frontend && pnpm generate:api
Picks up UpdateSavedView's response becoming void (no body).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xb5R7Eo19HUukBMVCgKPjS
2026-07-24 16:46:57 +05:30
Nikhil Soni
b3ce3a5d1f refactor(savedview): drop redundant GetView after UpdateView
No consumer needs the full view back from Update: neither active
frontend caller of useUpdateView (ExplorerOptions.tsx, pages/SaveView/
index.tsx) reads the mutation's response data -- both just check
success and separately call refetchAllView(). The signoz-mcp-server
does its own GetView before calling Update for its own validation, so
it doesn't depend on Update's response either.

The GetView call was, however, the only thing making PUT on a
nonexistent view id return an error -- bun's raw UPDATE with a
non-matching WHERE clause doesn't error on its own, so removing the
extra fetch without replacement would have silently turned "update a
bogus id" into a fake success. Replaced it with a RowsAffected() check
on the UPDATE result instead (same pattern already used in
llmpricingrule/spanmapper stores) -- no extra DB round trip, and it's
strictly more correct: a bogus id now returns a proper "not found"
error (matching the ErrorStatusCodes: 404 already declared on
UpdateSavedView's OpenAPIDef, which wasn't actually enforced before).

Response for Update is now nil (matching Delete's convention), since
nothing consumes the body.

Verified with go build, golangci-lint (0 issues), and a live smoke
test: a normal update still persists (confirmed via a follow-up GET),
and updating a nonexistent id now returns saved_view_not_found instead
of a silent success. Also re-verified /api/v1/explorer/views (same
shared handler) is unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xb5R7Eo19HUukBMVCgKPjS
2026-07-24 16:44:00 +05:30
Nikhil Soni
8cddda3ed6 chore: generate api spec
Regenerated via: go run cmd/enterprise/*.go generate openapi && cd frontend && pnpm generate:api
docs/api/openapi.yml had no diff (already up to date from prior commits
this session); the frontend generated schemas pick up the savedview
type changes (SourcePage/PanelType/QueryType enums, dropped category/
tags).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xb5R7Eo19HUukBMVCgKPjS
2026-07-24 16:29:54 +05:30
Nikhil Soni
33fdde9939 refactor(savedview): keep Module.GetViewsForFilters on individual args
Passing the whole *ListSavedViewsParams through to the module ties the
module's signature to the handler's request-binding shape. Revert to
individual args (sourcePage, name) -- the handler still binds/validates
via ListSavedViewsParams, it just unpacks before calling into the
module.

Verified with go build, golangci-lint (0 issues), no openapi.yml diff
(pure module-boundary change), and a live List smoke test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xb5R7Eo19HUukBMVCgKPjS
2026-07-24 16:19:23 +05:30
Nikhil Soni
affb94031d refactor(savedview): drop unused category filter, bind List params via struct
category was never passed by the frontend on any List call -- confirmed
via grep, no call site sets it. Drop it from ListSavedViewsParams and
the category-branching query in GetViewsForFilters.

Also switch List's handler to the binding.Query.BindQuery(...) +
params.Validate() pattern already used by dashboard v2's list handler
(pkg/modules/dashboard/impldashboard/v2_handler.go), instead of manually
pulling each field off r.URL.Query(). GetViewsForFilters now takes the
whole *ListSavedViewsParams instead of separate sourcePage/name/category
strings, matching Module.ListV2's params-struct signature.

ListSavedViewsParams.Validate() skips the SourcePage check when it's
zero (unset), consistent with how ListSort/ListOrder handle optional
enum query params in ListFilter.Validate() -- but validates it strictly
otherwise, so an invalid sourcePage on List now returns a clear error
instead of silently matching zero rows.

Verified with go build, golangci-lint (0 issues), an openapi.yml
regeneration (category query param removed, clean diff), and a live
List smoke test: valid sourcePage filters correctly, no sourcePage
returns empty (unchanged from before), invalid sourcePage is now
rejected with a validation error.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xb5R7Eo19HUukBMVCgKPjS
2026-07-24 16:15:59 +05:30
Nikhil Soni
54787a63d8 feat(savedview): drop category/tags from Postable/GettableSavedView
Both are unused: category is set nowhere in the frontend and read
nowhere either (never in SaveViewProps/UpdateViewProps, only appears
in the response-only ViewProps type); tags has no write path at all
(no UI to add them) -- the one place it's read (a homepage widget
badge list) already guards against the empty-string artifact this
produces, confirming nothing ever populates it.

Keeping StorableSavedView.Category/.Tags as-is (still NOT NULL text
columns) to avoid a migration in this PR -- marked `// TODO:
deprecated, remove it` for a follow-up that drops the columns.
NewStorableSavedView/NewGettableSavedViewFromStorable no longer
read/write them, so new rows get empty-string defaults same as before
tags was ever set, and existing rows' values are simply never
surfaced.

Verified with go build, golangci-lint (0 issues), an openapi.yml
regeneration (clean field removal from both request/response schemas),
and a live create/get/update/list/delete smoke test with no category/
tags in the payload.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xb5R7Eo19HUukBMVCgKPjS
2026-07-24 16:06:28 +05:30
Nikhil Soni
06ab96984f refactor(savedview): separate createdBy/updatedBy in NewStorableSavedView
NewStorableSavedView took a single createdBy param and used it for both
CreatedBy and UpdatedBy, which is only correct for a fresh create. Split
it into separate createdBy/updatedBy args so a caller can vary them
independently (e.g. an update path that only needs to bump updatedBy).
Both CreateView and UpdateView currently pass claims.Email for both --
UpdateView's result is used to patch update_at/update_by plus the
content columns via Set(), so the throwaway id/createdAt/createdBy it
computes stay unused there, same as before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xb5R7Eo19HUukBMVCgKPjS
2026-07-24 16:02:01 +05:30
Nikhil Soni
104bcc55b9 refactor(savedview): move type construction/conversion into savedviewtypes
implsavedview/module.go was hand-building StorableSavedView/GettableSavedView
struct literals and doing json.Marshal/Unmarshal + strings.Join/Split inline
in every method. Mirrors the tagtypes convention (NewTag,
NewGettableTagFromTag/NewGettableTagsFromTags) instead:

- NewStorableSavedView(orgID, createdBy, PostableSavedView) (*StorableSavedView, error)
  builds the DB row from a request, generating id/timestamps.
- NewGettableSavedViewFromStorable(*StorableSavedView) (*GettableSavedView, error)
  and NewGettableSavedViewsFromStorable (batch) build the API response,
  unmarshalling the JSON-encoded query blob.

UpdateView reuses NewStorableSavedView too -- it only pulls Name/Category/
SourcePage/Tags/Data/ExtraData/UpdatedAt/UpdatedBy out of the result for its
column-level Set(), so the throwaway id/createdAt/createdBy it also computes
are harmless.

No behavior change: verified with go build, golangci-lint (0 issues), a
no-op openapi.yml regeneration (pure internal refactor, no type-shape
change), and a live create/get/update/list/delete smoke test confirming
createdAt/createdBy survive an update while updatedAt changes, and tags/
category/compositeQuery all round-trip correctly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xb5R7Eo19HUukBMVCgKPjS
2026-07-24 15:42:50 +05:30
Nikhil Soni
ffbdf26ed7 feat(savedview): add SourcePage enum with validation
sourcePage was a bare string. The frontend only ever sends one of 4
values -- traces/logs/metrics (its own DataSource enum) plus a
special-cased "meter" string literal for the meter explorer -- matching
the legacy v3.DataSource enum exactly. Category has no such fixed set
(unused by the frontend entirely, always empty) so it stays a string.

SourcePage is local to savedviewtypes (valuer.String-backed, same
pattern as PanelType/QueryType), validated on PostableSavedView.Validate(),
and used directly as the StorableSavedView bun column type -- valuer.String
already implements driver.Valuer/sql.Scanner so no extra plumbing is
needed for it to round-trip through the DB.

Verified with go build, golangci-lint (0 issues), an openapi.yml
regeneration (sourcePage now has a proper enum schema instead of a bare
string, in both the request/response bodies and the List query param),
and a live smoke test: valid sourcePage round-trips through create/get/
list, invalid values are rejected on create.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xb5R7Eo19HUukBMVCgKPjS
2026-07-24 15:39:14 +05:30
Nikhil Soni
97ee59636b refactor(savedview): drop legacy v3 dependency for panel/query type
savedviewtypes.CompositeQuery used pkg/query-service/model/v3.PanelType
and v3.QueryType, pulling in the legacy package purely for two small
enums. Alerting already solved this exact problem for its own
qbtypes.QueryEnvelope-based composite query (ruletypes.PanelType/
QueryType in pkg/types/ruletypes/alerting.go) -- mirror that pattern
here instead:

- savedviewtypes.PanelType: local enum with 5 values (value/graph/
  table/list/trace). ruletypes.PanelType only has 3 (value/table/
  graph), which isn't enough for saved views -- log/trace explorer
  views need list/trace too.
- savedviewtypes.QueryType: local enum (builder/clickhouse_sql/
  promql). This is a UI-tab-selector concept (which query-builder mode
  the view was last edited in), distinct from qbtypes.QueryEnvelope.Type
  (the per-query envelope discriminator, e.g. builder_query/
  builder_formula/clickhouse_sql/promql, which can differ across
  entries in the same Queries array). Keeping it, just re-typed
  locally instead of importing v3 for it.

Bonus: unlike v3.PanelType (a bare Go string with no schema enum),
these implement jsonschema.Enum, so the generated OpenAPI spec now
lists the acceptable values instead of a bare `type: string`.

Verified with go build, golangci-lint (0 issues), an openapi.yml
regeneration (diff is just the two new enum schemas plus $ref swaps),
and a live smoke test confirming the wire format is unchanged and
invalid panelType/queryType values are rejected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xb5R7Eo19HUukBMVCgKPjS
2026-07-24 12:53:44 +05:30
Nikhil Soni
6e87eff704 chore: generate api spec 2026-07-23 17:05:37 +05:30
Nikhil Soni
21ac0f44e0 feat(savedview): make SavedView.CompositeQuery v5-only
Splits the domain type into request/response shapes so create/update
don't require id/createdAt/createdBy/updatedAt/updatedBy from the
client:

- PostableSavedView: request body for create/update (no id or
  server-populated audit fields).
- UpdatableSavedView: alias of PostableSavedView (a saved view is
  always replaced in full).
- GettableSavedView: response shape for get/list/create's-echo,
  carrying id and audit fields.

Module and Handler interfaces updated accordingly (CreateView/
UpdateView take PostableSavedView/UpdatableSavedView, GetView/
GetViewsForFilters return *GettableSavedView). The Update handler now
re-fetches and returns the persisted view instead of echoing the
request body, since callers (including the existing frontend type,
UpdateViewPayloadProps.data: ViewProps) expect id/timestamps back.

Also, per the v5-only typing work this continues:
- CompositeQuery is the new name for the saved-view query type
  (matches the established qbtypes.CompositeQuery naming), replacing
  the legacy v3.CompositeQuery for this domain.
- pkg/query-service/model/v3/v3.go now only differs from origin/main
  by the SavedView struct removal (and its now-unused valuer import)
  -- no stray struct tags left over from earlier iterations.
- Validate() uses errors.NewInvalidInputf with a package error code
  instead of fmt.Errorf, matching the forbidigo-clean pattern used
  elsewhere (e.g. dashboardtypes).
- pkg/types/savedviewtypes consolidated down to query.go and
  savedview.go; the separate list.go/domain.go files are gone.

Verified with go build, golangci-lint (0 issues), an openapi.yml
regeneration (clean diff), and a live create/get/update/list/delete
smoke test against /api/v2/saved_views.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xb5R7Eo19HUukBMVCgKPjS
2026-07-22 16:39:02 +05:30
Nikhil Soni
735b9e7d68 refactor(savedview): move SavedView domain type into savedviewtypes
Relocates the saved-view domain type from pkg/query-service/model/v3
(v3.SavedView) to pkg/types/savedviewtypes.SavedView, the conventional
home for domain types, following the same Handler/Module signatures.
The existing bun-persisted row type in that package is renamed from
SavedView to StorableSavedView to avoid a name collision (matching the
Storable*/domain-type naming convention used elsewhere, e.g.
dashboardtypes.StorableDashboard).

v3.SavedView was only referenced by the savedview module/handler
(verified via grep), so this is a mechanical move -- CompositeQuery
itself stays in model/v3 since ~25 other files depend on it.

No new handler methods or request/response types: the v2 routes added
in the previous commit keep using the same Create/Get/Update/Delete/
List methods as /api/v1/explorer/views. CompositeQuery already has
omitempty legacy (builderQueries/chQueries/promQueries) and v5
(queries) fields side by side, and Validate() already accepts a
v5-only payload, so one type/handler pair genuinely serves both API
versions -- no conversion layer needed.

Refs SigNoz/engineering-pod#4651
2026-07-21 15:31:13 +05:30
Nikhil Soni
29315d8c89 refactor: register savedview handler in signozapiserver
feat(savedview): register v2 saved view routes via handler.New()

Registers List/Create/Get/Update/Delete for saved views at
/api/v2/saved_views(/{viewId}) in signozapiserver using handler.New()
with OpenAPIDef, mirroring the alertmanager migration (#10941). This
unblocks audit-log instrumentation and Terraform resource generation,
which the legacy router.HandleFunc registrations in http_handler.go
can't support.

/api/v1/explorer/views keeps working unchanged. Wires the previously
dead addSavedViewRoutes into provider.AddToRouter, adds the missing
authz middleware (ViewAccess/EditAccess, matching the v1 access split),
adds required/nullable OpenAPI tags to v3.SavedView/CompositeQuery, and
regenerates docs/api/openapi.yml.

Refs SigNoz/engineering-pod#4651
2026-07-21 15:19:57 +05:30
16 changed files with 1900 additions and 159 deletions

View File

@@ -7831,6 +7831,116 @@ components:
enum:
- basic
type: string
SavedviewtypesCompositeQuery:
properties:
panelType:
$ref: '#/components/schemas/SavedviewtypesPanelType'
queries:
items:
$ref: '#/components/schemas/Querybuildertypesv5QueryEnvelope'
nullable: true
type: array
queryType:
$ref: '#/components/schemas/SavedviewtypesQueryType'
required:
- panelType
- queryType
- queries
type: object
SavedviewtypesDisplay:
properties:
color:
type: string
fontSize:
type: string
format:
type: string
maxLines:
type: integer
type: object
SavedviewtypesGettableSavedView:
properties:
createdAt:
format: date-time
type: string
createdBy:
type: string
id:
type: string
name:
type: string
schemaVersion:
type: string
sourcePage:
$ref: '#/components/schemas/SavedviewtypesSourcePage'
spec:
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
updatedAt:
format: date-time
type: string
updatedBy:
type: string
required:
- id
- name
- createdAt
- createdBy
- updatedAt
- updatedBy
- sourcePage
- schemaVersion
- spec
type: object
SavedviewtypesPanelType:
enum:
- value
- graph
- table
- list
- trace
type: string
SavedviewtypesPostableSavedView:
properties:
name:
type: string
schemaVersion:
type: string
sourcePage:
$ref: '#/components/schemas/SavedviewtypesSourcePage'
spec:
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
required:
- name
- sourcePage
- schemaVersion
- spec
type: object
SavedviewtypesQueryType:
enum:
- builder
- clickhouse_sql
- promql
type: string
SavedviewtypesSavedViewSpec:
properties:
compositeQuery:
$ref: '#/components/schemas/SavedviewtypesCompositeQuery'
display:
$ref: '#/components/schemas/SavedviewtypesDisplay'
selectedFields:
items:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
type: array
required:
- compositeQuery
type: object
SavedviewtypesSourcePage:
enum:
- traces
- logs
- metrics
- meter
type: string
ServiceaccounttypesDeprecatedPostableServiceAccountRole:
properties:
id:
@@ -22531,6 +22641,300 @@ paths:
summary: Test alert rule
tags:
- rules
/api/v2/saved_views:
get:
deprecated: false
description: Returns saved views for the calling user's org, optionally filtered
by source page and name.
operationId: ListSavedViews
parameters:
- in: query
name: sourcePage
schema:
$ref: '#/components/schemas/SavedviewtypesSourcePage'
- in: query
name: name
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
items:
$ref: '#/components/schemas/SavedviewtypesGettableSavedView'
nullable: true
type: array
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
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List saved views
tags:
- saved_view
post:
deprecated: false
description: Persists a saved view for the explore page. Returns the id of the
created view.
operationId: CreateSavedView
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SavedviewtypesPostableSavedView'
responses:
"200":
content:
application/json:
schema:
properties:
data:
nullable: true
type: string
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
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- EDITOR
- tokenizer:
- EDITOR
summary: Create saved view
tags:
- saved_view
/api/v2/saved_views/{viewId}:
delete:
deprecated: false
description: Deletes a saved view by id.
operationId: DeleteSavedView
parameters:
- in: path
name: viewId
required: true
schema:
type: string
responses:
"200":
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: Delete saved view
tags:
- saved_view
get:
deprecated: false
description: Returns a saved view by id.
operationId: GetSavedView
parameters:
- in: path
name: viewId
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/SavedviewtypesGettableSavedView'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get saved view
tags:
- saved_view
put:
deprecated: false
description: Replaces a saved view's name and query.
operationId: UpdateSavedView
parameters:
- in: path
name: viewId
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SavedviewtypesPostableSavedView'
responses:
"200":
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 saved view
tags:
- saved_view
/api/v2/sessions:
delete:
deprecated: false

View File

@@ -0,0 +1,491 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
*/
import { useMutation, useQuery } from 'react-query';
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
CreateSavedView200,
DeleteSavedViewPathParameters,
GetSavedView200,
GetSavedViewPathParameters,
ListSavedViews200,
ListSavedViewsParams,
RenderErrorResponseDTO,
SavedviewtypesPostableSavedViewDTO,
UpdateSavedViewPathParameters,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* Returns saved views for the calling user's org, optionally filtered by source page and name.
* @summary List saved views
*/
export const listSavedViews = (
params?: ListSavedViewsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListSavedViews200>({
url: `/api/v2/saved_views`,
method: 'GET',
params,
signal,
});
};
export const getListSavedViewsQueryKey = (params?: ListSavedViewsParams) => {
return [`/api/v2/saved_views`, ...(params ? [params] : [])] as const;
};
export const getListSavedViewsQueryOptions = <
TData = Awaited<ReturnType<typeof listSavedViews>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params?: ListSavedViewsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listSavedViews>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListSavedViewsQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listSavedViews>>> = ({
signal,
}) => listSavedViews(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listSavedViews>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListSavedViewsQueryResult = NonNullable<
Awaited<ReturnType<typeof listSavedViews>>
>;
export type ListSavedViewsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List saved views
*/
export function useListSavedViews<
TData = Awaited<ReturnType<typeof listSavedViews>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params?: ListSavedViewsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listSavedViews>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListSavedViewsQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List saved views
*/
export const invalidateListSavedViews = async (
queryClient: QueryClient,
params?: ListSavedViewsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListSavedViewsQueryKey(params) },
options,
);
return queryClient;
};
/**
* Persists a saved view for the explore page. Returns the id of the created view.
* @summary Create saved view
*/
export const createSavedView = (
savedviewtypesPostableSavedViewDTO?: BodyType<SavedviewtypesPostableSavedViewDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateSavedView200>({
url: `/api/v2/saved_views`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: savedviewtypesPostableSavedViewDTO,
signal,
});
};
export const getCreateSavedViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createSavedView>>,
TError,
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createSavedView>>,
TError,
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> },
TContext
> => {
const mutationKey = ['createSavedView'];
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 createSavedView>>,
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> }
> = (props) => {
const { data } = props ?? {};
return createSavedView(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateSavedViewMutationResult = NonNullable<
Awaited<ReturnType<typeof createSavedView>>
>;
export type CreateSavedViewMutationBody =
| BodyType<SavedviewtypesPostableSavedViewDTO>
| undefined;
export type CreateSavedViewMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create saved view
*/
export const useCreateSavedView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createSavedView>>,
TError,
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createSavedView>>,
TError,
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> },
TContext
> => {
return useMutation(getCreateSavedViewMutationOptions(options));
};
/**
* Deletes a saved view by id.
* @summary Delete saved view
*/
export const deleteSavedView = (
{ viewId }: DeleteSavedViewPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/saved_views/${viewId}`,
method: 'DELETE',
signal,
});
};
export const getDeleteSavedViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteSavedView>>,
TError,
{ pathParams: DeleteSavedViewPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteSavedView>>,
TError,
{ pathParams: DeleteSavedViewPathParameters },
TContext
> => {
const mutationKey = ['deleteSavedView'];
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 deleteSavedView>>,
{ pathParams: DeleteSavedViewPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteSavedView(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteSavedViewMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteSavedView>>
>;
export type DeleteSavedViewMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete saved view
*/
export const useDeleteSavedView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteSavedView>>,
TError,
{ pathParams: DeleteSavedViewPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteSavedView>>,
TError,
{ pathParams: DeleteSavedViewPathParameters },
TContext
> => {
return useMutation(getDeleteSavedViewMutationOptions(options));
};
/**
* Returns a saved view by id.
* @summary Get saved view
*/
export const getSavedView = (
{ viewId }: GetSavedViewPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetSavedView200>({
url: `/api/v2/saved_views/${viewId}`,
method: 'GET',
signal,
});
};
export const getGetSavedViewQueryKey = ({
viewId,
}: GetSavedViewPathParameters) => {
return [`/api/v2/saved_views/${viewId}`] as const;
};
export const getGetSavedViewQueryOptions = <
TData = Awaited<ReturnType<typeof getSavedView>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ viewId }: GetSavedViewPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getSavedView>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetSavedViewQueryKey({ viewId });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getSavedView>>> = ({
signal,
}) => getSavedView({ viewId }, signal);
return {
queryKey,
queryFn,
enabled: !!viewId,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getSavedView>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetSavedViewQueryResult = NonNullable<
Awaited<ReturnType<typeof getSavedView>>
>;
export type GetSavedViewQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get saved view
*/
export function useGetSavedView<
TData = Awaited<ReturnType<typeof getSavedView>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ viewId }: GetSavedViewPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getSavedView>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetSavedViewQueryOptions({ viewId }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get saved view
*/
export const invalidateGetSavedView = async (
queryClient: QueryClient,
{ viewId }: GetSavedViewPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetSavedViewQueryKey({ viewId }) },
options,
);
return queryClient;
};
/**
* Replaces a saved view's name and query.
* @summary Update saved view
*/
export const updateSavedView = (
{ viewId }: UpdateSavedViewPathParameters,
savedviewtypesPostableSavedViewDTO?: BodyType<SavedviewtypesPostableSavedViewDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/saved_views/${viewId}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: savedviewtypesPostableSavedViewDTO,
signal,
});
};
export const getUpdateSavedViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateSavedView>>,
TError,
{
pathParams: UpdateSavedViewPathParameters;
data?: BodyType<SavedviewtypesPostableSavedViewDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateSavedView>>,
TError,
{
pathParams: UpdateSavedViewPathParameters;
data?: BodyType<SavedviewtypesPostableSavedViewDTO>;
},
TContext
> => {
const mutationKey = ['updateSavedView'];
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 updateSavedView>>,
{
pathParams: UpdateSavedViewPathParameters;
data?: BodyType<SavedviewtypesPostableSavedViewDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateSavedView(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateSavedViewMutationResult = NonNullable<
Awaited<ReturnType<typeof updateSavedView>>
>;
export type UpdateSavedViewMutationBody =
| BodyType<SavedviewtypesPostableSavedViewDTO>
| undefined;
export type UpdateSavedViewMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update saved view
*/
export const useUpdateSavedView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateSavedView>>,
TError,
{
pathParams: UpdateSavedViewPathParameters;
data?: BodyType<SavedviewtypesPostableSavedViewDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateSavedView>>,
TError,
{
pathParams: UpdateSavedViewPathParameters;
data?: BodyType<SavedviewtypesPostableSavedViewDTO>;
},
TContext
> => {
return useMutation(getUpdateSavedViewMutationOptions(options));
};

View File

@@ -8920,6 +8920,109 @@ export interface RuletypesRuleDTO {
export enum RuletypesThresholdKindDTO {
basic = 'basic',
}
export enum SavedviewtypesPanelTypeDTO {
value = 'value',
graph = 'graph',
table = 'table',
list = 'list',
trace = 'trace',
}
export enum SavedviewtypesQueryTypeDTO {
builder = 'builder',
clickhouse_sql = 'clickhouse_sql',
promql = 'promql',
}
export interface SavedviewtypesCompositeQueryDTO {
panelType: SavedviewtypesPanelTypeDTO;
/**
* @type array,null
*/
queries: Querybuildertypesv5QueryEnvelopeDTO[] | null;
queryType: SavedviewtypesQueryTypeDTO;
}
export interface SavedviewtypesDisplayDTO {
/**
* @type string
*/
color?: string;
/**
* @type string
*/
fontSize?: string;
/**
* @type string
*/
format?: string;
/**
* @type integer
*/
maxLines?: number;
}
export enum SavedviewtypesSourcePageDTO {
traces = 'traces',
logs = 'logs',
metrics = 'metrics',
meter = 'meter',
}
export interface SavedviewtypesSavedViewSpecDTO {
compositeQuery: SavedviewtypesCompositeQueryDTO;
display?: SavedviewtypesDisplayDTO;
/**
* @type array
*/
selectedFields?: TelemetrytypesTelemetryFieldKeyDTO[];
}
export interface SavedviewtypesGettableSavedViewDTO {
/**
* @type string
* @format date-time
*/
createdAt: string;
/**
* @type string
*/
createdBy: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
schemaVersion: string;
sourcePage: SavedviewtypesSourcePageDTO;
spec: SavedviewtypesSavedViewSpecDTO;
/**
* @type string
* @format date-time
*/
updatedAt: string;
/**
* @type string
*/
updatedBy: string;
}
export interface SavedviewtypesPostableSavedViewDTO {
/**
* @type string
*/
name: string;
/**
* @type string
*/
schemaVersion: string;
sourcePage: SavedviewtypesSourcePageDTO;
spec: SavedviewtypesSavedViewSpecDTO;
}
export interface ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO {
/**
* @type string
@@ -12046,6 +12149,57 @@ export type TestRule200 = {
status: string;
};
export type ListSavedViewsParams = {
/**
* @description undefined
*/
sourcePage?: SavedviewtypesSourcePageDTO;
/**
* @type string
* @description undefined
*/
name?: string;
};
export type ListSavedViews200 = {
/**
* @type array,null
*/
data: SavedviewtypesGettableSavedViewDTO[] | null;
/**
* @type string
*/
status: string;
};
export type CreateSavedView200 = {
/**
* @type string,null
*/
data: string | null;
/**
* @type string
*/
status: string;
};
export type DeleteSavedViewPathParameters = {
viewId: string;
};
export type GetSavedViewPathParameters = {
viewId: string;
};
export type GetSavedView200 = {
data: SavedviewtypesGettableSavedViewDTO;
/**
* @type string
*/
status: string;
};
export type UpdateSavedViewPathParameters = {
viewId: string;
};
export type GetSessionContext200 = {
data: AuthtypesSessionContextDTO;
/**

View File

@@ -25,6 +25,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/promote"
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/savedview"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
@@ -75,6 +76,7 @@ type provider struct {
rulerHandler ruler.Handler
llmPricingRuleHandler llmpricingrule.Handler
statsHandler statsreporter.Handler
savedViewHandler savedview.Handler
}
func NewFactory(
@@ -110,6 +112,7 @@ func NewFactory(
traceDetailHandler tracedetail.Handler,
rulerHandler ruler.Handler,
statsHandler statsreporter.Handler,
savedViewHandler savedview.Handler,
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
return newProvider(
@@ -148,6 +151,7 @@ func NewFactory(
traceDetailHandler,
rulerHandler,
statsHandler,
savedViewHandler,
)
})
}
@@ -188,6 +192,7 @@ func newProvider(
traceDetailHandler tracedetail.Handler,
rulerHandler ruler.Handler,
statsHandler statsreporter.Handler,
savedViewHandler savedview.Handler,
) (apiserver.APIServer, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
router := mux.NewRouter().UseEncodedPath()
@@ -227,6 +232,7 @@ func newProvider(
rulerHandler: rulerHandler,
llmPricingRuleHandler: llmPricingRuleHandler,
statsHandler: statsHandler,
savedViewHandler: savedViewHandler,
}
provider.authzMiddleware = middleware.NewAuthZ(settings.Logger(), orgGetter, authzService)
@@ -359,6 +365,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addSavedViewRoutes(router); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,101 @@
package signozapiserver
import (
"net/http"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
func (provider *provider) addSavedViewRoutes(router *mux.Router) error {
if err := router.Handle("/api/v2/saved_views", handler.New(provider.authzMiddleware.ViewAccess(provider.savedViewHandler.List), handler.OpenAPIDef{
ID: "ListSavedViews",
Tags: []string{"saved_view"},
Summary: "List saved views",
Description: "Returns saved views for the calling user's org, optionally filtered by source page and name.",
Request: nil,
RequestQuery: new(savedviewtypes.ListSavedViewsParams),
RequestContentType: "",
Response: new([]*savedviewtypes.GettableSavedView),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/saved_views", handler.New(provider.authzMiddleware.EditAccess(provider.savedViewHandler.Create), handler.OpenAPIDef{
ID: "CreateSavedView",
Tags: []string{"saved_view"},
Summary: "Create saved view",
Description: "Persists a saved view for the explore page. Returns the id of the created view.",
Request: new(savedviewtypes.PostableSavedView),
RequestContentType: "application/json",
Response: new(valuer.UUID),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/saved_views/{viewId}", handler.New(provider.authzMiddleware.ViewAccess(provider.savedViewHandler.Get), handler.OpenAPIDef{
ID: "GetSavedView",
Tags: []string{"saved_view"},
Summary: "Get saved view",
Description: "Returns a saved view by id.",
Request: nil,
RequestContentType: "",
Response: new(savedviewtypes.GettableSavedView),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
})).Methods(http.MethodGet).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/saved_views/{viewId}", handler.New(provider.authzMiddleware.EditAccess(provider.savedViewHandler.Update), handler.OpenAPIDef{
ID: "UpdateSavedView",
Tags: []string{"saved_view"},
Summary: "Update saved view",
Description: "Replaces a saved view's name and query.",
Request: new(savedviewtypes.UpdatableSavedView),
RequestContentType: "application/json",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v2/saved_views/{viewId}", handler.New(provider.authzMiddleware.EditAccess(provider.savedViewHandler.Delete), handler.OpenAPIDef{
ID: "DeleteSavedView",
Tags: []string{"saved_view"},
Summary: "Delete saved view",
Description: "Deletes a saved view by id.",
Request: nil,
RequestContentType: "",
Response: nil,
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
})).Methods(http.MethodDelete).GetError(); err != nil {
return err
}
return nil
}

View File

@@ -2,15 +2,15 @@ package implsavedview
import (
"context"
"encoding/json"
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/savedview"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
@@ -33,14 +33,9 @@ func (handler *handler) Create(w http.ResponseWriter, r *http.Request) {
return
}
var view v3.SavedView
if err := json.NewDecoder(r.Body).Decode(&view); err != nil {
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode request body"))
return
}
// validate the query
if err := view.Validate(); err != nil {
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to validate request body"))
var view savedviewtypes.PostableSavedView
if err := binding.JSON.BindBody(r.Body, &view); err != nil {
render.Error(w, err)
return
}
@@ -95,14 +90,9 @@ func (handler *handler) Update(w http.ResponseWriter, r *http.Request) {
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse view id"))
return
}
var view v3.SavedView
if err := json.NewDecoder(r.Body).Decode(&view); err != nil {
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode request body"))
return
}
// validate the query
if err := view.Validate(); err != nil {
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to validate request body"))
var view savedviewtypes.UpdatableSavedView
if err := binding.JSON.BindBody(r.Body, &view); err != nil {
render.Error(w, err)
return
}
@@ -112,7 +102,7 @@ func (handler *handler) Update(w http.ResponseWriter, r *http.Request) {
return
}
render.Success(w, http.StatusOK, view)
render.Success(w, http.StatusOK, nil)
}
func (handler *handler) Delete(w http.ResponseWriter, r *http.Request) {
@@ -151,11 +141,17 @@ func (handler *handler) List(w http.ResponseWriter, r *http.Request) {
return
}
sourcePage := r.URL.Query().Get("sourcePage")
name := r.URL.Query().Get("name")
category := r.URL.Query().Get("category")
params := new(savedviewtypes.ListSavedViewsParams)
if err := binding.Query.BindQuery(r.URL.Query(), params); err != nil {
render.Error(w, err)
return
}
if err := params.Validate(); err != nil {
render.Error(w, err)
return
}
queries, err := handler.module.GetViewsForFilters(r.Context(), claims.OrgID, sourcePage, name, category)
queries, err := handler.module.GetViewsForFilters(r.Context(), claims.OrgID, params.SourcePage, params.Name)
if err != nil {
render.Error(w, err)
return

View File

@@ -0,0 +1,234 @@
package implsavedview
import (
"context"
"encoding/json"
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
// legacyExtraData mirrors the frontend's ad hoc extraData JSON shape
// (frontend/src/container/ExplorerOptions/ExplorerOptions.tsx) so /api/v1
// requests can be losslessly folded into the typed spec, and so /api/v1
// responses can synthesize the same shape back for the legacy frontend.
type legacyExtraData struct {
Color string `json:"color,omitempty"`
SelectColumns []telemetrytypes.TelemetryFieldKey `json:"selectColumns,omitempty"`
Format string `json:"format,omitempty"`
MaxLines int `json:"maxLines,omitempty"`
FontSize string `json:"fontSize,omitempty"`
}
func newPostableSavedViewFromV3(v *v3.SavedView) savedviewtypes.PostableSavedView {
var legacy legacyExtraData
if v.ExtraData != "" {
// Best-effort: malformed/older extraData shapes never fail the request,
// they just leave selectedFields/display empty.
_ = json.Unmarshal([]byte(v.ExtraData), &legacy)
}
return savedviewtypes.PostableSavedView{
Name: v.Name,
SourcePage: savedviewtypes.SourcePage{String: valuer.NewString(v.SourcePage)},
SavedViewData: savedviewtypes.SavedViewData{
SchemaVersion: savedviewtypes.SavedViewSchemaVersion,
Spec: savedviewtypes.SavedViewSpec{
CompositeQuery: &savedviewtypes.CompositeQuery{
PanelType: savedviewtypes.PanelType{String: valuer.NewString(string(v.CompositeQuery.PanelType))},
QueryType: savedviewtypes.QueryType{String: valuer.NewString(string(v.CompositeQuery.QueryType))},
Queries: v.CompositeQuery.Queries,
},
SelectedFields: legacy.SelectColumns,
Display: savedviewtypes.Display{
MaxLines: legacy.MaxLines,
FontSize: legacy.FontSize,
Format: legacy.Format,
Color: legacy.Color,
},
},
},
}
}
func newV3SavedViewFromGettable(v *savedviewtypes.GettableSavedView) (*v3.SavedView, error) {
extraData, err := json.Marshal(legacyExtraData{
Color: v.Spec.Display.Color,
SelectColumns: v.Spec.SelectedFields,
Format: v.Spec.Display.Format,
MaxLines: v.Spec.Display.MaxLines,
FontSize: v.Spec.Display.FontSize,
})
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in marshalling extra data")
}
return &v3.SavedView{
ID: v.ID,
Name: v.Name,
CreatedAt: v.CreatedAt,
CreatedBy: v.CreatedBy,
UpdatedAt: v.UpdatedAt,
UpdatedBy: v.UpdatedBy,
SourcePage: v.SourcePage.StringValue(),
CompositeQuery: &v3.CompositeQuery{
PanelType: v3.PanelType(v.Spec.CompositeQuery.PanelType.StringValue()),
QueryType: v3.QueryType(v.Spec.CompositeQuery.QueryType.StringValue()),
Queries: v.Spec.CompositeQuery.Queries,
},
ExtraData: string(extraData),
}, nil
}
func newV3SavedViewsFromGettable(views []*savedviewtypes.GettableSavedView) ([]*v3.SavedView, error) {
out := make([]*v3.SavedView, 0, len(views))
for _, view := range views {
v3View, err := newV3SavedViewFromGettable(view)
if err != nil {
return nil, err
}
out = append(out, v3View)
}
return out, nil
}
func (handler *handler) CreateV1(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
var v3View v3.SavedView
if err := json.NewDecoder(r.Body).Decode(&v3View); err != nil {
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode request body"))
return
}
if err := v3View.Validate(); err != nil {
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to validate request body"))
return
}
uuid, err := handler.module.CreateView(ctx, claims.OrgID, newPostableSavedViewFromV3(&v3View))
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, uuid)
}
func (handler *handler) GetV1(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
viewID := mux.Vars(r)["viewId"]
viewUUID, err := valuer.NewUUID(viewID)
if err != nil {
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse view id"))
return
}
view, err := handler.module.GetView(ctx, claims.OrgID, viewUUID)
if err != nil {
render.Error(w, err)
return
}
v3View, err := newV3SavedViewFromGettable(view)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, v3View)
}
func (handler *handler) UpdateV1(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
viewID := mux.Vars(r)["viewId"]
viewUUID, err := valuer.NewUUID(viewID)
if err != nil {
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse view id"))
return
}
var v3View v3.SavedView
if err := json.NewDecoder(r.Body).Decode(&v3View); err != nil {
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode request body"))
return
}
if err := v3View.Validate(); err != nil {
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to validate request body"))
return
}
err = handler.module.UpdateView(ctx, claims.OrgID, viewUUID, newPostableSavedViewFromV3(&v3View))
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, nil)
}
func (handler *handler) ListV1(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(w, err)
return
}
params := new(savedviewtypes.ListSavedViewsParams)
if err := binding.Query.BindQuery(r.URL.Query(), params); err != nil {
render.Error(w, err)
return
}
if err := params.Validate(); err != nil {
render.Error(w, err)
return
}
views, err := handler.module.GetViewsForFilters(r.Context(), claims.OrgID, params.SourcePage, params.Name)
if err != nil {
render.Error(w, err)
return
}
v3Views, err := newV3SavedViewsFromGettable(views)
if err != nil {
render.Error(w, err)
return
}
render.Success(w, http.StatusOK, v3Views)
}

View File

@@ -2,15 +2,10 @@ package implsavedview
import (
"context"
"encoding/json"
"strings"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/modules/savedview"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -24,144 +19,76 @@ func NewModule(sqlstore sqlstore.SQLStore) savedview.Module {
return &module{sqlstore: sqlstore}
}
func (module *module) GetViewsForFilters(ctx context.Context, orgID string, sourcePage string, name string, category string) ([]*v3.SavedView, error) {
var views []savedviewtypes.SavedView
var err error
if len(category) == 0 {
err = module.sqlstore.BunDB().NewSelect().Model(&views).Where("org_id = ? AND source_page = ? AND name LIKE ?", orgID, sourcePage, "%"+name+"%").Scan(ctx)
} else {
err = module.sqlstore.BunDB().NewSelect().Model(&views).Where("org_id = ? AND source_page = ? AND category LIKE ? AND name LIKE ?", orgID, sourcePage, "%"+category+"%", "%"+name+"%").Scan(ctx)
}
func (module *module) GetViewsForFilters(ctx context.Context, orgID string, sourcePage savedviewtypes.SourcePage, name string) ([]*savedviewtypes.GettableSavedView, error) {
var views []*savedviewtypes.StorableSavedView
err := module.sqlstore.BunDB().NewSelect().Model(&views).
Where("org_id = ? AND source_page = ? AND name LIKE ?", orgID, sourcePage, "%"+name+"%").
Scan(ctx)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in getting saved views")
}
var savedViews []*v3.SavedView
for _, view := range views {
var compositeQuery v3.CompositeQuery
err = json.Unmarshal([]byte(view.Data), &compositeQuery)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in unmarshalling explorer query data: %s", err.Error())
}
savedViews = append(savedViews, &v3.SavedView{
ID: view.ID,
Name: view.Name,
CreatedAt: view.CreatedAt,
CreatedBy: view.CreatedBy,
UpdatedAt: view.UpdatedAt,
UpdatedBy: view.UpdatedBy,
Tags: strings.Split(view.Tags, ","),
SourcePage: view.SourcePage,
CompositeQuery: &compositeQuery,
ExtraData: view.ExtraData,
})
}
return savedViews, nil
return savedviewtypes.NewGettableSavedViewsFromStorable(views), nil
}
func (module *module) CreateView(ctx context.Context, orgID string, view v3.SavedView) (valuer.UUID, error) {
data, err := json.Marshal(view.CompositeQuery)
func (module *module) CreateView(ctx context.Context, orgID string, view savedviewtypes.PostableSavedView) (valuer.UUID, error) {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return valuer.UUID{}, errors.WrapInternalf(err, errors.CodeInternal, "error in marshalling explorer query data")
}
uuid := valuer.GenerateUUID()
createdAt := time.Now()
updatedAt := time.Now()
claims, errv2 := authtypes.ClaimsFromContext(ctx)
if errv2 != nil {
return valuer.UUID{}, errors.NewInternalf(errors.CodeInternal, "error in getting email from context")
}
createBy := claims.Email
updatedBy := claims.Email
dbView := savedviewtypes.NewStorableSavedView(orgID, claims.Email, claims.Email, view)
dbView := savedviewtypes.SavedView{
TimeAuditable: types.TimeAuditable{
CreatedAt: createdAt,
UpdatedAt: updatedAt,
},
UserAuditable: types.UserAuditable{
CreatedBy: createBy,
UpdatedBy: updatedBy,
},
OrgID: orgID,
Identifiable: types.Identifiable{
ID: uuid,
},
Name: view.Name,
Category: view.Category,
SourcePage: view.SourcePage,
Tags: strings.Join(view.Tags, ","),
Data: string(data),
ExtraData: view.ExtraData,
}
_, err = module.sqlstore.BunDB().NewInsert().Model(&dbView).Exec(ctx)
_, err = module.sqlstore.BunDB().NewInsert().Model(dbView).Exec(ctx)
if err != nil {
return valuer.UUID{}, errors.WrapInternalf(err, errors.CodeInternal, "error in creating saved view")
}
return uuid, nil
return dbView.ID, nil
}
func (module *module) GetView(ctx context.Context, orgID string, uuid valuer.UUID) (*v3.SavedView, error) {
var view savedviewtypes.SavedView
func (module *module) GetView(ctx context.Context, orgID string, uuid valuer.UUID) (*savedviewtypes.GettableSavedView, error) {
var view savedviewtypes.StorableSavedView
err := module.sqlstore.BunDB().NewSelect().Model(&view).Where("org_id = ? AND id = ?", orgID, uuid.StringValue()).Scan(ctx)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in getting saved view")
}
var compositeQuery v3.CompositeQuery
err = json.Unmarshal([]byte(view.Data), &compositeQuery)
if err != nil {
return nil, errors.WrapInternalf(err, errors.CodeInternal, "error in unmarshalling explorer query data")
}
return &v3.SavedView{
ID: view.ID,
Name: view.Name,
Category: view.Category,
CreatedAt: view.CreatedAt,
CreatedBy: view.CreatedBy,
UpdatedAt: view.UpdatedAt,
UpdatedBy: view.UpdatedBy,
SourcePage: view.SourcePage,
Tags: strings.Split(view.Tags, ","),
CompositeQuery: &compositeQuery,
ExtraData: view.ExtraData,
}, nil
return savedviewtypes.NewGettableSavedViewFromStorable(&view), nil
}
func (module *module) UpdateView(ctx context.Context, orgID string, uuid valuer.UUID, view v3.SavedView) error {
data, err := json.Marshal(view.CompositeQuery)
func (module *module) UpdateView(ctx context.Context, orgID string, uuid valuer.UUID, view savedviewtypes.UpdatableSavedView) error {
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "error in marshalling explorer query data")
}
claims, errv2 := authtypes.ClaimsFromContext(ctx)
if errv2 != nil {
return errors.NewInternalf(errors.CodeInternal, "error in getting email from context")
}
updatedAt := time.Now()
updatedBy := claims.Email
dbView := savedviewtypes.NewStorableSavedView(orgID, claims.Email, claims.Email, view)
_, err = module.sqlstore.BunDB().NewUpdate().
Model(&savedviewtypes.SavedView{}).
Set("updated_at = ?, updated_by = ?, name = ?, category = ?, source_page = ?, tags = ?, data = ?, extra_data = ?",
updatedAt, updatedBy, view.Name, view.Category, view.SourcePage, strings.Join(view.Tags, ","), data, view.ExtraData).
res, err := module.sqlstore.BunDB().NewUpdate().
Model(&savedviewtypes.StorableSavedView{}).
Set("updated_at = ?, updated_by = ?, name = ?, source_page = ?, data = ?",
dbView.UpdatedAt, dbView.UpdatedBy, dbView.Name, dbView.SourcePage, dbView.Data).
Where("id = ?", uuid.StringValue()).
Where("org_id = ?", orgID).
Exec(ctx)
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "error in updating saved view")
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return errors.WrapInternalf(err, errors.CodeInternal, "error in updating saved view")
}
if rowsAffected == 0 {
return errors.NewNotFoundf(savedviewtypes.ErrCodeSavedViewNotFound, "saved view %s not found", uuid.StringValue())
}
return nil
}
func (module *module) DeleteView(ctx context.Context, orgID string, uuid valuer.UUID) error {
_, err := module.sqlstore.BunDB().NewDelete().
Model(&savedviewtypes.SavedView{}).
Model(&savedviewtypes.StorableSavedView{}).
Where("id = ?", uuid.StringValue()).
Where("org_id = ?", orgID).
Exec(ctx)
@@ -172,7 +99,7 @@ func (module *module) DeleteView(ctx context.Context, orgID string, uuid valuer.
}
func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
savedViews := []*savedviewtypes.SavedView{}
savedViews := []*savedviewtypes.StorableSavedView{}
err := module.
sqlstore.

View File

@@ -4,19 +4,19 @@ import (
"context"
"net/http"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/statsreporter"
"github.com/SigNoz/signoz/pkg/types/savedviewtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Module interface {
GetViewsForFilters(ctx context.Context, orgID string, sourcePage string, name string, category string) ([]*v3.SavedView, error)
GetViewsForFilters(ctx context.Context, orgID string, sourcePage savedviewtypes.SourcePage, name string) ([]*savedviewtypes.GettableSavedView, error)
CreateView(ctx context.Context, orgID string, view v3.SavedView) (valuer.UUID, error)
CreateView(ctx context.Context, orgID string, view savedviewtypes.PostableSavedView) (valuer.UUID, error)
GetView(ctx context.Context, orgID string, uuid valuer.UUID) (*v3.SavedView, error)
GetView(ctx context.Context, orgID string, uuid valuer.UUID) (*savedviewtypes.GettableSavedView, error)
UpdateView(ctx context.Context, orgID string, uuid valuer.UUID, view v3.SavedView) error
UpdateView(ctx context.Context, orgID string, uuid valuer.UUID, view savedviewtypes.UpdatableSavedView) error
DeleteView(ctx context.Context, orgID string, uuid valuer.UUID) error
@@ -33,9 +33,22 @@ type Handler interface {
// Updates the saved view
Update(http.ResponseWriter, *http.Request)
// Deletes the saved view
// Deletes the saved view. Shared by both API generations -- delete has no
// request/response body to reshape.
Delete(http.ResponseWriter, *http.Request)
// Lists the saved views
List(http.ResponseWriter, *http.Request)
// CreateV1 is the /api/v1/explorer/views wire-compatible variant of Create.
CreateV1(http.ResponseWriter, *http.Request)
// GetV1 is the /api/v1/explorer/views wire-compatible variant of Get.
GetV1(http.ResponseWriter, *http.Request)
// UpdateV1 is the /api/v1/explorer/views wire-compatible variant of Update.
UpdateV1(http.ResponseWriter, *http.Request)
// ListV1 is the /api/v1/explorer/views wire-compatible variant of List.
ListV1(http.ResponseWriter, *http.Request)
}

View File

@@ -504,10 +504,10 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/dashboards/{id}/lock", am.EditAccess(aH.Signoz.Handlers.Dashboard.LockUnlock)).Methods(http.MethodPut)
router.HandleFunc("/api/v2/variables/query", am.ViewAccess(aH.queryDashboardVarsV2)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/explorer/views", am.ViewAccess(aH.Signoz.Handlers.SavedView.List)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/explorer/views", am.EditAccess(aH.Signoz.Handlers.SavedView.Create)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/explorer/views/{viewId}", am.ViewAccess(aH.Signoz.Handlers.SavedView.Get)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/explorer/views/{viewId}", am.EditAccess(aH.Signoz.Handlers.SavedView.Update)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/explorer/views", am.ViewAccess(aH.Signoz.Handlers.SavedView.ListV1)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/explorer/views", am.EditAccess(aH.Signoz.Handlers.SavedView.CreateV1)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/explorer/views/{viewId}", am.ViewAccess(aH.Signoz.Handlers.SavedView.GetV1)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/explorer/views/{viewId}", am.EditAccess(aH.Signoz.Handlers.SavedView.UpdateV1)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/explorer/views/{viewId}", am.EditAccess(aH.Signoz.Handlers.SavedView.Delete)).Methods(http.MethodDelete)
router.HandleFunc("/api/v1/event", am.ViewAccess(aH.registerEvent)).Methods(http.MethodPost)

View File

@@ -14,9 +14,9 @@ import (
"github.com/pkg/errors"
signozerrors "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/valuer"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/valuer"
)
type DataSource string
@@ -1429,17 +1429,17 @@ func (p *Point) UnmarshalJSON(data []byte) error {
// The source page could be "traces", "logs", "metrics".
type SavedView struct {
ID valuer.UUID `json:"id,omitempty"`
Name string `json:"name"`
Category string `json:"category"`
CreatedAt time.Time `json:"createdAt"`
CreatedBy string `json:"createdBy"`
UpdatedAt time.Time `json:"updatedAt"`
UpdatedBy string `json:"updatedBy"`
SourcePage string `json:"sourcePage"`
Tags []string `json:"tags"`
CompositeQuery *CompositeQuery `json:"compositeQuery"`
Name string `json:"name" required:"true"`
Category string `json:"category" required:"true"`
CreatedAt time.Time `json:"createdAt" required:"true"`
CreatedBy string `json:"createdBy" required:"true"`
UpdatedAt time.Time `json:"updatedAt" required:"true"`
UpdatedBy string `json:"updatedBy" required:"true"`
SourcePage string `json:"sourcePage" required:"true"`
Tags []string `json:"tags" required:"true" nullable:"true"`
CompositeQuery *CompositeQuery `json:"compositeQuery" required:"true"`
// ExtraData is JSON encoded data used by frontend to store additional data
ExtraData string `json:"extraData"`
ExtraData string `json:"extraData" required:"true"`
}
func (eq *SavedView) Validate() error {

View File

@@ -30,6 +30,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/promote"
"github.com/SigNoz/signoz/pkg/modules/rawdataexport"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/savedview"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/modules/session"
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
@@ -88,6 +89,7 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ tracedetail.Handler }{},
struct{ ruler.Handler }{},
struct{ statsreporter.Handler }{},
struct{ savedview.Handler }{},
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
if err != nil {
return nil, err

View File

@@ -221,6 +221,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewAddRoleTransactionGroupsFactory(sqlstore, sqlschema),
sqlmigration.NewAddTagUniqueIndexFactory(sqlstore, sqlschema),
sqlmigration.NewAddTelemetryTuplesFactory(sqlstore),
sqlmigration.NewRestructureSavedViewSpecFactory(sqlstore),
)
}
@@ -320,6 +321,7 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.TraceDetail,
handlers.RulerHandler,
handlers.StatsHandler,
handlers.SavedView,
),
)
}

View File

@@ -0,0 +1,140 @@
package sqlmigration
import (
"context"
"database/sql"
"encoding/json"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlstore"
)
type restructureSavedViewSpec struct {
store sqlstore.SQLStore
}
func NewRestructureSavedViewSpecFactory(store sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("restructure_saved_view_spec"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return &restructureSavedViewSpec{store: store}, nil
})
}
func (migration *restructureSavedViewSpec) Register(migrations *migrate.Migrations) error {
return migrations.Register(migration.Up, migration.Down)
}
// legacySavedViewCompositeQuery is the bare shape saved_views.data held
// before this migration -- just the composite query, no schemaVersion/spec
// envelope. Queries is kept as raw JSON since the migration only needs to
// relocate it, not interpret it.
type legacySavedViewCompositeQuery struct {
PanelType string `json:"panelType"`
QueryType string `json:"queryType"`
Queries json.RawMessage `json:"queries"`
}
// legacySavedViewExtraData mirrors the frontend's ad hoc extraData JSON shape
// (frontend/src/container/ExplorerOptions/ExplorerOptions.tsx).
type legacySavedViewExtraData struct {
Color string `json:"color,omitempty"`
SelectColumns json.RawMessage `json:"selectColumns,omitempty"`
Format string `json:"format,omitempty"`
MaxLines int `json:"maxLines,omitempty"`
FontSize string `json:"fontSize,omitempty"`
}
type newSavedViewDisplay struct {
MaxLines int `json:"maxLines,omitempty"`
FontSize string `json:"fontSize,omitempty"`
Format string `json:"format,omitempty"`
Color string `json:"color,omitempty"`
}
type newSavedViewSpec struct {
CompositeQuery legacySavedViewCompositeQuery `json:"compositeQuery"`
SelectedFields json.RawMessage `json:"selectedFields,omitempty"`
Display newSavedViewDisplay `json:"display,omitzero"`
}
type newSavedViewData struct {
SchemaVersion string `json:"schemaVersion"`
Spec newSavedViewSpec `json:"spec"`
}
func (migration *restructureSavedViewSpec) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
var savedViews []struct {
ID string `bun:"id"`
Data string `bun:"data"`
ExtraData string `bun:"extra_data"`
}
err = tx.NewSelect().
Table("saved_views").
Column("id", "data", "extra_data").
Scan(ctx, &savedViews)
if err != nil && err != sql.ErrNoRows {
return err
}
for _, savedView := range savedViews {
var compositeQuery legacySavedViewCompositeQuery
if err := json.Unmarshal([]byte(savedView.Data), &compositeQuery); err != nil {
continue // invalid/empty JSON -- leave the row alone rather than fail the whole migration
}
var extraData legacySavedViewExtraData
if savedView.ExtraData != "" {
// Best-effort: malformed/older extraData shapes never fail the
// migration, they just leave selectedFields/display empty.
_ = json.Unmarshal([]byte(savedView.ExtraData), &extraData)
}
dataJSON, err := json.Marshal(newSavedViewData{
SchemaVersion: "v2",
Spec: newSavedViewSpec{
CompositeQuery: compositeQuery,
SelectedFields: extraData.SelectColumns,
Display: newSavedViewDisplay{
MaxLines: extraData.MaxLines,
FontSize: extraData.FontSize,
Format: extraData.Format,
Color: extraData.Color,
},
},
})
if err != nil {
return err
}
_, err = tx.NewUpdate().
Table("saved_views").
Set("data = ?", string(dataJSON)).
Where("id = ?", savedView.ID).
Exec(ctx)
if err != nil {
return err
}
}
for _, column := range []string{"extra_data", "category", "tags"} {
if err := migration.store.Dialect().DropColumn(ctx, tx, "saved_views", column); err != nil {
return err
}
}
return tx.Commit()
}
func (migration *restructureSavedViewSpec) Down(context.Context, *bun.DB) error {
// this migration is not reversible as we're transforming the structure
return nil
}

View File

@@ -0,0 +1,137 @@
package savedviewtypes
import (
"github.com/SigNoz/signoz/pkg/errors"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
// PanelType is the explore-page panel a saved view renders as. It is local
// to savedviewtypes (not the legacy pkg/query-service/model/v3.PanelType)
// following the same pattern ruletypes.PanelType uses alongside
// qbtypes.QueryEnvelope for alerts.
type PanelType struct {
valuer.String
}
var (
PanelTypeValue = PanelType{valuer.NewString("value")}
PanelTypeGraph = PanelType{valuer.NewString("graph")}
PanelTypeTable = PanelType{valuer.NewString("table")}
PanelTypeList = PanelType{valuer.NewString("list")}
PanelTypeTrace = PanelType{valuer.NewString("trace")}
)
// Enum implements jsonschema.Enum; returns the acceptable values for PanelType.
func (PanelType) Enum() []any {
return []any{
PanelTypeValue,
PanelTypeGraph,
PanelTypeTable,
PanelTypeList,
PanelTypeTrace,
}
}
func (p PanelType) Validate() error {
switch p {
case PanelTypeValue, PanelTypeGraph, PanelTypeTable, PanelTypeList, PanelTypeTrace:
return nil
default:
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "invalid panel type: %s", p.StringValue())
}
}
// QueryType records which query-builder mode (builder/ClickHouse SQL/PromQL)
// the view was last edited in -- it is a UI-tab selector, not a technical
// query-execution detail (that's qbtypes.QueryEnvelope.Type, which lives per
// query in Queries and can differ across entries). Local to savedviewtypes,
// mirroring ruletypes.QueryType.
type QueryType struct {
valuer.String
}
var (
QueryTypeBuilder = QueryType{valuer.NewString("builder")}
QueryTypeClickHouseSQL = QueryType{valuer.NewString("clickhouse_sql")}
QueryTypePromQL = QueryType{valuer.NewString("promql")}
)
// Enum implements jsonschema.Enum; returns the acceptable values for QueryType.
func (QueryType) Enum() []any {
return []any{
QueryTypeBuilder,
QueryTypeClickHouseSQL,
QueryTypePromQL,
}
}
func (q QueryType) Validate() error {
switch q {
case QueryTypeBuilder, QueryTypeClickHouseSQL, QueryTypePromQL:
return nil
default:
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "invalid query type: %s", q.StringValue())
}
}
// CompositeQuery is the query a saved view persists: a v5 query envelope
// list plus the panel/query type needed to render and edit it. Unlike the
// legacy v3.CompositeQuery it replaces here, this is v5-only by
// construction -- no builderQueries/chQueries/promQueries/unit/fillGaps.
// Saved views are always built from builder queries (never raw ClickHouse
// SQL or PromQL), so this covers every saved view.
type CompositeQuery struct {
PanelType PanelType `json:"panelType" required:"true"`
QueryType QueryType `json:"queryType" required:"true"`
Queries []qbtypes.QueryEnvelope `json:"queries" required:"true"`
}
func (q *CompositeQuery) Validate() error {
if err := q.PanelType.Validate(); err != nil {
return err
}
if err := q.QueryType.Validate(); err != nil {
return err
}
return (&qbtypes.CompositeQuery{Queries: q.Queries}).Validate()
}
// Display holds view-rendering preferences (log line rendering, list badge
// color) that used to live in the frontend-owned, backend-opaque extraData
// JSON blob.
type Display struct {
MaxLines int `json:"maxLines,omitempty"`
FontSize string `json:"fontSize,omitempty"`
Format string `json:"format,omitempty"`
Color string `json:"color,omitempty"`
}
// SavedViewSpec is the typed content of a saved view, mirroring the
// dashboardtypes v2 spec pattern (schemaVersion + typed spec).
type SavedViewSpec struct {
CompositeQuery *CompositeQuery `json:"compositeQuery" required:"true"`
SelectedFields []telemetrytypes.TelemetryFieldKey `json:"selectedFields,omitempty"`
Display Display `json:"display,omitzero"`
}
func (s *SavedViewSpec) Validate() error {
if s.CompositeQuery == nil {
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "composite query is required")
}
return s.CompositeQuery.Validate()
}
// SavedViewSchemaVersion is the only schemaVersion this codebase writes or accepts.
const SavedViewSchemaVersion = "v2"
// SavedViewData is what's persisted in saved_views.data and what the v2 API
// wire types nest spec/schemaVersion under.
type SavedViewData struct {
SchemaVersion string `json:"schemaVersion" required:"true"`
Spec SavedViewSpec `json:"spec" required:"true"`
}

View File

@@ -1,31 +1,161 @@
package savedviewtypes
import (
"bytes"
"encoding/json"
"strings"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
type SavedView struct {
var (
ErrCodeSavedViewInvalidInput = errors.MustNewCode("saved_view_invalid_input")
ErrCodeSavedViewNotFound = errors.MustNewCode("saved_view_not_found")
)
type SourcePage struct {
valuer.String
}
var (
SourcePageTraces = SourcePage{valuer.NewString("traces")}
SourcePageLogs = SourcePage{valuer.NewString("logs")}
SourcePageMetrics = SourcePage{valuer.NewString("metrics")}
SourcePageMeter = SourcePage{valuer.NewString("meter")}
)
func (SourcePage) Enum() []any {
return []any{
SourcePageTraces,
SourcePageLogs,
SourcePageMetrics,
SourcePageMeter,
}
}
func (s SourcePage) Validate() error {
switch s {
case SourcePageTraces, SourcePageLogs, SourcePageMetrics, SourcePageMeter:
return nil
default:
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "invalid source page: %s", s.StringValue())
}
}
type GettableSavedView struct {
ID valuer.UUID `json:"id" required:"true"`
Name string `json:"name" required:"true"`
CreatedAt time.Time `json:"createdAt" required:"true"`
CreatedBy string `json:"createdBy" required:"true"`
UpdatedAt time.Time `json:"updatedAt" required:"true"`
UpdatedBy string `json:"updatedBy" required:"true"`
SourcePage SourcePage `json:"sourcePage" required:"true"`
SavedViewData
}
type PostableSavedView struct {
Name string `json:"name" required:"true"`
SourcePage SourcePage `json:"sourcePage" required:"true"`
SavedViewData
}
type UpdatableSavedView = PostableSavedView
func (p *PostableSavedView) UnmarshalJSON(data []byte) error {
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
type alias PostableSavedView
var tmp alias
if err := dec.Decode(&tmp); err != nil {
return errors.WrapInvalidInputf(err, ErrCodeSavedViewInvalidInput, "invalid saved view request body").WithAdditional(err.Error())
}
*p = PostableSavedView(tmp)
return p.Validate()
}
func (p *PostableSavedView) Validate() error {
if err := p.SourcePage.Validate(); err != nil {
return err
}
if p.SchemaVersion != SavedViewSchemaVersion {
return errors.NewInvalidInputf(ErrCodeSavedViewInvalidInput, "schemaVersion must be %q, got %q", SavedViewSchemaVersion, p.SchemaVersion)
}
return p.Spec.Validate()
}
type ListSavedViewsParams struct {
SourcePage SourcePage `query:"sourcePage"`
Name string `query:"name"`
}
func (p *ListSavedViewsParams) Validate() error {
if p.SourcePage.IsZero() {
return nil
}
return p.SourcePage.Validate()
}
// StorableSavedView has SavedViewData (schemaVersion + spec) stored
// JSON-encoded in Data.
type StorableSavedView struct {
bun.BaseModel `bun:"table:saved_views"`
types.Identifiable
types.TimeAuditable
types.UserAuditable
OrgID string `json:"orgId" bun:"org_id,notnull"`
Name string `json:"name" bun:"name,type:text,notnull"`
Category string `json:"category" bun:"category,type:text,notnull"`
SourcePage string `json:"sourcePage" bun:"source_page,type:text,notnull"`
Tags string `json:"tags" bun:"tags,type:text"`
Data string `json:"data" bun:"data,type:text,notnull"`
ExtraData string `json:"extraData" bun:"extra_data,type:text"`
OrgID string `json:"orgId" bun:"org_id,notnull"`
Name string `json:"name" bun:"name,type:text,notnull"`
SourcePage SourcePage `json:"sourcePage" bun:"source_page,type:text,notnull"`
Data SavedViewData `json:"data" bun:"data,type:text,notnull"`
}
func NewStatsFromSavedViews(savedViews []*SavedView) map[string]any {
func NewStorableSavedView(orgID string, createdBy string, updatedBy string, view PostableSavedView) *StorableSavedView {
now := time.Now()
return &StorableSavedView{
Identifiable: types.Identifiable{ID: valuer.GenerateUUID()},
TimeAuditable: types.TimeAuditable{CreatedAt: now, UpdatedAt: now},
UserAuditable: types.UserAuditable{CreatedBy: createdBy, UpdatedBy: updatedBy},
OrgID: orgID,
Name: view.Name,
SourcePage: view.SourcePage,
Data: view.SavedViewData,
}
}
func NewGettableSavedViewFromStorable(view *StorableSavedView) *GettableSavedView {
return &GettableSavedView{
ID: view.ID,
Name: view.Name,
CreatedAt: view.CreatedAt,
CreatedBy: view.CreatedBy,
UpdatedAt: view.UpdatedAt,
UpdatedBy: view.UpdatedBy,
SourcePage: view.SourcePage,
SavedViewData: view.Data,
}
}
func NewGettableSavedViewsFromStorable(views []*StorableSavedView) []*GettableSavedView {
out := make([]*GettableSavedView, 0, len(views))
for _, view := range views {
out = append(out, NewGettableSavedViewFromStorable(view))
}
return out
}
func NewStatsFromSavedViews(savedViews []*StorableSavedView) map[string]any {
stats := make(map[string]any)
for _, savedView := range savedViews {
key := "savedview.source." + strings.ToLower(string(savedView.SourcePage)) + ".count"
key := "savedview.source." + strings.ToLower(savedView.SourcePage.StringValue()) + ".count"
if _, ok := stats[key]; !ok {
stats[key] = int64(1)
} else {