Compare commits

...

52 Commits

Author SHA1 Message Date
Abhi Kumar
b1e9b935c5 chore(dashboards-v2): tidy panel-editor query helpers
- useLegendSeries: drop redundant optional chaining on panel.spec.
- usePanelTypeSwitch: default null queries to [] for getBuilderQueries.
- Remove the unused getPanelKindLabel util.
2026-06-26 13:58:41 +05:30
Abhi Kumar
d4849984da refactor(dashboards-v2): rename Formatting section to "Formatting & Units"
Serialize section header test ids (lowercase, spaces → dashes) so a
multi-word title doesn't break the data-testid, and update the test.
2026-06-26 13:58:00 +05:30
Abhi Kumar
1e841949ab feat(dashboards-v2): redesign span-gaps as a "Disconnect values" control
Replace the raw seconds input with a Never/Threshold toggle plus a
duration "Threshold value" field. The threshold is stored verbatim as a
duration string ("10m", "5s") — the wire format the backend expects — and
parsed back to seconds only for rendering and validation. Threads the
query step interval through the config pane to seed/floor the threshold,
and rejects invalid or below-step-interval entries inline (V1 parity).
2026-06-26 13:54:13 +05:30
Abhi Kumar
478b4b3cd0 fix(dashboards-v2): allow clearing the threshold value input
The threshold "Value" field was a controlled numeric input, so an
emptied field snapped back to 0 (Number("") is 0, not NaN) and the
seeded 0 could never be removed. Hold a local string so the field can
be cleared and edited; shared by all threshold row variants.
2026-06-26 13:45:55 +05:30
Abhi Kumar
811c7bba90 feat(dashboards-v2): add "Move out of section" panel action
The "Move to section" submenu only listed titled sections, so a panel
in a titled section couldn't be moved back to the untitled root. Add a
direct "Move out of section" action, shown when the panel sits in a
titled section and an untitled root section exists to receive it.
2026-06-26 13:45:41 +05:30
Abhi Kumar
5a5c10b333 fix(dashboards-v2): place toolbar-created panels in the root section
The top-right "New Panel" button creates a panel with no section context,
which createPanelOps resolved to the LAST section instead of the root.
Fall back to the first (root) section when no valid index is given; still
create an untitled section when the dashboard has none.
2026-06-26 13:45:26 +05:30
Abhi Kumar
ed8c8046f4 fix(dashboards-v2): disable panel types unsupported by the datasource
A new panel's builder is seeded with the kind's default signal, but
`spec.queries` stays empty until the query is modified — so the type
switcher saw an undefined datasource and never disabled incompatible
types (e.g. List on a metrics panel, which then breaks rendering).
Resolve the signal with a fallback to the kind's default signal so
compatibility is enforced from the first render.
2026-06-26 13:44:56 +05:30
Abhi Kumar
ec88a1560a refactor(dashboards-v2): let consumers control panel-type icon rendering
Store icon component references in PANEL_TYPES instead of pre-rendered
elements, so each consumer (selection modal, type switcher) decides the
icon size and other props at the render site. Renames constants.tsx ->
constants.ts now that it holds no JSX.
2026-06-26 03:58:53 +05:30
Abhi Kumar
27b73064c2 feat(dashboards-v2): reuse findFreeSlot for panel clone placement
Clone now places the copy beside the last row when it fits, else wraps
to a fresh row at the section bottom — matching the new-panel save path
instead of always starting a new row.
2026-06-26 03:44:26 +05:30
Abhi Kumar
55569c5986 feat(dashboards-v2): switch panel visualization type in the editor
Wire the panel-type switcher into the config pane: every kind now declares a
`visualization` section (with `switchPanelKind`) so the switcher is reachable
from any panel, VisualizationSection renders it, and ConfigPane threads the
current kind/signal and an onChange down through SectionSlot. The editor drives
the in-place kind switch via usePanelTypeSwitch, so switching is reversible
within an edit session and a first visit rebuilds the query + resets the spec
(carrying unit/decimals).
2026-06-26 01:41:45 +05:30
Abhi Kumar
9d9d6c9b93 feat(dashboards-v2): add the PanelTypeSwitcher control
A presentational visualization-type selector (built on ConfigSelect) that lists
the panel kinds and disables types whose supported signals exclude the panel's
current datasource (V1 parity — e.g. List needs logs/traces, not metrics). Rename
the Value option label to Number so the switcher (and the type-selection modal)
read consistently. Not yet rendered anywhere.
2026-06-26 01:41:45 +05:30
Abhi Kumar
2fe2744340 feat(dashboards-v2): add panel-type switch logic
Two building blocks for switching a panel's kind in place:

- getSwitchedPluginSpec: on a first-time switch to a new kind, rebuild the
  plugin spec from the kind's defaults, carrying over unit/decimals (and seeding
  List columns from the signal).
- usePanelTypeSwitch: a per-kind session cache that makes switches reversible
  (Table -> List -> Table restores the original query + spec) and rebuilds the
  query for the new panel type on a first visit.

Both are standalone and not yet wired into the editor.
2026-06-26 01:41:45 +05:30
Abhi Kumar
c512fbafc9 refactor(dashboards-v2): extend ConfigSelect for the type switcher
Add per-option `disabled` and let `icon` be an arbitrary `ReactNode` (not just
a `SegmentIconName`) so the upcoming panel-type switcher can disable unsupported
types and render panel-type glyphs. Also widen the ConfigSegmented group to full
width so segmented controls fill the config pane.
2026-06-26 01:41:45 +05:30
Abhi Kumar
aabf1fb18c chore: pr review changes 2026-06-26 01:41:45 +05:30
Abhi Kumar
757da5cd57 refactor(dashboards-v2): type panel signals as TelemetrytypesSignalDTO
Replace the query-builder `DataSource` enum with `TelemetrytypesSignalDTO`
across the panel signal plumbing — the panel definitions' `supportedSignals`,
the List columns editor/suggestions/defaults, and the editor query-sync and
column-seeding hooks — and make the signal non-optional where a panel always
resolves one. Drops the now-dead `DataSource` imports.
2026-06-26 01:41:42 +05:30
Abhi Kumar
b4fc02f18e refactor(dashboards-v2): address PR review feedback
- getPanelDefinition: make PanelRegistry total over PanelKind so it never
  returns undefined (a missing kind is now a compile error)
- toQueryEnvelopes/hasRunnableQueries: accept `... | null` and drop the
  `?? []` at call sites
- consolidate panel loading to a single `isFetching` signal (Panel,
  PreviewPane, PanelEditor)
- rename defaultDataSource -> signal; use TelemetrytypesSignalDTO enum for
  the List default columns
- useCreatePanel now owns the panel-type picker state, deduping the three
  add-panel triggers; rename pluginKind -> panelKind
- LabelThresholdRow: extract onSave into a callback; ThresholdsSection test
  uses userEvent
- PreviewPane: drop the optional chaining on the required display field
- trim over-verbose comments
2026-06-26 01:41:27 +05:30
Abhi Kumar
9295c384a8 docs(dashboards-v2): trim verbose comments to minimal-why style
Condense multi-paragraph block comments and drop comments that restate
the code across the new-panel editor surface, keeping only the non-obvious
"why" and concise JSDoc on functions and type fields. Comment-only — no
code, types, or test assertions changed.
2026-06-26 01:28:55 +05:30
Abhi Kumar
9ea086e6d8 feat(dashboards-v2): create new panels from the editor
Add a draft-first create flow: picking a panel type opens the editor at
`/panel/new?panelKind=…&layoutIndex=…` on an in-memory default panel, and
nothing is persisted until save — cancelling leaves the dashboard
untouched (V1 parity). On save a new panel is minted (uuid) and added to
the target section via `createPanelOps` (resolves the section, or creates
one when the dashboard has none).

All "Add panel" triggers (section header, empty section, empty dashboard,
toolbar) route through a single useCreatePanel hook + the V2 type picker;
the leftover V1 global modal and useAddPanelToSection are removed.

New panels seed sensible defaults from the kind's supported signals: the
query datasource (e.g. List → logs, not the unsupported metrics default)
and, for List, the signal's default columns — so the Columns control
isn't empty on first open. New panels always re-serialize their query for
the kind on save, so the persisted query is valid even if untouched.

echo "--- working tree after commits (should be only dev hack) ---"; git status --short
2026-06-26 01:12:07 +05:30
Abhi Kumar
a5c8f413ff style(dashboards-v2): format list panel header with oxfmt 2026-06-26 01:03:54 +05:30
Abhi Kumar
46c7f2c285 feat(dashboards-v2): move list columns editor below the query builder
Replace the config-pane Columns section with a dnd-kit reorderable editor
rendered beneath the query builder (V1 parity); sanitize selectFields to the
field-key DTO so saved columns drop non-contract keys (isIndexed).
2026-06-26 01:03:52 +05:30
Abhi Kumar
c6330182f2 feat(dashboards-v2): list columns editor with datasource column switch 2026-06-26 01:03:38 +05:30
Abhi Kumar
afe643647a feat(dashboards-v2): list panel kind (logs/traces) with row detail 2026-06-26 01:02:40 +05:30
Abhi kumar
853397a79e feat(dashboards-v2): create new panels from the editor (#11777)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
* feat(dashboards-v2): list panel kind (logs/traces) with row detail

* feat(dashboards-v2): list columns editor with datasource column switch

* feat(dashboards-v2): move list columns editor below the query builder

Replace the config-pane Columns section with a dnd-kit reorderable editor
rendered beneath the query builder (V1 parity); sanitize selectFields to the
field-key DTO so saved columns drop non-contract keys (isIndexed).

* style(dashboards-v2): format list panel header with oxfmt

* refactor(dashboards-v2): drop the unsupported-panel fallback

Every panel kind now has a renderer, so the "not yet supported in V2"
body is dead. Remove UnsupportedPanelBody and render the panel body
behind a plain `panelDefinition &&` guard. Also clean up the panel-kind
typing in Panel.tsx: `spec.plugin.kind` is non-optional and already a
PanelKind, so the `as unknown as` cast and `?.` chains go away.

* refactor(dashboards-v2): tighten the getPanelDefinition cast

Drop the dead `if (!kind)` guard (a partial registry already returns
undefined for absent keys) and reduce the `as unknown as` double-cast to
a single `as` — comparability clears it, so the unknown laundering was
unnecessary.

* refactor(dashboards-v2): narrow renderer props to the panel kind

PanelRendererProps<K> now carries a `panel` narrowed to kind K (via a
PluginOfKind helper that picks the variant from the generated plugin
union), so each renderer reads `panel.spec.plugin.spec` as its exact spec
DTO. Removes the per-renderer `as <Kind>SpecDTO` cast (and the now-dead
`?? {}` fallbacks) across all seven renderers; the single unavoidable
widening stays in getPanelDefinition.

* feat(dashboards-v2): create new panels from the editor

Add a draft-first create flow: picking a panel type opens the editor at
`/panel/new?panelKind=…&layoutIndex=…` on an in-memory default panel, and
nothing is persisted until save — cancelling leaves the dashboard
untouched (V1 parity). On save a new panel is minted (uuid) and added to
the target section via `createPanelOps` (resolves the section, or creates
one when the dashboard has none).

All "Add panel" triggers (section header, empty section, empty dashboard,
toolbar) route through a single useCreatePanel hook + the V2 type picker;
the leftover V1 global modal and useAddPanelToSection are removed.

New panels seed sensible defaults from the kind's supported signals: the
query datasource (e.g. List → logs, not the unsupported metrics default)
and, for List, the signal's default columns — so the Columns control
isn't empty on first open. New panels always re-serialize their query for
the kind on save, so the persisted query is valid even if untouched.

echo "--- working tree after commits (should be only dev hack) ---"; git status --short

* fix(dashboards-v2): place a new panel in the last row's free space

createPanelOps placed every new panel at x:0 on a fresh row, so a half-full
last row left its right half empty. Find the first free slot in the section's
last row (right of its panels) and only wrap to a new row when it can't fit.

* fix(uplot): center a single-item chart legend

isSingleRow read the container width from a ref inside useMemo, so it computed
once at mount (ref still null → width 0 → false) and only recovered if the
component happened to re-render. The uPlot legend re-renders on plot sync so it
recovered by luck; the Pie legend doesn't, leaving a single item clipped in a
narrow grid track. Measure the container with useResizeObserver instead.

* feat(dashboards-v2): unified panel empty, no-data and no-query states

Add a shared PanelMessage component (icon + title + description + optional
action) backing every non-chart panel state. PanelBody now shows a 'Nothing to
visualize yet' state for panels with no runnable query and a polished error
state with Retry. NoData becomes a 'No data in this time range' affordance with
an optional Retry, wired through a new optional refetch on the renderer props.

* fix(dashboards-v2): derive the preview "Plotted with" tag from the panel query

The editor preview hardcoded EQueryType.QUERY_BUILDER, so PromQL/ClickHouse
panels were mislabelled and list panels still showed the tag. Add a V2 PlotTag
(mirroring V1: hidden for list panels and before a query exists) fed by a new
getPanelQueryType util that reads the panel's V5 envelopes.

* feat(dashboards-v2): panel header title with description tooltip

Replace PanelHeader's ReactNode title with explicit name + description props,
rendering the description as an info-icon tooltip in the header instead of a
wrapper around the title. Panel passes them directly (no headerTitle memo).

* refactor(dashboards-v2): let consumers own the refetch loader signal

usePanelQuery returns the raw react-query isLoading; the dashboard panel and the
editor preview now compute isLoading || isFetching themselves, so each surface
decides whether a background refetch shows the full loader.

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

* fix(dashboards-v2): default a threshold's label to an empty string on save

* feat(dashboards-v2): show the panel header in the editor preview

Reuse PanelHeader in PreviewPane so the editor preview shows the panel
title, description tooltip, refetch spinner, and error/warning indicators,
with the body rendered flush beneath it like the dashboard grid panel.

Add a hideActions flag to PanelHeader to suppress the actions menu in the
editor: View/Edit/Clone/Delete don't apply there, and omitting panelActions
alone isn't enough since View/Edit/Download survive their own gates. Wire the
table/list header search through the preview as the grid does, and thread the
raw isFetching for the header's refetch spinner.

* feat(dashboards-v2): seed new panels with per-kind config defaults

A new panel was seeded with an empty plugin spec, so the config pane's
dropdowns and segmented controls (time scope, legend position, line style /
interpolation, fill mode) opened with nothing selected.

Derive a default plugin spec from each kind's declared sections via a new pure
buildDefaultPluginSpec helper and seed it in createDefaultPanel. Each default
equals the matching renderer fallback, so only the config-pane display changes,
not the rendered output. Controls whose empty state already reads as the chart
default (unit/decimals "auto", switches, numeric "Auto" inputs) are left unset.

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

* fix(dashboards-v2): preserve list columns across datasource round-trips

The List panel stores a single selectFields list, so switching the query
signal replaced the columns with the new datasource's defaults — going
logs -> traces -> logs discarded a customized logs selection.

Remember each signal's columns and restore them when switching back;
defaults still seed a signal the first time it's seen.

* docs(dashboards-v2): trim verbose comments to minimal-why style

Condense multi-paragraph block comments and drop comments that restate
the code across the new-panel editor surface, keeping only the non-obvious
"why" and concise JSDoc on functions and type fields. Comment-only — no
code, types, or test assertions changed.

* fix(dashboards-v2): show resource attributes like service.name in trace list panels

The List renderer only flattened nested resource/attribute maps for logs, so a
selected resource field such as service.name (nested under resources_string)
rendered N/A for traces even though the same data shows in V1. Flatten traces too.

* feat(dashboards-v2): open new List panels on a runnable logs query (V1 parity)

A new List panel opened blank: its seed query had an empty orderBy and lived only
in the builder, so the preview didn't run on open. Seed spec.queries at creation
with the V1 list logs query (orderBy timestamp desc) so the panel opens on logs,
pre-sorted, and the preview runs immediately; wire up useSeedNewListColumns so its
default columns (timestamp, body) are populated too. Switching to traces is handled
by the builder's list-view path.

* fix(dashboards-v2): drop all config-pane sections from the List panel

List columns are edited below the query builder, and Context Links isn't wanted in
the config pane, so the List panel now declares no config-pane sections.

* refactor(dashboards-v2): address PR review feedback

- getPanelDefinition: make PanelRegistry total over PanelKind so it never
  returns undefined (a missing kind is now a compile error)
- toQueryEnvelopes/hasRunnableQueries: accept `... | null` and drop the
  `?? []` at call sites
- consolidate panel loading to a single `isFetching` signal (Panel,
  PreviewPane, PanelEditor)
- rename defaultDataSource -> signal; use TelemetrytypesSignalDTO enum for
  the List default columns
- useCreatePanel now owns the panel-type picker state, deduping the three
  add-panel triggers; rename pluginKind -> panelKind
- LabelThresholdRow: extract onSave into a callback; ThresholdsSection test
  uses userEvent
- PreviewPane: drop the optional chaining on the required display field
- trim over-verbose comments

* refactor(dashboards-v2): type panel signals as TelemetrytypesSignalDTO

Replace the query-builder `DataSource` enum with `TelemetrytypesSignalDTO`
across the panel signal plumbing — the panel definitions' `supportedSignals`,
the List columns editor/suggestions/defaults, and the editor query-sync and
column-seeding hooks — and make the signal non-optional where a panel always
resolves one. Drops the now-dead `DataSource` imports.

* chore: pr review changes

* chore: pr review changes

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:48:20 +00:00
Vinicius Lourenço
bd526df11d feat(roles): rework the create/update pages (#11815)
* fix(styles): little fix for callout

I will upstream this fix eventually

* fix(settings): ensure tab content can use height: 100%

We will need this fix for the create/edit role page later during json mode

* refactor(authz): define the verbs that supports selectorId

* refactor(roles): drop old components

We will rewrite them

* refactor(roles): use css modules on role settings

* refactor(roles): rewrite delete role modal to use dialog component

* feat(roles): add initial type files

* feat(error-in-place): add testId to allow assert on future tests

* refactor(permission-denied-full-page): change the wording a little bit to match the API

This change was requested by Vikrant

* feat(service-account-drawer): add copy button for ID

Suggested by https://github.com/SigNoz/platform-pod/issues/2529#issuecomment-4776975323

* fix(service-account-drawer): not validating the permissions correctly for read/update

Found at https://github.com/SigNoz/platform-pod/issues/2529#issuecomment-4776975323

* feat(timezone): add little helper to allow format optional dates

* refactor(authz): export parse permission to get objectId when needed

* test(authz): add helper to grant by prefix

* test(test-utils): include tooltip provider

* feat(hooks): add hook to block navigation via history

* feat(roles): add hook to map API interface to own interface for easier integration

* feat(roles): add hook to fetch role permissions

* feat(roles): add config object around how to define each section for each resource

* feat(roles): add config for monaco when showing json

* feat(view-roles): add selected ids component

* feat(view-roles): add component row to render selected roles

* feat(view-roles): add card component to render each resource selected

* feat(view-roles): add overview component to view roles

* feat(view-roles): add readonly json viewer

* feat(view-roles): add view role page

* test(view-roles): add tests for view roles

* feat(create-edit-roles): add hook for form validation

* feat(create-edit-roles): add hook for unsaved changes

* feat(create-edit-roles): add hook for generic logic for create-edit

* feat(create-edit-roles): add component to render json

* feat(create-edit-roles): add component to add new selectors for a permission

* feat(create-edit-roles): add component to select scopes of a permission

* feat(create-edit-roles): add component render resources to be selected

* feat(create-edit-roles): add component render permission editor

* feat(create-edit-roles): add page to create/edit roles

* test(create-edit-roles): add tests for page and subcomponents

* feat(list-roles): refactor to css modules & add row click to open view role page

* fix(list-roles): change description when feature gate is disabled

* refactor(roles): removed action labels and use function instead

* refactor(authz): add todo about permission.config

* feat(roles): add routes for create/edit/view

* test(service-account): update missing tests

* fix(pr): address comments

* fix(pr): rename suffix of hooks from callbacks to actions

* fix(pr): remove all usages of getByText

* test(use-navigation-blocker): fix test
2026-06-25 17:24:16 +00:00
Nikhil Mantri
8ac07d3d37 feat(infra-monitoring): v2 onboarding/checks api (#11104)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore: baseline setup

* chore: endpoint detail update

* chore: added logic for hosts v3 api

* fix: bug fix

* chore: disk usage

* chore: added validate function

* chore: added some unit tests

* chore: return status as a string

* chore: yarn generate api

* chore: removed isSendingK8sAgentsMetricsCode

* chore: moved funcs

* chore: added validation on order by

* chore: added pods list logic

* chore: updated openapi yml

* chore: updated spec

* chore: pods api meta start time

* chore: nil pointer check

* chore: nil pointer dereference fix in req.Filter

* chore: added temporalities of metrics

* chore: added pods metrics temporality

* chore: unified composite key function

* chore: code improvements

* chore: added pods list api updates

* chore: hostStatusNone added for clarity that this field can be left empty as well in payload

* chore: yarn generate api

* chore: return errors from getMetadata and lint fix

* chore: return errors from getMetadata and lint fix

* chore: added hostName logic

* chore: modified getMetadata query

* chore: add type for response and files rearrange

* chore: warnings added passing from queryResponse warning to host lists response struct

* chore: added better metrics existence check

* chore: added a TODO remark

* chore: added required metrics check

* chore: distributed samples table to local table change for get metadata

* chore: frontend fix

* chore: endpoint correction

* chore: endpoint modification openapi

* chore: escape backtick to prevent sql injection

* chore: rearrage

* chore: improvements

* chore: validate order by to validate function

* chore: improved description

* chore: added TODOs and made filterByStatus a part of filter struct

* chore: ignore empty string hosts in get active hosts

* feat(infra-monitoring): v2 hosts list - return counts of active & inactive hosts for custom group by attributes (#10956)

* chore: add functionality for showing active and inactive counts in custom group by

* chore: bug fix

* chore: added subquery for active and total count

* chore: ignore empty string hosts in get active hosts

* fix: sinceUnixMilli for determining active hosts compute once per request

* chore: refactor code

* chore: rename HostsList -> ListHosts

* chore: rearrangement

* chore: inframonitoring types renaming

* chore: added types package

* chore: file structure further breakdown for clarity

* chore: comments correction

* chore: removed temporalities

* chore: pods code restructuring

* chore: comments resolve

* chore: added json tag required: true

* chore: removed pod metric temporalities

* chore: removed internal server error

* chore: added status unauthorized

* chore: remove a defensive nil map check, the function ensure non-nil map when err nil

* chore: cleanup and rename

* chore: make sort stable in case of tiebreaker by comparing composite group by keys

* chore: added types and constants

* chore: added specs for all component types

* chore: added attrs presence check function

* chore: added onboarding splits

* chore: regen api client for inframonitoring

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

* chore: added required tags

* chore: added support for pod phase unknown

* chore: removed pods - order by phase

* chore: improved api description to document -1 as no data in numeric fields

* fix: rebase fixes

* chore: added onboarding api

* chore: renamed method

* chore: get onboarding spec

* chore: simplify

* chore: readability improvement

* chore: added a note from otel

* chore: added a note from otel

* feat(infra-monitoring): v2 pods list apis - phase counts when custom grouping (#11088)

* chore: added phase counts feature

* chore: added queries for pod phase counts in custom group by

* chore: added unknown phase count

* fix: isPodUIDInGroupBy in buildPodRecords

* chore: 3 cte --> 2 cte

* chore: pod phase with local table of time series as counts

* chore: comment correction

* chore: corrected comment

* chore: value column for samples table added

* chore: removed query G for phase counts

* chore: rename variable

* chore: added PodPhaseNum constants to types

* chore: updated comment

* chore: onboarding specs updated to match v2 infra-monitoring apis

* chore: integration tests added

* chore: documentation future links added

* chore: added new metrics existence function + modified return types

* chore: not required parameter removal

* chore: reformatted integration tests

* chore: renamed onboarding -> checks

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ashwin Bhatkal <ashwin96@gmail.com>
2026-06-25 13:57:50 +00:00
Nageshbansal
9bab8e0ae2 docs(deploy): restructure migration guide and add Docker Swarm (#11851)
* docs(deploy): restructure migration guide and add Docker Swarm

* docs(deploy): title migration guide for both install script and deploy

* docs: fix the signoz docs troubleshooting guide url

* docs(deploy): point troubleshooting at Slack and Foundry issues
2026-06-25 11:48:37 +00:00
Nityananda Gohain
8040f222ad fix: enable opamp for llmpricing/spanmmapper and remove unit from config (#11285)
* fix: enable opamp for llmpricing

* fix: test files

* chore: cleanups

* fix: don't deploy to opamp if ff is disabled

* fix: enable spanmapper behild feature flag
2026-06-25 11:38:53 +00:00
Nikhil Mantri
a45212cb79 feat(infra-monitoring): remove requiredMetricsCheck from v2 APIs + integration tests for new querier warnings behaviour. (#11792)
* chore: metric_name required checks removed

* refactor(inframonitoring): drop required-metrics test, add test hosts_warnings

* chore: updated integration tests for pods

* chore: updated integration tests for volumes

* chore: updated integration tests for nodes

* chore: updated integration tests for clusters and deployments

* chore: updated statefulsets integration tests

* chore: updated integration tests for jobs and daemonsets

* chore: removed unwanted provisional comments

* test(inframonitoring): dedupe metric-availability warning fixtures, reuse existing datasets

* test(inframonitoring): isolate hosts metric-key-pair warning fixture

Scenario-2 shared host acc-h1 + the accuracy fixture with
test_hosts_value_accuracy, causing a deterministic CI failure (empty
warnings). Restore the dedicated kp-h1 fixture + deployment.environment
groupBy so the warning assertion never depends on data shared with
another test.

* chore: removed unimportant tests for same thing

* test(inframonitoring): drop obsolete metric-key-pair warning scenario

PR #11835 removed the "key X not found on metric" warning from the metric

statement builder (lexer-derived check can't tell a key from a value, so

$variables got false-flagged). The metric_key_pair_not_seen scenario asserted

that now-removed warning, so it fails deterministically after merging main.

Remove the scenario and its orphaned fixture; never-seen-metric coverage

("never been received", still emitted) stays.
2026-06-25 10:26:50 +00:00
Abir Roy
a609a4044c fix(ui): resolve monaco find widget clipping and flickering (#11826)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-06-24 20:11:11 +00:00
Nikhil Mantri
f78d98ea71 feat(metrics-explorer): move metric_name from path param to query param (#11745)
* chore: metricName to post body for POST /api/v2/metrics/{metric_name}/metadata

* chore: metricName to query param for GET /api/v2/metrics/{metric_name}/metadata

* chore: added metricName in api get metric attributes

* chore: highlights api modified

* chore: alerts api modified

* chore: dashboards api modified

* chore: description added for metric_name query params

* feat(metrics-explorer): integrate metricName query/body API change in frontend (#11818)

* feat(metrics-explorer): integrate metricName query/body API change in frontend

The metrics-explorer endpoints moved metric_name off the URL path: the
five GETs (attributes, metadata, highlights, alerts, dashboards) now take
a required `metricName` query param, and POST /metadata reads metricName
from the request body.

- Regenerate the orval client from the updated openapi spec, so the GET
  helpers build `/api/v2/metrics/<op>?metricName=...` (URL-encoded, so
  slashed cloud metric names work) and updateMetricMetadata posts to
  `/api/v2/metrics/metadata` with metricName in the body.
- Collapse the useGetMetricAttributes call to the single merged params
  object (metricName + start/end).
- Drop the now-removed pathParams wrapper from both updateMetricMetadata
  call sites; the payload builders already include metricName in the body.
- Update the Metadata test to assert metricName inside the request body.

* revert(metrics-explorer): drop slashed-metric-name band-aid guards

These two defensive guards were added as temporary workarounds for the
metric_name-with-slash bug (SigNoz/signoz#11527, #11528), which returned
200 + HTML instead of JSON. The root cause is fixed by moving metricName
to a query/body param, so the band-aids are no longer needed and revert
to the original intended code.

- MetricDetails.tsx: `!metricMetadataResponse?.data` -> `!metricMetadataResponse`
- AllAttributes.tsx: `?.data?.attributes` -> `?.data.attributes`

* chore: added description for metricName query params

---------

Co-authored-by: Ashwin Bhatkal <ashwin96@gmail.com>
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-06-24 19:40:09 +00:00
Ashwin Bhatkal
f60e5039be feat(dashboard-v2): toolbar repositioning, JSON editor & expandable variables bar (#11837)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat(dashboard-v2): json editor drawer to edit dashboard as raw JSON

Right-side drawer with a Monaco JSON editor (reusing the import-JSON theme),
a Format/Copy/Download/Reset toolbar, live JSON validation, and Apply via the
full-document updateDashboardV2 endpoint. Cmd/Ctrl+Enter applies; Esc closes.

* feat(dashboard-v2): grouped actions menu with clone, new section & edit-as-JSON

Regroup the actions dropdown into labelled Dashboard/Data/Layout groups, add
Clone dashboard (cloneDashboardV2 + navigate) and New section (useAddSection),
and surface an Edit as JSON button that opens the JSON editor drawer. The menu
trigger is a labelled "Actions" button with a 9-grid icon; the time selector
moves out of the actions row to the toolbar's second row.

* feat(dashboard-v2): description-on-hover and space-aware tag overflow

Collapse the dashboard description behind an info icon shown on hover, and move
tags inline after the title where they restrict to the available width and
collapse the remainder into a +N badge (reusing the alerts badge measuring).

* refactor(dashboard-v2): two-row toolbar with a floated, expandable variables bar

Second toolbar row floats the time-range selector top-right; the variables bar
flows beside it, collapsing to a single line with an inline +N trigger that hugs
the last visible pill. Expanding clears the float so the pills pack full-width
on the lines beneath the time selector (no stair-stepping). Overflow pills are
display:none but stay mounted (widths cached) so auto-selection and option
fetching keep driving the panels. Also centre the variable info icon, give the
pills a visible --l3-border (and drop the single-select's stray inner border so
it matches), and replace the toolbar's fuzzy drop shadow with a token hairline.

* feat(dashboard-v2): section title modal & scroll to the new section

New section now opens a title-entry modal instead of inserting a default-named
section, and the view scrolls the freshly created section into view once the
refetch renders it. Generalise the rename modal into a shared SectionTitleModal
reused by both create and rename.

* test(dashboard-v2): cover the JSON editor hook and drawer

useJsonEditor: seeding, live validation, format/reset, dirty tracking, apply
(no-op when clean/invalid, PUTs the narrowed body, error handling) and re-seed
on re-open. JsonEditorDrawer: toolbar/footer wiring, validation text, Apply
enablement, editor changes and Cmd/Ctrl+Enter — with Monaco and the hook mocked.

* refactor(dashboard-v2): extract generic useInlineOverflowCount hook

Address review on the variables bar: generalise the single-line overflow
measurement into hooks/useInlineOverflowCount (container of data-overflow-item
children, with gap/reserveWidth/enabled options) so it's reusable elsewhere, and
clarify the internal variable names (container/itemWidths/availableWidth/etc.).

* refactor(dashboard-v2): use descriptive names in useInlineOverflowCount

Replace single-letter/abbreviated locals (el, i, w, width/widths) with
itemElement/index/itemWidth/cachedWidth(s) inside the measure loop.
2026-06-24 12:54:34 +00:00
Vikrant Gupta
a483ef81a4 feat(authz): add transaction groups JSON schema (#11827)
* feat(authz): add transaction group schema and validations

* fix(authz): drop constant errorFormat param from wrapValidationError

unparam flagged wrapValidationError's errorFormat parameter since all
call sites passed the same "%s: %s". Inline the format and trim the
argument at each call site. No behavior change.

* feat(authz): better error handling

* chore(authz): suffix generated web settings schema with .schema.json

Rename webSettings.json to webSettings.schema.json to follow the JSON
Schema file-naming convention and match transactionGroups.schema.json.
Updates the generator output path, the json2ts input + banner in
package.json, and the generated banner comment.

* feat(authz): add schema titles
2026-06-24 11:27:19 +00:00
Abhi kumar
b9c107a851 fix(dashboards-v2): stop infinite render loop on dashboards with no variable selections (#11841)
selectVariableValues returned an inline `{}` fallback whenever a dashboard had
no stored selections. Zustand reads selectors through useSyncExternalStore,
which compares snapshots with Object.is, so a fresh object every call reads as a
perpetually-changed snapshot and React re-renders without end ("Maximum update
depth exceeded").

This surfaced specifically on fresh/empty dashboards: when a dashboard has
variables, the seeding effect in useVariableSelection populates the store with a
stable object and the loop never starts; with no variables that effect
early-returns, the entry stays undefined, and the selector mints a new `{}` on
every render. VariablesBar renders null in that case, but its hook still
subscribes, so the loop fires anyway.

Return a single module-level empty map so the snapshot is referentially stable.
2026-06-24 10:16:48 +00:00
Nikhil Soni
5f6cc4c297 feat(data-export): support client-provided offset in export_raw_data API (#11825)
* feat: add support for offset in export api

* chore: add tests similar to limit

* Remove unnecessary tests

This reverts commit 2cc123d34f.
2026-06-24 09:29:54 +00:00
Srikanth Chekuri
69e4c3c6f3 chore(querier): route metric reads to buffer/reduced tables behind a flag (#11801)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
Fixes https://github.com/SigNoz/engineering-pod/issues/5341
2026-06-24 07:20:57 +00:00
Srikanth Chekuri
bc8c36095c fix: do not warn filter variables as missing keys (#11835)
* fix: do not warn filter variables as missing keys
Fixes https://github.com/SigNoz/engineering-pod/issues/5481

* chore: add regression test
2026-06-24 06:12:20 +00:00
Vikrant Gupta
385135ba6c chore: add @H4ad as frontend codeowner for authz, members and service accounts (#11834)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-06-23 20:35:38 +00:00
Miguel Valdes
ea4ac08666 feat(deprecate): install script and promote foundry (#11811)
* feat(signoz): deprecate install script

* feat(signoz): include kind in migration doc

* feat(deprecation): remove legacy files and update migration guide

* feat(deprecation): update readme, no docker compose reference

* feat(deprecation): improved instructions

* feat(deprecation): updating docs based on feedback

* feat(deprecation): typos and redundant lines

* feat(deprecation): include checklist and update references

* feat(deprecation): restore install script

* feat(deprecation): include link in script
2026-06-23 20:09:07 +00:00
Vikrant Gupta
65cdf8b74d feat(user): add delete user API (#11833) 2026-06-23 19:13:13 +00:00
Ashwin Bhatkal
a4ff4093cd feat(dashboard-v2): variable settings editor UX polishes (#11775)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat(dashboard-v2): variable model, adapters, validation & cycle detection

* feat(dashboard-v2): variable editor building-block components

* feat(dashboard-v2): variable form with type tabs & per-type fields

* feat(dashboard-v2): variables list with drag reorder & inline delete

* refactor(dashboard-v2): migrate variables bar to the overhauled model
2026-06-23 12:16:00 +00:00
Abhi kumar
b2acf3e856 feat(dashboards-v2): config pane + section editors (#11770)
* feat(dashboards-v2): panel query hook with pagination and time window

* refactor(dashboards-v2): trim comment noise and use JSDoc for function docs

Reduce over-commenting flagged in review across the V2 dashboard code:
remove comments that restate the code, shrink verbose blocks to a line, and
keep only the non-obvious "why". Function/helper docs are written as JSDoc;
inline implementation notes stay as line comments.

* fix(dashboards-v2): harden list pagination against bad inputs

Clamp the page size and offset to finite, positive values before deriving
pageIndex/canNext so the pager can never produce NaN/Infinity/negative state,
and ignore a non-positive setPageSize so the size state stays valid. canNext
now uses `rowCount >= pageSize` to also cover an over-fetch.

Add edge-case tests: empty/non-raw response, full vs partial page, nextCursor,
goNext advancing the page, and a rejected zero page size.

* feat(dashboards-v2): config pane shell with reusable form controls

* feat(dashboards-v2): chart panel section editors

* feat(dashboards-v2): thresholds section editor with per-variant rows

* refactor(dashboards-v2): address config-pane review feedback

- Drop unnecessary optional chaining on the always-present panel spec
  (display / plugin / data); type the formatting cast via PanelFormattingSlice.
- Rename the section-lens read/write to get/update (+ slice helpers).
- Derive the histogram NumericBound from the buckets DTO.
- Use userEvent (via tests/test-utils) in the Axes/Buckets/Thresholds tests.
- Use the SigNoz Button for the legend-color Reset action.

* feat(dashboards-v2): table panel (#11771)

* feat(dashboards-v2): table panel renderer with record-table prep

* feat(dashboards-v2): resizable, persisted table columns

* fix(dashboards-v2): keep clickhouse table columns distinct

A scalar/table panel backed by a clickhouse_sql query collapsed every value
column onto the query name: getColName/getColId had no clickhouse handling,
so all value columns resolved to the query name ("A") and the row builder
overwrote each cell with the last column's value.

clickhouse_sql queries carry no aggregation metadata but the response column
holds the user's real SQL alias in col.name. Thread the request's
clickhouse_sql query names through, and name/key those columns by col.name so
each stays distinct. Ports the V2 half of the merged V1 fix (#11794).

* feat(dashboard-v2) - list panel (#11766)

* feat(dashboards-v2): prepare V5 raw tables for the list panel

* feat(dashboards-v2): list panel kind (logs/traces) with row detail

* feat(dashboards-v2): list columns editor with datasource column switch

* feat(dashboards-v2): move list columns editor below the query builder

Replace the config-pane Columns section with a dnd-kit reorderable editor
rendered beneath the query builder (V1 parity); sanitize selectFields to the
field-key DTO so saved columns drop non-contract keys (isIndexed).

* style(dashboards-v2): format list panel header with oxfmt

* refactor(dashboards-v2): address list-panel PR review feedback

- type the telemetry signal as TelemetrytypesSignalDTO (enum) and drop the
  raw-string comparisons across the list panel renderer, columns, row
  interaction, prep util, and the columns-switch hook
- make prepareRawTable's selectFields a required array; make the log
  drawer's selectedTab always defined (defaults to OVERVIEW)
- read/write the list spec via spec.plugin.spec (plugin is non-optional)
- move isPlainObject (lodash) / coerceToString / toFiniteNumber out of
  rawRowToILog into shared utils
- replace the columns editor's hand-built popover + native buttons with
  @signozhq/ui Button and a searchable Combobox
- switch ListColumnsEditor tests to userEvent; trim verbose comments

* refactor(dashboards-v2): de-duplicate panel cell + column-unit helpers

- collapse the per-panel stringifyCell copies onto the shared coerceToString
  util (list + table column builders)
- extract the column-unit resolver to Panels/utils/getColumnUnit; the table
  renderer and the editor's column options now share one implementation,
  aligning the editor onto the renderer's "empty entry = no unit" semantics
- export a single TableRowData type from tableColumns (with the antd key)
  instead of two divergent local copies

* refactor(dashboards-v2): clean up comments across ListPanel components

* fix(dashboards-v2): drop duplicated list-pagination test block

The `describe('list pagination')` block in usePanelQuery.test.tsx was
duplicated byte-for-byte, tripping oxlint sonarjs(no-identical-functions)
on the repeated rawResponse/withResponse helpers and failing lint CI.
2026-06-23 11:20:16 +00:00
Abhi kumar
949d18f028 feat(dashboards-v2): panel editor foundation + qb/perses adapter (#11769)
* feat(dashboards-v2): pure-V5 perses query adapter and request builder

* feat(dashboards-v2): panel query hook with pagination and time window

* refactor(dashboards-v2): per-kind panel definitions and registry

* feat(dashboards-v2): role and kind-gated panel actions with header chrome

* feat(dashboards-v2): panel editor route with live preview and query builder

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

* fix(dashboards-v2): make the editor dirty-check immune to query re-serialization

* fix(dashboards-v2): persist raw/list panels as a bare BuilderQuery

* feat(dashboards-v2): optional footer slot below the query builder

* refactor(dashboards-v2): render the editor preview through the shared panel body

The editor preview duplicated PanelBody's loading/error/renderer state
machine. Delegate to PanelBody so the preview is the production render
path, differing only by panelMode (DASHBOARD_EDIT) and the forwarded
server pager. PanelBody's panelMode and dashboardPreference become
optional (the preview has no dashboard-wide preferences), and its error
state now surfaces the backend message via panelStatusFromError instead
of the raw axios "status code 4xx".

* feat(dashboards-v2): richer panel status popover for errors and warnings

Replace the plain status tooltip with a card: variant-coloured icon, the
error/warning code and message, an optional Open Docs link, a MESSAGES
count pill, and the per-item message list. Hosted in @signozhq/ui
TooltipSimple with its padding/width cap stripped so the card owns its
own layout. Bump the header actions gap so the status icon sits clear of
its neighbours.

* refactor(dashboards-v2): address panel-editor foundation PR review

- fix failing lint: drop the unused Typography import in PanelStatusContent
- drop redundant optional chaining on the required spec/plugin (usePanelQuery,
  PanelEditor index, HistogramPanel sections)
- move ComparisonThresholdShape into types/threshold
- derive PanelActionId from keyof PanelActionCapabilities
- fold the per-kind header search flag into actions (drop PanelHeaderControls)
- default PanelHeader searchTerm to ''
- drive the editor's discard prompt through useConfirmableAction
- trim over-verbose comments; PanelHeader test uses userEvent

* refactor(dashboards-v2): trim comment noise and use JSDoc for function docs

Reduce over-commenting flagged in review across the V2 dashboard code:
remove comments that restate the code, shrink verbose blocks to a line, and
keep only the non-obvious "why". Function/helper docs are written as JSDoc;
inline implementation notes stay as line comments.

* fix(dashboards-v2): harden list pagination against bad inputs

Clamp the page size and offset to finite, positive values before deriving
pageIndex/canNext so the pager can never produce NaN/Infinity/negative state,
and ignore a non-positive setPageSize so the size state stays valid. canNext
now uses `rowCount >= pageSize` to also cover an over-fetch.

Add edge-case tests: empty/non-raw response, full vs partial page, nextCursor,
goNext advancing the page, and a rejected zero page size.

* refactor(dashboards-v2): localize the perses adapter's envelope casts

The adapter bridges the generated query-envelope DTO (enum type, undiscriminated
spec) and the hand-written QueryEnvelope (typed spec) the V1 mappers consume —
nominally distinct types for the same wire shape, so a structural cast is
unavoidable. Confine those casts to two named `*Envelopes` converters and a
builder-query predicate, keep an explicit typed checkpoint for the composite
spec, and correct the stale "Orval erases spec to unknown" comments.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:39:08 +00:00
Aditya Singh
8180436432 chore: update code owner (#11821) 2026-06-23 09:18:00 +00:00
Nityananda Gohain
ad243b88aa feat: send all data for trace list api (#10583)
* chore: send all data for trace list api

* chore: add integration tests

* chore: fix tests

* chore: add comment

Co-authored-by: Tushar Vats <tushar@signoz.io>

* fix: retain existing behaviour

* fix: add changes

* fix: linting issues

* fix: py-fmt

* fix: restrict merging to only span data

* fix: address comments

* fix: address comments

* fix: send parsed events and links

* fix: remove unnecessary tests

* fix: lint issues

* fix: send all data for trace operators as well

* fix: lint issues

* fix: move tests to the same file

* fix: tests

* fix: lint

* fix: comment

* fix: lint

---------

Co-authored-by: Tushar Vats <tushar@signoz.io>
2026-06-23 08:25:59 +00:00
Nityananda Gohain
3369ed7172 chore: add flag for ai observability (#11806)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore: add flag for ai observability

* chore: add enable prefix
2026-06-23 02:47:00 +00:00
Vikrant Gupta
a98b84c1cd feat(user): accept custom roles in user invite (#11802)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat(user): accept custom roles in user invite

* feat(user): use binding package

* feat(user): more domain restrictions

* feat(user): use suggestions

* feat(user): use suggestions

* feat(user): use pointer postable role
2026-06-22 20:09:36 +00:00
Ashwin Bhatkal
4dda1e0ab5 feat(dashboards): views-first V2 dashboards list with filters, saved views, and tabbed new-dashboard modal (#11682)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat(dashboard-v2): add persisted views store, types, and filter-query helpers

* feat(dashboard-v2): add filter state hook and filter zone

* feat(dashboard-v2): add views rail with save and dirty-state flow

* feat(dashboard-v2): add list status bar with rail collapse

* feat(dashboard-v2): add favorites and recently-viewed to dashboard rows

* feat(dashboard-v2): add persisted visible-columns store

* feat(dashboard-v2): add tabbed new-dashboard modal (blank / template / import)

* feat(dashboard-v2): full-width skeleton loading state

* feat(dashboard-v2): compose views, filters, and inline metadata into the list page

* chore(dashboard-v2): remove superseded create dropdown and standalone modals

* feat(dashboard-v2): add duplicate (clone) action to dashboard rows

* refactor(dashboards-v2): move toPostableTags to utils next to its inverse

* refactor(dashboards-v2): use signoz Button for view rows & delete action

* refactor(dashboards-v2): rename filterStatesEqual to areFilterStatesEqual
2026-06-22 13:06:05 +00:00
Ashwin Bhatkal
749943abe4 feat(dashboard-v2): runtime variable selection (#11646)
* feat(dashboard-v2): variable-selection store, dependency graph & sort helpers

* feat(dashboard-v2): runtime variables bar & per-type selectors

* feat(dashboard-v2): mount variables bar in dashboard toolbar
2026-06-22 11:36:43 +00:00
Tushar Vats
4f51ee37ba fix: modularize query range function (#11774) 2026-06-22 11:35:33 +00:00
Abhi kumar
d5617657b5 fix(dashboard): clickhouse table panel collapses value columns onto query name (#11794)
* fix(dashboard): clickhouse table panel collapses value columns onto query name

A table/scalar panel backed by a ClickHouse SQL query rendered every
aggregation column with the header "A" (the query name) and the same value in
each, while only the group columns (e.g. service.name) showed correctly.

Root cause: the scalar-response column-naming utils derive a value column's
display name and row-data key from request-side aggregation metadata, which
only exists for builder_query envelopes. A clickhouse_sql query has none, so
getColName/getColId fell through to the query name for every value column.
Sharing one id ("A") collapsed all value columns onto a single row key, so the
last column written (total_requests) overwrote the rest.

The backend already returns correct data: readAsScalar names each ClickHouse
SELECT column with its real SQL alias and a unique aggregationIndex. This is a
frontend-only consumption fix.

Fix: when a column belongs to a clickhouse_sql query (determined from the
request's query type, not a name heuristic), name and key it by the response
column's real SQL alias. Builder queries are unchanged; formulas/promql keep
the legend || queryName fallback. Applied to both the V1 converter
(convertV5Response.ts, the live table-panel path) and the V2 path
(prepareScalarTables.ts).

* chore: minor type fix
2026-06-22 08:31:06 +00:00
Nityananda Gohain
5600576722 chore: add search and override filters in pricing model list api (#11735) 2026-06-22 08:23:12 +00:00
Vikrant Gupta
f84b818552 feat(authz): add unified role APIs (#11798)
* feat(authz): add unified role APIs

* feat(authz): update openapi spec

* feat(authz): restructure the chunked write to the openfga server

* feat(authz): fix the order for minimal gitdiff

* feat(authz): update openapi spec

* feat(authz): fix the create API

* feat(authz): better error messages
2026-06-22 07:31:40 +00:00
651 changed files with 39319 additions and 11940 deletions

88
.github/CODEOWNERS vendored
View File

@@ -109,6 +109,25 @@ go.mod @therealpandey
/pkg/modules/role/ @therealpandey
/pkg/types/coretypes/ @therealpandey @vikrantgupta25
/frontend/src/hooks/useAuthZ/ @H4ad
/frontend/src/components/GuardAuthZ/ @H4ad
/frontend/src/components/AuthZTooltip/ @H4ad
/frontend/src/components/createGuardedRoute/ @H4ad
/frontend/src/container/RolesSettings/ @H4ad
/frontend/src/components/RolesSelect/ @H4ad
/frontend/src/pages/MembersSettings/ @H4ad
/frontend/src/container/MembersSettings/ @H4ad
/frontend/src/components/MembersTable/ @H4ad
/frontend/src/components/EditMemberDrawer/ @H4ad
/frontend/src/components/InviteMembersModal/ @H4ad
/frontend/src/hooks/member/ @H4ad
/frontend/src/pages/ServiceAccountsSettings/ @H4ad
/frontend/src/container/ServiceAccountsSettings/ @H4ad
/frontend/src/components/ServiceAccountsTable/ @H4ad
/frontend/src/components/ServiceAccountDrawer/ @H4ad
/frontend/src/components/CreateServiceAccountModal/ @H4ad
/frontend/src/hooks/serviceAccount/ @H4ad
# IdentN Owners
/pkg/identn/ @therealpandey
@@ -199,3 +218,72 @@ go.mod @therealpandey
## OpenAPI Schema - Generated
/frontend/src/api/generated/services/ @therealpandey @vikrantgupta25 @srikanthccv
/docs/api/openapi.yml @therealpandey @vikrantgupta25 @srikanthccv
## Logs
/frontend/src/pages/Logs/ @SigNoz/events-frontend
/frontend/src/pages/LogsExplorer/ @SigNoz/events-frontend
/frontend/src/pages/LogsModulePage/ @SigNoz/events-frontend
/frontend/src/pages/LogsSettings/ @SigNoz/events-frontend
/frontend/src/pages/LiveLogs/ @SigNoz/events-frontend
/frontend/src/container/LogsExplorerChart/ @SigNoz/events-frontend
/frontend/src/container/LogsExplorerContext/ @SigNoz/events-frontend
/frontend/src/container/LogsExplorerList/ @SigNoz/events-frontend
/frontend/src/container/LogsExplorerTable/ @SigNoz/events-frontend
/frontend/src/container/LogsExplorerViews/ @SigNoz/events-frontend
/frontend/src/container/LogsFilters/ @SigNoz/events-frontend
/frontend/src/container/LogsSearchFilter/ @SigNoz/events-frontend
/frontend/src/container/LogsTable/ @SigNoz/events-frontend
/frontend/src/container/LogsAggregate/ @SigNoz/events-frontend
/frontend/src/container/LogsContextList/ @SigNoz/events-frontend
/frontend/src/container/LogsIndexToFields/ @SigNoz/events-frontend
/frontend/src/container/LogsLoading/ @SigNoz/events-frontend
/frontend/src/container/LogsPanelTable/ @SigNoz/events-frontend
/frontend/src/container/LogControls/ @SigNoz/events-frontend
/frontend/src/container/LogDetailedView/ @SigNoz/events-frontend
/frontend/src/container/LogExplorerQuerySection/ @SigNoz/events-frontend
/frontend/src/container/LogLiveTail/ @SigNoz/events-frontend
/frontend/src/container/LiveLogs/ @SigNoz/events-frontend
/frontend/src/container/EmptyLogsSearch/ @SigNoz/events-frontend
/frontend/src/container/NoLogs/ @SigNoz/events-frontend
/frontend/src/components/Logs/ @SigNoz/events-frontend
/frontend/src/components/LogDetail/ @SigNoz/events-frontend
/frontend/src/components/LogsFormatOptionsMenu/ @SigNoz/events-frontend
/frontend/src/hooks/logs/ @SigNoz/events-frontend
## Logs Pipelines
/frontend/src/pages/Pipelines/ @SigNoz/events-frontend
/frontend/src/container/PipelinePage/ @SigNoz/events-frontend
## Traces / Trace Explorer
/frontend/src/pages/Trace/ @SigNoz/events-frontend
/frontend/src/pages/TracesExplorer/ @SigNoz/events-frontend
/frontend/src/pages/TracesModulePage/ @SigNoz/events-frontend
/frontend/src/container/Trace/ @SigNoz/events-frontend
/frontend/src/container/TracesExplorer/ @SigNoz/events-frontend
/frontend/src/container/TracesTableComponent/ @SigNoz/events-frontend
## Trace Funnels
/frontend/src/pages/TracesFunnels/ @SigNoz/events-frontend
/frontend/src/pages/TracesFunnelDetails/ @SigNoz/events-frontend
/frontend/src/hooks/TracesFunnels/ @SigNoz/events-frontend
## Trace Details
/frontend/src/pages/TraceDetailsV3/ @SigNoz/events-frontend
/frontend/src/pages/TraceDetailOldRedirect/ @SigNoz/events-frontend
/frontend/src/hooks/trace/ @SigNoz/events-frontend
## Exceptions
/frontend/src/pages/AllErrors/ @SigNoz/events-frontend
/frontend/src/pages/ErrorDetails/ @SigNoz/events-frontend
/frontend/src/container/AllError/ @SigNoz/events-frontend
/frontend/src/container/ErrorDetails/ @SigNoz/events-frontend
## External APIs
/frontend/src/pages/ApiMonitoring/ @SigNoz/events-frontend
/frontend/src/container/ApiMonitoring/ @SigNoz/events-frontend
## Messaging Queues
/frontend/src/pages/MessagingQueues/ @SigNoz/events-frontend
/frontend/src/components/MessagingQueues/ @SigNoz/events-frontend
/frontend/src/components/MessagingQueueHealthCheck/ @SigNoz/events-frontend
/frontend/src/hooks/messagingQueue/ @SigNoz/events-frontend

View File

@@ -140,3 +140,20 @@ jobs:
run: |
go run cmd/enterprise/*.go generate config web-settings
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in web settings schema. Run go run cmd/enterprise/*.go generate config web-settings locally and commit."; exit 1)
transaction-groups:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: self-checkout
uses: actions/checkout@v4
- name: go-install
uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: generate-transaction-groups
run: |
go run cmd/enterprise/*.go generate config transaction-groups
git diff --compact-summary --exit-code || (echo; echo "Unexpected difference in transaction groups schema. Run go run cmd/enterprise/*.go generate config transaction-groups locally and commit."; exit 1)

View File

@@ -6,12 +6,15 @@ import (
"reflect"
"strings"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/web"
"github.com/spf13/cobra"
"github.com/swaggest/jsonschema-go"
)
const webSettingsSchemaPath = "docs/config/web-settings.json"
const webSettingsSchemaPath = "frontend/src/schemas/generated/webSettings.schema.json"
const transactionGroupsSchemaPath = "frontend/src/schemas/generated/transactionGroups.schema.json"
func registerGenerateConfig(parentCmd *cobra.Command) {
configCmd := &cobra.Command{
@@ -27,6 +30,14 @@ func registerGenerateConfig(parentCmd *cobra.Command) {
},
})
configCmd.AddCommand(&cobra.Command{
Use: "transaction-groups",
Short: "Generate JSON Schema for transaction groups",
RunE: func(currCmd *cobra.Command, args []string) error {
return generateTransactionGroups()
},
})
parentCmd.AddCommand(configCmd)
}
@@ -52,6 +63,7 @@ func generateWebSettings() error {
return err
}
schema.WithTitle("WebSettings")
data, err := json.MarshalIndent(schema, "", " ")
if err != nil {
return err
@@ -59,3 +71,31 @@ func generateWebSettings() error {
return os.WriteFile(webSettingsSchemaPath, append(data, '\n'), 0o600)
}
func generateTransactionGroups() error {
falseVal := false
noAdditional := jsonschema.SchemaOrBool{TypeBoolean: &falseVal}
reflector := jsonschema.Reflector{}
reflector.DefaultOptions = append(reflector.DefaultOptions,
jsonschema.InterceptSchema(func(params jsonschema.InterceptSchemaParams) (bool, error) {
if params.Value.Kind() == reflect.Struct {
params.Schema.AdditionalProperties = &noAdditional
}
return false, nil
}),
)
schema, err := reflector.Reflect(authtypes.TransactionGroups{})
if err != nil {
return err
}
schema.WithTitle("TransactionGroups")
data, err := json.MarshalIndent(schema, "", " ")
if err != nil {
return err
}
return os.WriteFile(transactionGroupsSchemaPath, append(data, '\n'), 0o600)
}

270
deploy/MIGRATION.md Normal file
View File

@@ -0,0 +1,270 @@
# Migrating from the install script and `deploy/` to Foundry
The install script (`install.sh`) and the bundled Compose and Swarm files
under `deploy/` are deprecated in favor of [Foundry][foundry], the supported
way to install and manage SigNoz. This guide moves an existing Docker Compose
or Docker Swarm deployment to Foundry and reattaches your existing volumes, so
your data is preserved.
> [!IMPORTANT]
> This guide is only for **existing** `install.sh` / `deploy/` deployments.
> Setting up SigNoz for the first time? Skip migration and install Foundry
> directly: [SigNoz install docs][install-docs].
## How it works
Foundry splits a deployment into two commands:
- `foundryctl forge` generates the deployment manifests from a `casting.yaml`.
It never touches running containers, so it is safe to re-run while you
iterate.
- `foundryctl cast` applies those manifests: it (re)creates the containers and
reuses the volumes you point it at.
You write one `casting.yaml`, point a few patches at your existing data
volumes, then cast. The steps below are the same for Compose and Swarm; they
differ only in the casting (step 3) and how you stop the old stack (step 5).
## Prerequisites
- An existing SigNoz deployment from `install.sh` or `deploy/` (Compose or
Swarm).
- `foundryctl` (installed in step 1).
## Migrate
### 1. Install Foundry
```bash
curl -fsSL https://signoz.io/foundry.sh | bash
```
### 2. Keep your rollback path
This migration reattaches your existing volumes in place; it does not move or
delete your data. The only destructive action is passing `--volumes` / `-v`
when you stop the old stack (step 5), so avoid that flag.
> [!IMPORTANT]
> Keep a copy of your existing `docker-compose.yaml` / stack file (and any
> config it references). SigNoz no longer distributes these files, so this copy
> is your only way to roll back.
### 3. Write your `casting.yaml`
Use the casting for your deployment. Both reproduce the legacy single-node
setup (ClickHouse + ZooKeeper + SQLite) and reattach your existing volumes;
they differ only in `spec.deployment.flavor` and the volume-reuse patch
(Compose volumes have a `name` to replace; Swarm volumes are bare, so the whole
entry is replaced). If your deployment ran more than one shard or replica,
adjust the volume patches accordingly. The
[Docker Compose example][compose-example] is a useful reference.
> [!IMPORTANT]
> The `replica` and `shard` macros are placeholders. Replace them with the
> values from your existing ClickHouse config (the `macros` section of
> `config.xml` / `metrika.xml`), or the generated manifests will not match your
> existing data.
<details>
<summary><b>Docker Compose</b> casting.yaml</summary>
```yaml
# casting.yaml
apiVersion: v1alpha1
kind: Installation
metadata:
name: signoz
spec:
deployment:
flavor: compose
mode: docker
metastore:
kind: sqlite
telemetrykeeper:
kind: zookeeper
telemetrystore:
spec:
config:
data:
config-0-0.yaml: |
macros:
replica: "example01-01-1" # replace with your replica macro
shard: "01" # replace with your shard macro
patches:
- target: "deployment/compose.yaml"
operations:
- op: replace
path: /volumes/signoz-telemetrykeeper-0-data/name
value: signoz-zookeeper-1
- op: replace
path: /volumes/signoz-telemetrystore-0-0-data/name
value: signoz-clickhouse
- op: replace
path: /volumes/signoz-metastore-sqlite-0-data/name
value: signoz-sqlite
- op: add
path: /services/signoz-telemetrykeeper-zookeeper-0/user
value: root
```
</details>
<details>
<summary><b>Docker Swarm</b> casting.yaml</summary>
```yaml
# casting.yaml
apiVersion: v1alpha1
kind: Installation
metadata:
name: signoz
spec:
deployment:
flavor: swarm
mode: docker
metastore:
kind: sqlite
telemetrykeeper:
kind: zookeeper
telemetrystore:
spec:
config:
data:
config-0-0.yaml: |
macros:
replica: "example01-01-1" # replace with your replica macro
shard: "01" # replace with your shard macro
patches:
- target: "deployment/compose.yaml"
operations:
- op: replace
path: /volumes/signoz-telemetrykeeper-0-data
value:
name: signoz-zookeeper-1
- op: replace
path: /volumes/signoz-telemetrystore-0-0-data
value:
name: signoz-clickhouse
- op: replace
path: /volumes/signoz-metastore-sqlite-0-data
value:
name: signoz-sqlite
- op: add
path: /services/signoz-telemetrykeeper-zookeeper-0/user
value: root
```
</details>
> [!NOTE]
> The `user: root` patch on the ZooKeeper service lets the container read and
> write the data in your reused ZooKeeper volume, whose files the legacy setup
> created as `root`. Without it, ZooKeeper may fail to start with permission
> errors.
If you had custom configuration (SMTP, extra ingestion receivers/processors,
or custom ClickHouse settings), carry it over via [patches][patches],
[custom config files][custom-config], or [environment variables][env-vars].
### 4. Generate and review the manifests
```bash
foundryctl forge -f casting.yaml
```
Review `pours/deployment/` before deploying:
- [ ] Container images match your current deployment. Foundry generates with
`latest` by default; if your SigNoz version was older than latest, check the
[upgrade path][upgrade-path] first.
- [ ] The generated manifests match your previous configuration, especially
`compose.yaml` (the new entry point for your deployment).
- [ ] The ClickHouse config is now YAML rather than XML; confirm your custom
settings carried over (see [ClickHouse configuration files][ch-config] for
the XML-to-YAML mapping).
### 5. Stop the old deployment
Use the command for your deployment. Do **not** pass `--volumes` / `-v`; that
would delete the data you are migrating.
```bash
docker compose down # Compose
docker stack rm signoz # Swarm
```
> [!NOTE]
> This causes downtime, so plan accordingly.
Confirm nothing is still bound to the volumes before continuing:
```bash
docker ps -a
```
### 6. Deploy with Foundry
```bash
foundryctl cast -f casting.yaml
```
This recreates the containers against your existing volumes and pulls the
images. The migration container runs the schema migrations as part of `cast`.
**Prefer not to use `cast`?** The manifests in `pours/deployment/` are standard
Docker artifacts you can apply yourself. Run the command from that directory so
the relative config paths resolve:
```bash
cd pours/deployment
docker compose up -d # Compose
docker stack deploy -c compose.yaml signoz # Swarm
```
## Verify
- All SigNoz containers are running.
- The UI is reachable on `http://localhost:8080`, and OTLP on `4317` (gRPC)
and `4318` (HTTP), so already-instrumented apps and saved bookmarks keep
working.
- Your existing data is present in the UI, and new data is being ingested.
- ClickHouse and ZooKeeper logs show no errors.
## Roll back
Step 5 left your volumes untouched, so your data is intact. To return to the
previous setup:
1. Bring down the Foundry deployment (`docker compose down` or
`docker stack rm signoz`, again without `-v`).
2. Confirm the containers are gone with `docker ps -a`.
3. Re-apply your backed-up stack: `docker compose up -d` (Compose) or
`docker stack deploy -c docker-compose.yaml signoz` (Swarm). It reattaches
the same volumes and restores your prior state.
## Troubleshooting
If the migration runs into trouble, reach out on [Slack][slack] or open a
[Foundry issue][foundry-issues].
## References
- [Foundry][foundry]
- [Casting file reference][casting-ref]
- [Custom config files][custom-config]
- [Patches][patches]
- [SigNoz documentation][signoz-docs]
[foundry]: https://github.com/SigNoz/foundry
[install-docs]: https://signoz.io/docs/install/
[compose-example]: https://github.com/SigNoz/foundry/tree/main/docs/examples/docker/compose
[patches]: https://github.com/SigNoz/foundry/blob/main/docs/concepts/patches.md
[custom-config]: https://github.com/SigNoz/foundry/blob/main/docs/concepts/moldings.md#custom-config-files
[env-vars]: https://github.com/SigNoz/foundry/blob/main/docs/reference/casting-file.md#molding-spec
[casting-ref]: https://github.com/SigNoz/foundry/blob/main/docs/reference/casting-file.md
[ch-config]: https://clickhouse.com/docs/operations/configuration-files
[upgrade-path]: https://signoz.io/docs/operate/upgrade/
[slack]: https://signoz.io/slack
[foundry-issues]: https://github.com/SigNoz/foundry/issues
[signoz-docs]: https://signoz.io/docs

View File

@@ -3,77 +3,16 @@
Check that you have cloned [signoz/signoz](https://github.com/signoz/signoz)
and currently are in `signoz/deploy` folder.
## Docker
## Installation
If you don't have docker set up, please follow [this guide](https://docs.docker.com/engine/install/)
to set up docker before proceeding with the next steps.
> **Note:** The `install.sh` script and the `docker-compose` manifests have been deprecated.
### Using Install Script
SigNoz now installs and runs through [Foundry](https://signoz.io/docs/install/docker/).
Now run the following command to install:
> **Already running SigNoz via Docker Compose?** See the [Migration Guide](./MIGRATION.md) to transition your existing deployment to Foundry.
```sh
./install.sh
```
### Using Docker Compose
If you don't have docker compose set up, please follow [this guide](https://docs.docker.com/compose/install/)
to set up docker compose before proceeding with the next steps.
```sh
cd deploy/docker
docker compose up -d
```
Open http://localhost:8080 in your favourite browser.
To start collecting logs and metrics from your infrastructure, run the following command:
```sh
cd generator/infra
docker compose up -d
```
To start generating sample traces, run the following command:
```sh
cd generator/hotrod
docker compose up -d
```
In a couple of minutes, you should see the data generated from hotrod in SigNoz UI.
For more details, please refer to the [SigNoz documentation](https://signoz.io/docs/install/docker/).
## Docker Swarm
To install SigNoz using Docker Swarm, run the following command:
```sh
cd deploy/docker-swarm
docker stack deploy -c docker-compose.yaml signoz
```
Open http://localhost:8080 in your favourite browser.
To start collecting logs and metrics from your infrastructure, run the following command:
```sh
cd generator/infra
docker stack deploy -c docker-compose.yaml infra
```
To start generating sample traces, run the following command:
```sh
cd generator/hotrod
docker stack deploy -c docker-compose.yaml hotrod
```
In a couple of minutes, you should see the data generated from hotrod in SigNoz UI.
For more details, please refer to the [SigNoz documentation](https://signoz.io/docs/install/docker-swarm/).
Please follow the latest installation instructions at [signoz.io/docs/install/docker](https://signoz.io/docs/install/docker/).
Foundry has support for **different platforms and architectures**, please review the project documentation for more details.
## Uninstall/Troubleshoot?

View File

@@ -1,75 +0,0 @@
<?xml version="1.0"?>
<clickhouse>
<!-- ZooKeeper is used to store metadata about replicas, when using Replicated tables.
Optional. If you don't use replicated tables, you could omit that.
See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/
-->
<zookeeper>
<node index="1">
<host>zookeeper-1</host>
<port>2181</port>
</node>
<node index="2">
<host>zookeeper-2</host>
<port>2181</port>
</node>
<node index="3">
<host>zookeeper-3</host>
<port>2181</port>
</node>
</zookeeper>
<!-- Configuration of clusters that could be used in Distributed tables.
https://clickhouse.com/docs/en/operations/table_engines/distributed/
-->
<remote_servers>
<cluster>
<!-- Inter-server per-cluster secret for Distributed queries
default: no secret (no authentication will be performed)
If set, then Distributed queries will be validated on shards, so at least:
- such cluster should exist on the shard,
- such cluster should have the same secret.
And also (and which is more important), the initial_user will
be used as current user for the query.
Right now the protocol is pretty simple and it only takes into account:
- cluster name
- query
Also it will be nice if the following will be implemented:
- source hostname (see interserver_http_host), but then it will depends from DNS,
it can use IP address instead, but then the you need to get correct on the initiator node.
- target hostname / ip address (same notes as for source hostname)
- time-based security tokens
-->
<!-- <secret></secret> -->
<shard>
<!-- Optional. Whether to write data to just one of the replicas. Default: false (write data to all replicas). -->
<!-- <internal_replication>false</internal_replication> -->
<!-- Optional. Shard weight when writing data. Default: 1. -->
<!-- <weight>1</weight> -->
<replica>
<host>clickhouse</host>
<port>9000</port>
<!-- Optional. Priority of the replica for load_balancing. Default: 1 (less value has more priority). -->
<!-- <priority>1</priority> -->
</replica>
</shard>
<shard>
<replica>
<host>clickhouse-2</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>clickhouse-3</host>
<port>9000</port>
</replica>
</shard>
</cluster>
</remote_servers>
</clickhouse>

View File

@@ -1,75 +0,0 @@
<?xml version="1.0"?>
<clickhouse>
<!-- ZooKeeper is used to store metadata about replicas, when using Replicated tables.
Optional. If you don't use replicated tables, you could omit that.
See https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/replication/
-->
<zookeeper>
<node index="1">
<host>zookeeper-1</host>
<port>2181</port>
</node>
<!-- <node index="2">
<host>zookeeper-2</host>
<port>2181</port>
</node>
<node index="3">
<host>zookeeper-3</host>
<port>2181</port>
</node> -->
</zookeeper>
<!-- Configuration of clusters that could be used in Distributed tables.
https://clickhouse.com/docs/en/operations/table_engines/distributed/
-->
<remote_servers>
<cluster>
<!-- Inter-server per-cluster secret for Distributed queries
default: no secret (no authentication will be performed)
If set, then Distributed queries will be validated on shards, so at least:
- such cluster should exist on the shard,
- such cluster should have the same secret.
And also (and which is more important), the initial_user will
be used as current user for the query.
Right now the protocol is pretty simple and it only takes into account:
- cluster name
- query
Also it will be nice if the following will be implemented:
- source hostname (see interserver_http_host), but then it will depends from DNS,
it can use IP address instead, but then the you need to get correct on the initiator node.
- target hostname / ip address (same notes as for source hostname)
- time-based security tokens
-->
<!-- <secret></secret> -->
<shard>
<!-- Optional. Whether to write data to just one of the replicas. Default: false (write data to all replicas). -->
<!-- <internal_replication>false</internal_replication> -->
<!-- Optional. Shard weight when writing data. Default: 1. -->
<!-- <weight>1</weight> -->
<replica>
<host>clickhouse</host>
<port>9000</port>
<!-- Optional. Priority of the replica for load_balancing. Default: 1 (less value has more priority). -->
<!-- <priority>1</priority> -->
</replica>
</shard>
<!-- <shard>
<replica>
<host>clickhouse-2</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>clickhouse-3</host>
<port>9000</port>
</replica>
</shard> -->
</cluster>
</remote_servers>
</clickhouse>

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +0,0 @@
<functions>
<function>
<type>executable</type>
<name>histogramQuantile</name>
<return_type>Float64</return_type>
<argument>
<type>Array(Float64)</type>
<name>buckets</name>
</argument>
<argument>
<type>Array(Float64)</type>
<name>counts</name>
</argument>
<argument>
<type>Float64</type>
<name>quantile</name>
</argument>
<format>CSV</format>
<command>./histogramQuantile</command>
</function>
</functions>

View File

@@ -1,41 +0,0 @@
<?xml version="1.0"?>
<clickhouse>
<storage_configuration>
<disks>
<default>
<keep_free_space_bytes>10485760</keep_free_space_bytes>
</default>
<s3>
<type>s3</type>
<!-- For S3 cold storage,
if region is us-east-1, endpoint can be https://<bucket-name>.s3.amazonaws.com
if region is not us-east-1, endpoint should be https://<bucket-name>.s3-<region>.amazonaws.com
For GCS cold storage,
endpoint should be https://storage.googleapis.com/<bucket-name>/data/
-->
<endpoint>https://BUCKET-NAME.s3-REGION-NAME.amazonaws.com/data/</endpoint>
<access_key_id>ACCESS-KEY-ID</access_key_id>
<secret_access_key>SECRET-ACCESS-KEY</secret_access_key>
<!-- In case of S3, uncomment the below configuration in case you want to read
AWS credentials from the Environment variables if they exist. -->
<!-- <use_environment_credentials>true</use_environment_credentials> -->
<!-- In case of GCS, uncomment the below configuration, since GCS does
not support batch deletion and result in error messages in logs. -->
<!-- <support_batch_delete>false</support_batch_delete> -->
</s3>
</disks>
<policies>
<tiered>
<volumes>
<default>
<disk>default</disk>
</default>
<s3>
<disk>s3</disk>
<perform_ttl_move_on_insert>0</perform_ttl_move_on_insert>
</s3>
</volumes>
</tiered>
</policies>
</storage_configuration>
</clickhouse>

View File

@@ -1,123 +0,0 @@
<?xml version="1.0"?>
<clickhouse>
<!-- See also the files in users.d directory where the settings can be overridden. -->
<!-- Profiles of settings. -->
<profiles>
<!-- Default settings. -->
<default>
<!-- Maximum memory usage for processing single query, in bytes. -->
<max_memory_usage>10000000000</max_memory_usage>
<!-- How to choose between replicas during distributed query processing.
random - choose random replica from set of replicas with minimum number of errors
nearest_hostname - from set of replicas with minimum number of errors, choose replica
with minimum number of different symbols between replica's hostname and local hostname
(Hamming distance).
in_order - first live replica is chosen in specified order.
first_or_random - if first replica one has higher number of errors, pick a random one from replicas with minimum number of errors.
-->
<load_balancing>random</load_balancing>
</default>
<!-- Profile that allows only read queries. -->
<readonly>
<readonly>1</readonly>
</readonly>
</profiles>
<!-- Users and ACL. -->
<users>
<!-- If user name was not specified, 'default' user is used. -->
<default>
<!-- See also the files in users.d directory where the password can be overridden.
Password could be specified in plaintext or in SHA256 (in hex format).
If you want to specify password in plaintext (not recommended), place it in 'password' element.
Example: <password>qwerty</password>.
Password could be empty.
If you want to specify SHA256, place it in 'password_sha256_hex' element.
Example: <password_sha256_hex>65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5</password_sha256_hex>
Restrictions of SHA256: impossibility to connect to ClickHouse using MySQL JS client (as of July 2019).
If you want to specify double SHA1, place it in 'password_double_sha1_hex' element.
Example: <password_double_sha1_hex>e395796d6546b1b65db9d665cd43f0e858dd4303</password_double_sha1_hex>
If you want to specify a previously defined LDAP server (see 'ldap_servers' in the main config) for authentication,
place its name in 'server' element inside 'ldap' element.
Example: <ldap><server>my_ldap_server</server></ldap>
If you want to authenticate the user via Kerberos (assuming Kerberos is enabled, see 'kerberos' in the main config),
place 'kerberos' element instead of 'password' (and similar) elements.
The name part of the canonical principal name of the initiator must match the user name for authentication to succeed.
You can also place 'realm' element inside 'kerberos' element to further restrict authentication to only those requests
whose initiator's realm matches it.
Example: <kerberos />
Example: <kerberos><realm>EXAMPLE.COM</realm></kerberos>
How to generate decent password:
Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha256sum | tr -d '-'
In first line will be password and in second - corresponding SHA256.
How to generate double SHA1:
Execute: PASSWORD=$(base64 < /dev/urandom | head -c8); echo "$PASSWORD"; echo -n "$PASSWORD" | sha1sum | tr -d '-' | xxd -r -p | sha1sum | tr -d '-'
In first line will be password and in second - corresponding double SHA1.
-->
<password></password>
<!-- List of networks with open access.
To open access from everywhere, specify:
<ip>::/0</ip>
To open access only from localhost, specify:
<ip>::1</ip>
<ip>127.0.0.1</ip>
Each element of list has one of the following forms:
<ip> IP-address or network mask. Examples: 213.180.204.3 or 10.0.0.1/8 or 10.0.0.1/255.255.255.0
2a02:6b8::3 or 2a02:6b8::3/64 or 2a02:6b8::3/ffff:ffff:ffff:ffff::.
<host> Hostname. Example: server01.clickhouse.com.
To check access, DNS query is performed, and all received addresses compared to peer address.
<host_regexp> Regular expression for host names. Example, ^server\d\d-\d\d-\d\.clickhouse\.com$
To check access, DNS PTR query is performed for peer address and then regexp is applied.
Then, for result of PTR query, another DNS query is performed and all received addresses compared to peer address.
Strongly recommended that regexp is ends with $
All results of DNS requests are cached till server restart.
-->
<networks>
<ip>::/0</ip>
</networks>
<!-- Settings profile for user. -->
<profile>default</profile>
<!-- Quota for user. -->
<quota>default</quota>
<!-- User can create other users and grant rights to them. -->
<!-- <access_management>1</access_management> -->
</default>
</users>
<!-- Quotas. -->
<quotas>
<!-- Name of quota. -->
<default>
<!-- Limits for time interval. You could specify many intervals with different limits. -->
<interval>
<!-- Length of interval. -->
<duration>3600</duration>
<!-- No limits. Just calculate resource usage for time interval. -->
<queries>0</queries>
<errors>0</errors>
<result_rows>0</result_rows>
<read_rows>0</read_rows>
<execution_time>0</execution_time>
</interval>
</default>
</quotas>
</clickhouse>

View File

@@ -1,16 +0,0 @@
from locust import HttpUser, task, between
class UserTasks(HttpUser):
wait_time = between(5, 15)
@task
def rachel(self):
self.client.get("/dispatch?customer=123&nonse=0.6308392664170006")
@task
def trom(self):
self.client.get("/dispatch?customer=392&nonse=0.015296363321630757")
@task
def japanese(self):
self.client.get("/dispatch?customer=731&nonse=0.8022286220408668")
@task
def coffee(self):
self.client.get("/dispatch?customer=567&nonse=0.0022220379420636593")

View File

@@ -1 +0,0 @@
server_endpoint: ws://signoz:4320/v1/opamp

View File

@@ -1,25 +0,0 @@
# my global config
global:
scrape_interval: 5s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
# scrape_timeout is set to the global default (10s).
# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files: []
# - "first_rules.yml"
# - "second_rules.yml"
# - 'alerts.yml'
# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs: []
remote_read:
- url: tcp://clickhouse:9000/signoz_metrics

View File

@@ -1,288 +0,0 @@
version: "3"
x-common: &common
networks:
- signoz-net
deploy:
restart_policy:
condition: on-failure
logging:
options:
max-size: 50m
max-file: "3"
x-clickhouse-defaults: &clickhouse-defaults
!!merge <<: *common
image: clickhouse/clickhouse-server:25.5.6
tty: true
deploy:
labels:
signoz.io/scrape: "true"
signoz.io/port: "9363"
signoz.io/path: "/metrics"
depends_on:
- zookeeper-1
- zookeeper-2
- zookeeper-3
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- 0.0.0.0:8123/ping
interval: 30s
timeout: 5s
retries: 3
ulimits:
nproc: 65535
nofile:
soft: 262144
hard: 262144
environment:
- CLICKHOUSE_SKIP_USER_SETUP=1
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: signoz/zookeeper:3.7.1
user: root
deploy:
labels:
signoz.io/scrape: "true"
signoz.io/port: "9141"
signoz.io/path: "/metrics"
healthcheck:
test:
- CMD-SHELL
- curl -s -m 2 http://localhost:8080/commands/ruok | grep error | grep null
interval: 30s
timeout: 5s
retries: 3
x-db-depend: &db-depend
!!merge <<: *common
depends_on:
- clickhouse
- clickhouse-2
- clickhouse-3
services:
init-clickhouse:
!!merge <<: *common
image: clickhouse/clickhouse-server:25.5.6
command:
- bash
- -c
- |
version="v0.0.1"
node_os=$$(uname -s | tr '[:upper:]' '[:lower:]')
node_arch=$$(uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/)
echo "Fetching histogram-binary for $${node_os}/$${node_arch}"
cd /tmp
wget -O histogram-quantile.tar.gz "https://github.com/SigNoz/signoz/releases/download/histogram-quantile%2F$${version}/histogram-quantile_$${node_os}_$${node_arch}.tar.gz"
tar -xvzf histogram-quantile.tar.gz
mv histogram-quantile /var/lib/clickhouse/user_scripts/histogramQuantile
deploy:
restart_policy:
condition: on-failure
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
zookeeper-1:
!!merge <<: *zookeeper-defaults
# ports:
# - "2181:2181"
# - "2888:2888"
# - "3888:3888"
volumes:
- ./clickhouse-setup/data/zookeeper-1:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=1
- ZOO_SERVERS=0.0.0.0:2888:3888,zookeeper-2:2888:3888,zookeeper-3:2888:3888
- ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
zookeeper-2:
!!merge <<: *zookeeper-defaults
# ports:
# - "2182:2181"
# - "2889:2888"
# - "3889:3888"
volumes:
- ./clickhouse-setup/data/zookeeper-2:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=2
- ZOO_SERVERS=zookeeper-1:2888:3888,0.0.0.0:2888:3888,zookeeper-3:2888:3888
- ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
zookeeper-3:
!!merge <<: *zookeeper-defaults
# ports:
# - "2183:2181"
# - "2890:2888"
# - "3890:3888"
volumes:
- ./clickhouse-setup/data/zookeeper-3:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=3
- ZOO_SERVERS=zookeeper-1:2888:3888,zookeeper-2:2888:3888,0.0.0.0:2888:3888
- ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
clickhouse:
!!merge <<: *clickhouse-defaults
# TODO: needed for schema-migrator to work, remove this redundancy once we have a better solution
hostname: clickhouse
# ports:
# - "9000:9000"
# - "8123:8123"
# - "9181:9181"
configs:
- source: clickhouse-config
target: /etc/clickhouse-server/config.xml
- source: clickhouse-users
target: /etc/clickhouse-server/users.xml
- source: clickhouse-custom-function
target: /etc/clickhouse-server/custom-function.xml
- source: clickhouse-cluster
target: /etc/clickhouse-server/config.d/cluster.ha.xml
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ./clickhouse-setup/data/clickhouse/:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
clickhouse-2:
!!merge <<: *clickhouse-defaults
hostname: clickhouse-2
# ports:
# - "9001:9000"
# - "8124:8123"
# - "9182:9181"
configs:
- source: clickhouse-config
target: /etc/clickhouse-server/config.xml
- source: clickhouse-users
target: /etc/clickhouse-server/users.xml
- source: clickhouse-custom-function
target: /etc/clickhouse-server/custom-function.xml
- source: clickhouse-cluster
target: /etc/clickhouse-server/config.d/cluster.ha.xml
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ./clickhouse-setup/data/clickhouse-2/:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
clickhouse-3:
!!merge <<: *clickhouse-defaults
hostname: clickhouse-3
# ports:
# - "9002:9000"
# - "8125:8123"
# - "9183:9181"
configs:
- source: clickhouse-config
target: /etc/clickhouse-server/config.xml
- source: clickhouse-users
target: /etc/clickhouse-server/users.xml
- source: clickhouse-custom-function
target: /etc/clickhouse-server/custom-function.xml
- source: clickhouse-cluster
target: /etc/clickhouse-server/config.d/cluster.ha.xml
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ./clickhouse-setup/data/clickhouse-3/:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.129.0
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port
volumes:
- ./clickhouse-setup/data/signoz/:/var/lib/signoz/
environment:
- SIGNOZ_ALERTMANAGER_PROVIDER=signoz
- SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db
- SIGNOZ_TOKENIZER_JWT_SECRET=secret
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- localhost:8080/api/v1/health
interval: 30s
timeout: 5s
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.5
entrypoint:
- /bin/sh
command:
- -c
- |
/signoz-otel-collector migrate sync check &&
/signoz-otel-collector --config=/etc/otel-collector-config.yaml --manager-config=/etc/manager-config.yaml --copy-path=/var/tmp/collector-config.yaml
configs:
- source: otel-collector-config
target: /etc/otel-collector-config.yaml
- source: otel-manager-config
target: /etc/manager-config.yaml
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}}
- LOW_CARDINAL_EXCEPTION_GROUPING=false
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
ports:
# - "1777:1777" # pprof extension
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
deploy:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.5
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
entrypoint:
- /bin/sh
command:
- -c
- |
/signoz-otel-collector migrate bootstrap &&
/signoz-otel-collector migrate sync up &&
/signoz-otel-collector migrate async up
networks:
signoz-net:
name: signoz-net
volumes:
clickhouse:
name: signoz-clickhouse
clickhouse-2:
name: signoz-clickhouse-2
clickhouse-3:
name: signoz-clickhouse-3
sqlite:
name: signoz-sqlite
zookeeper-1:
name: signoz-zookeeper-1
zookeeper-2:
name: signoz-zookeeper-2
zookeeper-3:
name: signoz-zookeeper-3
configs:
clickhouse-config:
file: ../common/clickhouse/config.xml
clickhouse-users:
file: ../common/clickhouse/users.xml
clickhouse-custom-function:
file: ../common/clickhouse/custom-function.xml
clickhouse-cluster:
file: ../common/clickhouse/cluster.ha.xml
otel-collector-config:
file: ./otel-collector-config.yaml
otel-manager-config:
file: ../common/signoz/otel-collector-opamp-config.yaml

View File

@@ -1,206 +0,0 @@
version: "3"
x-common: &common
networks:
- signoz-net
deploy:
restart_policy:
condition: on-failure
logging:
options:
max-size: 50m
max-file: "3"
x-clickhouse-defaults: &clickhouse-defaults
!!merge <<: *common
image: clickhouse/clickhouse-server:25.5.6
tty: true
deploy:
labels:
signoz.io/scrape: "true"
signoz.io/port: "9363"
signoz.io/path: "/metrics"
depends_on:
- init-clickhouse
- zookeeper-1
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- 0.0.0.0:8123/ping
interval: 30s
timeout: 5s
retries: 3
ulimits:
nproc: 65535
nofile:
soft: 262144
hard: 262144
environment:
- CLICKHOUSE_SKIP_USER_SETUP=1
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: signoz/zookeeper:3.7.1
user: root
deploy:
labels:
signoz.io/scrape: "true"
signoz.io/port: "9141"
signoz.io/path: "/metrics"
healthcheck:
test:
- CMD-SHELL
- curl -s -m 2 http://localhost:8080/commands/ruok | grep error | grep null
interval: 30s
timeout: 5s
retries: 3
x-db-depend: &db-depend
!!merge <<: *common
depends_on:
- clickhouse
services:
init-clickhouse:
!!merge <<: *common
image: clickhouse/clickhouse-server:25.5.6
command:
- bash
- -c
- |
version="v0.0.1"
node_os=$$(uname -s | tr '[:upper:]' '[:lower:]')
node_arch=$$(uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/)
echo "Fetching histogram-binary for $${node_os}/$${node_arch}"
cd /tmp
wget -O histogram-quantile.tar.gz "https://github.com/SigNoz/signoz/releases/download/histogram-quantile%2F$${version}/histogram-quantile_$${node_os}_$${node_arch}.tar.gz"
tar -xvzf histogram-quantile.tar.gz
mv histogram-quantile /var/lib/clickhouse/user_scripts/histogramQuantile
deploy:
restart_policy:
condition: on-failure
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
zookeeper-1:
!!merge <<: *zookeeper-defaults
# ports:
# - "2181:2181"
# - "2888:2888"
# - "3888:3888"
volumes:
- zookeeper-1:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=1
- ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
clickhouse:
!!merge <<: *clickhouse-defaults
# TODO: needed for clickhouse TCP connectio
hostname: clickhouse
# ports:
# - "9000:9000"
# - "8123:8123"
# - "9181:9181"
configs:
- source: clickhouse-config
target: /etc/clickhouse-server/config.xml
- source: clickhouse-users
target: /etc/clickhouse-server/users.xml
- source: clickhouse-custom-function
target: /etc/clickhouse-server/custom-function.xml
- source: clickhouse-cluster
target: /etc/clickhouse-server/config.d/cluster.xml
volumes:
- clickhouse:/var/lib/clickhouse/
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.129.0
ports:
- "8080:8080" # signoz port
volumes:
- sqlite:/var/lib/signoz/
environment:
- SIGNOZ_ALERTMANAGER_PROVIDER=signoz
- SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db
- SIGNOZ_TOKENIZER_JWT_SECRET=secret
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- localhost:8080/api/v1/health
interval: 30s
timeout: 5s
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.5
entrypoint:
- /bin/sh
command:
- -c
- |
/signoz-otel-collector migrate sync check &&
/signoz-otel-collector --config=/etc/otel-collector-config.yaml --manager-config=/etc/manager-config.yaml --copy-path=/var/tmp/collector-config.yaml
configs:
- source: otel-collector-config
target: /etc/otel-collector-config.yaml
- source: otel-manager-config
target: /etc/manager-config.yaml
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}}
- LOW_CARDINAL_EXCEPTION_GROUPING=false
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
ports:
# - "1777:1777" # pprof extension
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
deploy:
replicas: 3
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:v0.144.5
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
entrypoint:
- /bin/sh
command:
- -c
- |
/signoz-otel-collector migrate bootstrap &&
/signoz-otel-collector migrate sync up &&
/signoz-otel-collector migrate async up
networks:
signoz-net:
name: signoz-net
volumes:
clickhouse:
name: signoz-clickhouse
sqlite:
name: signoz-sqlite
zookeeper-1:
name: signoz-zookeeper-1
configs:
clickhouse-config:
file: ../common/clickhouse/config.xml
clickhouse-users:
file: ../common/clickhouse/users.xml
clickhouse-custom-function:
file: ../common/clickhouse/custom-function.xml
clickhouse-cluster:
file: ../common/clickhouse/cluster.xml
otel-collector-config:
file: ./otel-collector-config.yaml
otel-manager-config:
file: ../common/signoz/otel-collector-opamp-config.yaml

View File

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

View File

@@ -1 +0,0 @@
COMPOSE_PROJECT_NAME=signoz

View File

@@ -1,3 +0,0 @@
This data directory is deprecated and will be removed in the future.
Please use the migration script under `scripts/volume-migration` to migrate data from bind mounts to Docker volumes.
The script also renames the project name to `signoz` and the network name to `signoz-net` (if not already in place).

View File

@@ -1,2 +0,0 @@
This directory is deprecated and will be removed in the future.
Please use the new directory for Clickhouse setup scripts: `scripts/clickhouse` instead.

View File

@@ -1,265 +0,0 @@
version: "3"
x-common: &common
networks:
- signoz-net
restart: unless-stopped
logging:
options:
max-size: 50m
max-file: "3"
x-clickhouse-defaults: &clickhouse-defaults
!!merge <<: *common
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
image: clickhouse/clickhouse-server:25.5.6
tty: true
labels:
signoz.io/scrape: "true"
signoz.io/port: "9363"
signoz.io/path: "/metrics"
depends_on:
init-clickhouse:
condition: service_completed_successfully
zookeeper-1:
condition: service_healthy
zookeeper-2:
condition: service_healthy
zookeeper-3:
condition: service_healthy
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- 0.0.0.0:8123/ping
interval: 30s
timeout: 5s
retries: 3
ulimits:
nproc: 65535
nofile:
soft: 262144
hard: 262144
environment:
- CLICKHOUSE_SKIP_USER_SETUP=1
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: signoz/zookeeper:3.7.1
user: root
labels:
signoz.io/scrape: "true"
signoz.io/port: "9141"
signoz.io/path: "/metrics"
healthcheck:
test:
- CMD-SHELL
- curl -s -m 2 http://localhost:8080/commands/ruok | grep error | grep null
interval: 30s
timeout: 5s
retries: 3
x-db-depend: &db-depend
!!merge <<: *common
depends_on:
clickhouse:
condition: service_healthy
clickhouse-2:
condition: service_healthy
clickhouse-3:
condition: service_healthy
services:
init-clickhouse:
!!merge <<: *common
image: clickhouse/clickhouse-server:25.5.6
container_name: signoz-init-clickhouse
command:
- bash
- -c
- |
version="v0.0.1"
node_os=$$(uname -s | tr '[:upper:]' '[:lower:]')
node_arch=$$(uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/)
echo "Fetching histogram-binary for $${node_os}/$${node_arch}"
cd /tmp
wget -O histogram-quantile.tar.gz "https://github.com/SigNoz/signoz/releases/download/histogram-quantile%2F$${version}/histogram-quantile_$${node_os}_$${node_arch}.tar.gz"
tar -xvzf histogram-quantile.tar.gz
mv histogram-quantile /var/lib/clickhouse/user_scripts/histogramQuantile
restart: on-failure
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
zookeeper-1:
!!merge <<: *zookeeper-defaults
container_name: signoz-zookeeper-1
# ports:
# - "2181:2181"
# - "2888:2888"
# - "3888:3888"
volumes:
- zookeeper-1:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=1
- ZOO_SERVERS=0.0.0.0:2888:3888,zookeeper-2:2888:3888,zookeeper-3:2888:3888
- ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
zookeeper-2:
!!merge <<: *zookeeper-defaults
container_name: signoz-zookeeper-2
# ports:
# - "2182:2181"
# - "2889:2888"
# - "3889:3888"
volumes:
- zookeeper-2:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=2
- ZOO_SERVERS=zookeeper-1:2888:3888,0.0.0.0:2888:3888,zookeeper-3:2888:3888
- ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
zookeeper-3:
!!merge <<: *zookeeper-defaults
container_name: signoz-zookeeper-3
# ports:
# - "2183:2181"
# - "2890:2888"
# - "3890:3888"
volumes:
- zookeeper-3:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=3
- ZOO_SERVERS=zookeeper-1:2888:3888,zookeeper-2:2888:3888,0.0.0.0:2888:3888
- ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
clickhouse:
!!merge <<: *clickhouse-defaults
container_name: signoz-clickhouse
# ports:
# - "9000:9000"
# - "8123:8123"
# - "9181:9181"
volumes:
- ../common/clickhouse/config.xml:/etc/clickhouse-server/config.xml
- ../common/clickhouse/users.xml:/etc/clickhouse-server/users.xml
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ../common/clickhouse/cluster.ha.xml:/etc/clickhouse-server/config.d/cluster.xml
- clickhouse:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
clickhouse-2:
!!merge <<: *clickhouse-defaults
container_name: signoz-clickhouse-2
# ports:
# - "9001:9000"
# - "8124:8123"
# - "9182:9181"
volumes:
- ../common/clickhouse/config.xml:/etc/clickhouse-server/config.xml
- ../common/clickhouse/users.xml:/etc/clickhouse-server/users.xml
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ../common/clickhouse/cluster.ha.xml:/etc/clickhouse-server/config.d/cluster.xml
- clickhouse-2:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
clickhouse-3:
!!merge <<: *clickhouse-defaults
container_name: signoz-clickhouse-3
# ports:
# - "9002:9000"
# - "8125:8123"
# - "9183:9181"
volumes:
- ../common/clickhouse/config.xml:/etc/clickhouse-server/config.xml
- ../common/clickhouse/users.xml:/etc/clickhouse-server/users.xml
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ../common/clickhouse/cluster.ha.xml:/etc/clickhouse-server/config.d/cluster.xml
- clickhouse-3:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.129.0}
container_name: signoz
ports:
- "8080:8080" # signoz port
volumes:
- sqlite:/var/lib/signoz/
environment:
- SIGNOZ_ALERTMANAGER_PROVIDER=signoz
- SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db
- SIGNOZ_TOKENIZER_JWT_SECRET=secret
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- localhost:8080/api/v1/health
interval: 30s
timeout: 5s
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.5}
container_name: signoz-otel-collector
entrypoint:
- /bin/sh
command:
- -c
- |
/signoz-otel-collector migrate sync check &&
/signoz-otel-collector --config=/etc/otel-collector-config.yaml --manager-config=/etc/manager-config.yaml --copy-path=/var/tmp/collector-config.yaml
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- ../common/signoz/otel-collector-opamp-config.yaml:/etc/manager-config.yaml
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
- LOW_CARDINAL_EXCEPTION_GROUPING=false
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
ports:
# - "1777:1777" # pprof extension
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.5}
container_name: signoz-telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
entrypoint:
- /bin/sh
command:
- -c
- |
/signoz-otel-collector migrate bootstrap &&
/signoz-otel-collector migrate sync up &&
/signoz-otel-collector migrate async up
restart: on-failure
networks:
signoz-net:
name: signoz-net
volumes:
clickhouse:
name: signoz-clickhouse
clickhouse-2:
name: signoz-clickhouse-2
clickhouse-3:
name: signoz-clickhouse-3
sqlite:
name: signoz-sqlite
zookeeper-1:
name: signoz-zookeeper-1
zookeeper-2:
name: signoz-zookeeper-2
zookeeper-3:
name: signoz-zookeeper-3

View File

@@ -1,185 +0,0 @@
version: "3"
x-common: &common
networks:
- signoz-net
restart: unless-stopped
logging:
options:
max-size: 50m
max-file: "3"
x-clickhouse-defaults: &clickhouse-defaults
!!merge <<: *common
image: clickhouse/clickhouse-server:25.5.6
tty: true
labels:
signoz.io/scrape: "true"
signoz.io/port: "9363"
signoz.io/path: "/metrics"
depends_on:
init-clickhouse:
condition: service_completed_successfully
zookeeper-1:
condition: service_healthy
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- 0.0.0.0:8123/ping
interval: 30s
timeout: 5s
retries: 3
ulimits:
nproc: 65535
nofile:
soft: 262144
hard: 262144
environment:
- CLICKHOUSE_SKIP_USER_SETUP=1
x-zookeeper-defaults: &zookeeper-defaults
!!merge <<: *common
image: signoz/zookeeper:3.7.1
user: root
labels:
signoz.io/scrape: "true"
signoz.io/port: "9141"
signoz.io/path: "/metrics"
healthcheck:
test:
- CMD-SHELL
- curl -s -m 2 http://localhost:8080/commands/ruok | grep error | grep null
interval: 30s
timeout: 5s
retries: 3
x-db-depend: &db-depend
!!merge <<: *common
depends_on:
clickhouse:
condition: service_healthy
services:
init-clickhouse:
!!merge <<: *common
image: clickhouse/clickhouse-server:25.5.6
container_name: signoz-init-clickhouse
command:
- bash
- -c
- |
version="v0.0.1"
node_os=$$(uname -s | tr '[:upper:]' '[:lower:]')
node_arch=$$(uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/)
echo "Fetching histogram-binary for $${node_os}/$${node_arch}"
cd /tmp
wget -O histogram-quantile.tar.gz "https://github.com/SigNoz/signoz/releases/download/histogram-quantile%2F$${version}/histogram-quantile_$${node_os}_$${node_arch}.tar.gz"
tar -xvzf histogram-quantile.tar.gz
mv histogram-quantile /var/lib/clickhouse/user_scripts/histogramQuantile
restart: on-failure
volumes:
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
zookeeper-1:
!!merge <<: *zookeeper-defaults
container_name: signoz-zookeeper-1
# ports:
# - "2181:2181"
# - "2888:2888"
# - "3888:3888"
volumes:
- zookeeper-1:/bitnami/zookeeper
environment:
- ZOO_SERVER_ID=1
- ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
clickhouse:
!!merge <<: *clickhouse-defaults
container_name: signoz-clickhouse
# ports:
# - "9000:9000"
# - "8123:8123"
# - "9181:9181"
volumes:
- ../common/clickhouse/config.xml:/etc/clickhouse-server/config.xml
- ../common/clickhouse/users.xml:/etc/clickhouse-server/users.xml
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
- ../common/clickhouse/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
- clickhouse:/var/lib/clickhouse/
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.129.0}
container_name: signoz
ports:
- "8080:8080" # signoz port
volumes:
- sqlite:/var/lib/signoz/
environment:
- SIGNOZ_ALERTMANAGER_PROVIDER=signoz
- SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db
- SIGNOZ_TOKENIZER_JWT_SECRET=secret
healthcheck:
test:
- CMD
- wget
- --spider
- -q
- localhost:8080/api/v1/health
interval: 30s
timeout: 5s
retries: 3
otel-collector:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.5}
container_name: signoz-otel-collector
entrypoint:
- /bin/sh
command:
- -c
- |
/signoz-otel-collector migrate sync check &&
/signoz-otel-collector --config=/etc/otel-collector-config.yaml --manager-config=/etc/manager-config.yaml --copy-path=/var/tmp/collector-config.yaml
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- ../common/signoz/otel-collector-opamp-config.yaml:/etc/manager-config.yaml
environment:
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
- LOW_CARDINAL_EXCEPTION_GROUPING=false
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
ports:
# - "1777:1777" # pprof extension
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
signoz-telemetrystore-migrator:
!!merge <<: *db-depend
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.5}
container_name: signoz-telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION=true
- SIGNOZ_OTEL_COLLECTOR_TIMEOUT=10m
entrypoint:
- /bin/sh
command:
- -c
- |
/signoz-otel-collector migrate bootstrap &&
/signoz-otel-collector migrate sync up &&
/signoz-otel-collector migrate async up
restart: on-failure
networks:
signoz-net:
name: signoz-net
volumes:
clickhouse:
name: signoz-clickhouse
sqlite:
name: signoz-sqlite
zookeeper-1:
name: signoz-zookeeper-1

View File

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

View File

@@ -2,562 +2,24 @@
set -o errexit
# Variables
BASE_DIR="$(dirname "$(readlink -f "$0")")"
DOCKER_STANDALONE_DIR="docker"
DOCKER_SWARM_DIR="docker-swarm" # TODO: Add docker swarm support
# Regular Colors
Black='\033[0;30m' # Black
Red='\[\e[0;31m\]' # Red
Green='\033[0;32m' # Green
Yellow='\033[0;33m' # Yellow
Blue='\033[0;34m' # Blue
Purple='\033[0;35m' # Purple
Cyan='\033[0;36m' # Cyan
White='\033[0;37m' # White
NC='\033[0m' # No Color
is_command_present() {
type "$1" >/dev/null 2>&1
}
# Check whether 'wget' command exists.
has_wget() {
has_cmd wget
}
# Check whether 'curl' command exists.
has_curl() {
has_cmd curl
}
# Check whether the given command exists.
has_cmd() {
command -v "$1" > /dev/null 2>&1
}
# Check if docker compose plugin is present
has_docker_compose_plugin() {
docker compose version > /dev/null 2>&1
}
is_mac() {
[[ $OSTYPE == darwin* ]]
}
is_arm64(){
[[ `uname -m` == 'arm64' || `uname -m` == 'aarch64' ]]
}
check_os() {
if is_mac; then
package_manager="brew"
desired_os=1
os="Mac"
return
fi
if is_arm64; then
arch="arm64"
arch_official="aarch64"
else
arch="amd64"
arch_official="x86_64"
fi
platform=$(uname -s | tr '[:upper:]' '[:lower:]')
os_name="$(cat /etc/*-release | awk -F= '$1 == "NAME" { gsub(/"/, ""); print $2; exit }')"
case "$os_name" in
Ubuntu*|Pop!_OS)
desired_os=1
os="ubuntu"
package_manager="apt-get"
;;
Amazon\ Linux*)
desired_os=1
os="amazon linux"
package_manager="yum"
;;
Debian*)
desired_os=1
os="debian"
package_manager="apt-get"
;;
Linux\ Mint*)
desired_os=1
os="linux mint"
package_manager="apt-get"
;;
Red\ Hat*)
desired_os=1
os="rhel"
package_manager="yum"
;;
CentOS*)
desired_os=1
os="centos"
package_manager="yum"
;;
Rocky*)
desired_os=1
os="centos"
package_manager="yum"
;;
SLES*)
desired_os=1
os="sles"
package_manager="zypper"
;;
openSUSE*)
desired_os=1
os="opensuse"
package_manager="zypper"
;;
*)
desired_os=0
os="Not Found: $os_name"
esac
}
# This function checks if the relevant ports required by SigNoz are available or not
# The script should error out in case they aren't available
check_ports_occupied() {
local port_check_output
local ports_pattern="8080|4317"
if is_mac; then
port_check_output="$(netstat -anp tcp | awk '$6 == "LISTEN" && $4 ~ /^.*\.('"$ports_pattern"')$/')"
elif is_command_present ss; then
# The `ss` command seems to be a better/faster version of `netstat`, but is not available on all Linux
# distributions by default. Other distributions have `ss` but no `netstat`. So, we try for `ss` first, then
# fallback to `netstat`.
port_check_output="$(ss --all --numeric --tcp | awk '$1 == "LISTEN" && $4 ~ /^.*:('"$ports_pattern"')$/')"
elif is_command_present netstat; then
port_check_output="$(netstat --all --numeric --tcp | awk '$6 == "LISTEN" && $4 ~ /^.*:('"$ports_pattern"')$/')"
fi
if [[ -n $port_check_output ]]; then
send_event "port_not_available"
echo "+++++++++++ ERROR ++++++++++++++++++++++"
echo "SigNoz requires ports 8080 & 4317 to be open. Please shut down any other service(s) that may be running on these ports."
echo "You can run SigNoz on another port following this guide https://signoz.io/docs/install/troubleshooting/"
echo "++++++++++++++++++++++++++++++++++++++++"
echo ""
exit 1
fi
}
install_docker() {
echo "++++++++++++++++++++++++"
echo "Setting up docker repos"
if [[ $package_manager == apt-get ]]; then
apt_cmd="$sudo_cmd apt-get --yes --quiet"
$apt_cmd update
$apt_cmd install software-properties-common gnupg-agent
curl -fsSL "https://download.docker.com/linux/$os/gpg" | $sudo_cmd apt-key add -
$sudo_cmd add-apt-repository \
"deb [arch=$arch] https://download.docker.com/linux/$os $(lsb_release -cs) stable"
$apt_cmd update
echo "Installing docker"
$apt_cmd install docker-ce docker-ce-cli containerd.io
elif [[ $package_manager == zypper ]]; then
zypper_cmd="$sudo_cmd zypper --quiet --no-gpg-checks --non-interactive"
echo "Installing docker"
if [[ $os == sles ]]; then
os_sp="$(cat /etc/*-release | awk -F= '$1 == "VERSION_ID" { gsub(/"/, ""); print $2; exit }')"
os_arch="$(uname -i)"
SUSEConnect -p sle-module-containers/$os_sp/$os_arch -r ''
fi
$zypper_cmd install docker docker-runc containerd
$sudo_cmd systemctl enable docker.service
elif [[ $package_manager == yum && $os == 'amazon linux' ]]; then
echo
echo "Amazon Linux detected ... "
echo
# yum install docker
# service docker start
$sudo_cmd yum install -y amazon-linux-extras
$sudo_cmd amazon-linux-extras enable docker
$sudo_cmd yum install -y docker
else
yum_cmd="$sudo_cmd yum --assumeyes --quiet"
$yum_cmd install yum-utils
$sudo_cmd yum-config-manager --add-repo https://download.docker.com/linux/$os/docker-ce.repo
echo "Installing docker"
$yum_cmd install docker-ce docker-ce-cli containerd.io
fi
}
compose_version () {
local compose_version
compose_version="$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep 'tag_name' | cut -d\" -f4)"
echo "${compose_version:-v2.18.1}"
}
install_docker_compose() {
if [[ $package_manager == "apt-get" || $package_manager == "zypper" || $package_manager == "yum" ]]; then
if [[ ! -f /usr/bin/docker-compose ]];then
echo "++++++++++++++++++++++++"
echo "Installing docker-compose"
compose_url="https://github.com/docker/compose/releases/download/$(compose_version)/docker-compose-$platform-$arch_official"
echo "Downloading docker-compose from $compose_url"
$sudo_cmd curl -L "$compose_url" -o /usr/local/bin/docker-compose
$sudo_cmd chmod +x /usr/local/bin/docker-compose
$sudo_cmd ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
echo "docker-compose installed!"
echo ""
fi
else
send_event "docker_compose_not_found"
echo "+++++++++++ IMPORTANT READ ++++++++++++++++++++++"
echo "docker-compose not found! Please install docker-compose first and then continue with this installation."
echo "Refer https://docs.docker.com/compose/install/ for installing docker-compose."
echo "+++++++++++++++++++++++++++++++++++++++++++++++++"
exit 1
fi
}
start_docker() {
echo -e "🐳 Starting Docker ...\n"
if [[ $os == "Mac" ]]; then
open --background -a Docker && while ! docker system info > /dev/null 2>&1; do sleep 1; done
else
if ! $sudo_cmd systemctl is-active docker.service > /dev/null; then
echo "Starting docker service"
$sudo_cmd systemctl start docker.service
fi
if [[ -z $sudo_cmd ]]; then
if ! docker ps > /dev/null && true; then
request_sudo
fi
fi
fi
}
wait_for_containers_start() {
local timeout=$1
# The while loop is important because for-loops don't work for dynamic values
while [[ $timeout -gt 0 ]]; do
status_code="$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:8080/api/v1/health?live=1" || true)"
if [[ status_code -eq 200 ]]; then
break
else
echo -ne "Waiting for all containers to start. This check will timeout in $timeout seconds ...\r\c"
fi
((timeout--))
sleep 1
done
echo ""
}
bye() { # Prints a friendly good bye message and exits the script.
# Switch back to the original directory
popd > /dev/null 2>&1
if [[ "$?" -ne 0 ]]; then
set +o errexit
echo "🔴 The containers didn't seem to start correctly. Please run the following command to check containers that may have errored out:"
echo ""
echo -e "cd ${DOCKER_STANDALONE_DIR}"
echo -e "$sudo_cmd $docker_compose_cmd ps -a"
echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/"
echo "or reach us for support in #help channel in our Slack Community https://signoz.io/slack"
echo "++++++++++++++++++++++++++++++++++++++++"
if [[ $email == "" ]]; then
echo -e "\n📨 Please share your email to receive support with the installation"
read -rp 'Email: ' email
while [[ $email == "" ]]
do
read -rp 'Email: ' email
done
fi
send_event "installation_support"
echo ""
echo -e "\nWe will reach out to you at the email provided shortly, Exiting for now. Bye! 👋 \n"
exit 0
fi
}
request_sudo() {
if hash sudo 2>/dev/null; then
echo -e "\n\n🙇 We will need sudo access to complete the installation."
if (( $EUID != 0 )); then
sudo_cmd="sudo"
echo -e "Please enter your sudo password, if prompted."
if ! $sudo_cmd -l | grep -e "NOPASSWD: ALL" > /dev/null && ! $sudo_cmd -v; then
echo "Need sudo privileges to proceed with the installation."
exit 1;
fi
echo -e "Got it! Thanks!! 🙏\n"
echo -e "Okay! We will bring up the SigNoz cluster from here 🚀\n"
fi
fi
}
Yellow='\033[0;33m' # Yellow
Green='\033[0;32m' # Green
NC='\033[0m' # No Color
echo ""
echo -e "👋 Thank you for trying out SigNoz! "
echo -e "👋 Thank you for trying out SigNoz!"
echo ""
echo -e "SigNoz now installs and runs through ${Green}Foundry${NC}."
echo ""
echo -e "${Yellow}⚠️ This install script has been deprecated and is no longer maintained.${NC}"
echo -e "${Yellow}⚠️ Please see https://github.com/SigNoz/signoz/blob/main/deploy/README.md for new installation and migrations to Foundry.${NC}"
echo ""
echo ""
echo -e "Please follow the latest installation instructions here:"
echo -e "${Green}👉 https://signoz.io/docs/install/docker/${NC}"
echo ""
echo -e "🙏 Thank you!"
echo ""
sudo_cmd=""
docker_compose_cmd=""
# Check sudo permissions
if (( $EUID != 0 )); then
echo "🟡 Running installer with non-sudo permissions."
echo " In case of any failure or prompt, please consider running the script with sudo privileges."
echo ""
else
sudo_cmd="sudo"
fi
# Checking OS and assigning package manager
desired_os=0
os=""
email=""
echo -e "🌏 Detecting your OS ...\n"
check_os
# Obtain unique installation id
# sysinfo="$(uname -a)"
# if [[ $? -ne 0 ]]; then
# uuid="$(uuidgen)"
# uuid="${uuid:-$(cat /proc/sys/kernel/random/uuid)}"
# sysinfo="${uuid:-$(cat /proc/sys/kernel/random/uuid)}"
# fi
if ! sysinfo="$(uname -a)"; then
uuid="$(uuidgen)"
uuid="${uuid:-$(cat /proc/sys/kernel/random/uuid)}"
sysinfo="${uuid:-$(cat /proc/sys/kernel/random/uuid)}"
fi
digest_cmd=""
if hash shasum 2>/dev/null; then
digest_cmd="shasum -a 256"
elif hash sha256sum 2>/dev/null; then
digest_cmd="sha256sum"
elif hash openssl 2>/dev/null; then
digest_cmd="openssl dgst -sha256"
fi
if [[ -z $digest_cmd ]]; then
SIGNOZ_INSTALLATION_ID="$sysinfo"
else
SIGNOZ_INSTALLATION_ID=$(echo "$sysinfo" | $digest_cmd | grep -E -o '[a-zA-Z0-9]{64}')
fi
setup_type='clickhouse'
# Run bye if failure happens
trap bye EXIT
URL="https://api.segment.io/v1/track"
HEADER_1="Content-Type: application/json"
HEADER_2="Authorization: Basic OWtScko3b1BDR1BFSkxGNlFqTVBMdDVibGpGaFJRQnI="
send_event() {
error=""
case "$1" in
'install_started')
event="Installation Started"
;;
'os_not_supported')
event="Installation Error"
error="OS Not Supported"
;;
'docker_not_installed')
event="Installation Error"
error="Docker not installed"
;;
'docker_compose_not_found')
event="Installation Error"
event="Docker Compose not found"
;;
'port_not_available')
event="Installation Error"
error="port not available"
;;
'installation_error_checks')
event="Installation Error - Checks"
error="Containers not started"
others='"data": "some_checks",'
;;
'installation_support')
event="Installation Support"
others='"email": "'"$email"'",'
;;
'installation_success')
event="Installation Success"
;;
'identify_successful_installation')
event="Identify Successful Installation"
others='"email": "'"$email"'",'
;;
*)
print_error "unknown event type: $1"
exit 1
;;
esac
if [[ "$error" != "" ]]; then
error='"error": "'"$error"'", '
fi
DATA='{ "anonymousId": "'"$SIGNOZ_INSTALLATION_ID"'", "event": "'"$event"'", "properties": { "os": "'"$os"'", '"$error $others"' "setup_type": "'"$setup_type"'" } }'
if has_curl; then
curl -sfL -d "$DATA" --header "$HEADER_1" --header "$HEADER_2" "$URL" > /dev/null 2>&1
elif has_wget; then
wget -q --post-data="$DATA" --header "$HEADER_1" --header "$HEADER_2" "$URL" > /dev/null 2>&1
fi
}
send_event "install_started"
if [[ $desired_os -eq 0 ]]; then
send_event "os_not_supported"
fi
# Check is Docker daemon is installed and available. If not, the install & start Docker for Linux machines. We cannot automatically install Docker Desktop on Mac OS
if ! is_command_present docker; then
if [[ $package_manager == "apt-get" || $package_manager == "zypper" || $package_manager == "yum" ]]; then
request_sudo
install_docker
# enable docker without sudo from next reboot
sudo usermod -aG docker "${USER}"
elif is_mac; then
echo ""
echo "+++++++++++ IMPORTANT READ ++++++++++++++++++++++"
echo "Docker Desktop must be installed manually on Mac OS to proceed. Docker can only be installed automatically on Ubuntu / openSUSE / SLES / Redhat / Cent OS"
echo "https://docs.docker.com/docker-for-mac/install/"
echo "++++++++++++++++++++++++++++++++++++++++++++++++"
send_event "docker_not_installed"
exit 1
else
echo ""
echo "+++++++++++ IMPORTANT READ ++++++++++++++++++++++"
echo "Docker must be installed manually on your machine to proceed. Docker can only be installed automatically on Ubuntu / openSUSE / SLES / Redhat / Cent OS"
echo "https://docs.docker.com/get-docker/"
echo "++++++++++++++++++++++++++++++++++++++++++++++++"
send_event "docker_not_installed"
exit 1
fi
fi
if has_docker_compose_plugin; then
echo "docker compose plugin is present, using it"
docker_compose_cmd="docker compose"
# Install docker-compose
else
docker_compose_cmd="docker-compose"
if ! is_command_present docker-compose; then
request_sudo
install_docker_compose
fi
fi
start_docker
# Switch to the Docker Standalone directory
pushd "${BASE_DIR}/${DOCKER_STANDALONE_DIR}" > /dev/null 2>&1
# check for open ports, if signoz is not installed
if is_command_present docker-compose; then
if $sudo_cmd $docker_compose_cmd ps | grep "signoz" | grep -q "healthy" > /dev/null 2>&1; then
echo "SigNoz already installed, skipping the occupied ports check"
else
check_ports_occupied
fi
fi
echo ""
echo -e "\n🟡 Pulling the latest container images for SigNoz.\n"
$sudo_cmd $docker_compose_cmd pull
echo ""
echo "🟡 Starting the SigNoz containers. It may take a few minutes ..."
echo
# The $docker_compose_cmd command does some nasty stuff for the `--detach` functionality. So we add a `|| true` so that the
# script doesn't exit because this command looks like it failed to do it's thing.
$sudo_cmd $docker_compose_cmd up --detach --remove-orphans || true
wait_for_containers_start 60
echo ""
if [[ $status_code -ne 200 ]]; then
echo "+++++++++++ ERROR ++++++++++++++++++++++"
echo "🔴 The containers didn't seem to start correctly. Please run the following command to check containers that may have errored out:"
echo ""
echo "cd ${DOCKER_STANDALONE_DIR}"
echo "$sudo_cmd $docker_compose_cmd ps -a"
echo ""
echo "Try bringing down the containers and retrying the installation"
echo "cd ${DOCKER_STANDALONE_DIR}"
echo "$sudo_cmd $docker_compose_cmd down -v"
echo ""
echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/"
echo "or reach us on SigNoz for support https://signoz.io/slack"
echo "++++++++++++++++++++++++++++++++++++++++"
send_event "installation_error_checks"
exit 1
else
send_event "installation_success"
echo "++++++++++++++++++ SUCCESS ++++++++++++++++++++++"
echo ""
echo "🟢 Your installation is complete!"
echo ""
echo -e "🟢 SigNoz is running on http://localhost:8080"
echo ""
echo " By default, retention period is set to 15 days for logs and traces, and 30 days for metrics."
echo -e "To change this, navigate to the General tab on the Settings page of SigNoz UI. For more details, refer to https://signoz.io/docs/userguide/retention-period \n"
echo " To bring down SigNoz and clean volumes:"
echo ""
echo "cd ${DOCKER_STANDALONE_DIR}"
echo "$sudo_cmd $docker_compose_cmd down -v"
echo ""
echo "+++++++++++++++++++++++++++++++++++++++++++++++++"
echo ""
echo "👉 Need help in Getting Started?"
echo -e "Join us on Slack https://signoz.io/slack"
echo ""
echo -e "\n📨 Please share your email to receive support & updates about SigNoz!"
read -rp 'Email: ' email
while [[ $email == "" ]]
do
read -rp 'Email: ' email
done
send_event "identify_successful_installation"
fi
echo -e "\n🙏 Thank you!\n"
exit 0

File diff suppressed because it is too large Load Diff

View File

@@ -179,13 +179,36 @@ func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context,
return provider.Write(ctx, tuples, nil)
}
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.RoleWithTransactionGroups) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
return provider.store.Create(ctx, role)
existingRole, err := provider.GetByOrgIDAndName(ctx, orgID, role.Name)
if err != nil && !errors.Asc(err, authtypes.ErrCodeRoleNotFound) {
return err
}
if existingRole != nil {
return errors.Newf(errors.TypeAlreadyExists, authtypes.ErrCodeRoleAlreadyExists, "role with name: %s already exists", existingRole.Name)
}
tuples, err := authtypes.NewTuplesFromTransactionGroups(role.Name, orgID, role.TransactionGroups)
if err != nil {
return err
}
err = provider.Write(ctx, tuples, nil)
if err != nil {
return err
}
if err := provider.store.Create(ctx, role.Role); err != nil {
return err
}
return nil
}
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) (*authtypes.Role, error) {
@@ -213,6 +236,26 @@ func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, ro
return role, nil
}
func (provider *provider) GetWithTransactionGroups(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.RoleWithTransactionGroups, error) {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
role, err := provider.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
tuples, err := provider.readAllTuplesForRole(ctx, role.Name, orgID)
if err != nil {
return nil, err
}
transactionGroups := authtypes.MustNewTransactionGroupsFromTuples(tuples)
return authtypes.MakeRoleWithTransactionGroups(role, transactionGroups), nil
}
func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*coretypes.Object, error) {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
@@ -247,6 +290,36 @@ func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id
return objects, nil
}
func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updatedRole *authtypes.RoleWithTransactionGroups) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
existingRole, err := provider.GetWithTransactionGroups(ctx, orgID, updatedRole.ID)
if err != nil {
return err
}
additions, deletions := existingRole.TransactionGroups.Diff(updatedRole.TransactionGroups)
additionTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, additions)
if err != nil {
return err
}
deletionTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, deletions)
if err != nil {
return err
}
err = provider.Write(ctx, additionTuples, deletionTuples)
if err != nil {
return err
}
return provider.store.Update(ctx, orgID, updatedRole.Role)
}
func (provider *provider) Patch(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
@@ -286,7 +359,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
role, err := provider.store.Get(ctx, orgID, id)
role, err := provider.GetWithTransactionGroups(ctx, orgID, id)
if err != nil {
return err
}
@@ -302,7 +375,12 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
}
}
if err := provider.deleteTuples(ctx, role.Name, orgID); err != nil {
tuples, err := authtypes.NewTuplesFromTransactionGroups(role.Name, orgID, role.TransactionGroups)
if err != nil {
return err
}
if err := provider.Write(ctx, nil, tuples); err != nil {
return errors.WithAdditionalf(err, "failed to delete tuples for the role: %s", role.Name)
}
@@ -361,7 +439,7 @@ func (provider *provider) getManagedRoleTransactionTuples(orgID valuer.UUID) []*
return tuples
}
func (provider *provider) deleteTuples(ctx context.Context, roleName string, orgID valuer.UUID) error {
func (provider *provider) readAllTuplesForRole(ctx context.Context, roleName string, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
subject := authtypes.MustNewSubject(coretypes.NewResourceRole(), roleName, orgID, &coretypes.VerbAssignee)
tuples := make([]*openfgav1.TupleKey, 0)
@@ -371,26 +449,10 @@ func (provider *provider) deleteTuples(ctx context.Context, roleName string, org
Object: objectType.StringValue() + ":",
})
if err != nil {
return err
return nil, err
}
tuples = append(tuples, typeTuples...)
}
if len(tuples) == 0 {
return nil
}
for idx := 0; idx < len(tuples); idx += provider.config.OpenFGA.MaxTuplesPerWrite {
end := idx + provider.config.OpenFGA.MaxTuplesPerWrite
if end > len(tuples) {
end = len(tuples)
}
err := provider.Write(ctx, nil, tuples[idx:end])
if err != nil {
return err
}
}
return nil
return tuples, nil
}

View File

@@ -98,6 +98,15 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
Route: "",
})
aiObservability := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureEnableAIObservability, evalCtx)
featureSet = append(featureSet, &licensetypes.Feature{
Name: valuer.NewString(flagger.FeatureEnableAIObservability.String()),
Active: aiObservability,
Usage: 0,
UsageLimit: -1,
Route: "",
})
if constants.IsDotMetricsEnabled {
for idx, feature := range featureSet {
if feature.Name == licensetypes.DotMetricsEnabled {

View File

@@ -90,8 +90,12 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
// initiate agent config handler
agentConfMgr, err := agentConf.Initiate(&agentConf.ManagerOptions{
Store: signoz.SQLStore,
AgentFeatures: []agentConf.AgentFeature{logParsingPipelineController},
Store: signoz.SQLStore,
AgentFeatures: []agentConf.AgentFeature{
logParsingPipelineController,
signoz.Modules.SpanMapper,
signoz.Modules.LLMPricingRule,
},
})
if err != nil {
return nil, err

View File

@@ -29,6 +29,18 @@ if (!HTMLElement.prototype.scrollIntoView) {
HTMLElement.prototype.scrollIntoView = function (): void {};
}
// jsdom doesn't implement the Pointer Capture API, which Radix UI primitives
// (e.g. @signozhq/ui Select) call when opening. Stub them so those components
// can be exercised in tests.
if (!HTMLElement.prototype.hasPointerCapture) {
HTMLElement.prototype.hasPointerCapture = function (): boolean {
return false;
};
}
if (!HTMLElement.prototype.releasePointerCapture) {
HTMLElement.prototype.releasePointerCapture = function (): void {};
}
if (typeof window.IntersectionObserver === 'undefined') {
class IntersectionObserverMock {
observe(): void {}

View File

@@ -25,7 +25,7 @@
"test": "jest",
"test:changedsince": "jest --changedSince=main --coverage --silent",
"generate:api": "orval --config ./orval.config.ts && sh scripts/post-types-generation.sh",
"generate:config:web-settings": "json2ts ../docs/config/web-settings.json -o src/types/generated/webSettings.ts --style.useTabs --style.tabWidth=1 --style.singleQuote --bannerComment '/* AUTO GENERATED FILE - DO NOT EDIT - GENERATED FROM docs/config/web-settings.json */'"
"generate:config:web-settings": "json2ts ./src/schemas/generated/webSettings.schema.json -o src/types/generated/webSettings.ts --style.useTabs --style.tabWidth=1 --style.singleQuote --bannerComment '/* AUTO GENERATED FILE - DO NOT EDIT - GENERATED FROM frontend/src/schemas/generated/webSettings.schema.json */'"
},
"engines": {
"node": ">=22.0.0",

View File

@@ -15,6 +15,8 @@
"logs_to_metrics": "Logs To Metrics",
"roles": "Roles",
"role_details": "Role Details",
"role_edit": "Edit Role",
"role_create": "Create Role",
"members": "Members",
"service_accounts": "Service Accounts",
"mcp_server": "MCP Server"

View File

@@ -82,6 +82,8 @@
"TRACE_DETAIL_OLD": "SigNoz | Trace Detail",
"SERVICE_TOP_LEVEL_OPERATIONS": "SigNoz | Service Operations",
"ROLE_DETAILS": "SigNoz | Role Details",
"ROLE_CREATE": "SigNoz | Create Role",
"ROLE_EDIT": "SigNoz | Edit Role",
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
"PUBLIC_DASHBOARD": "SigNoz | Dashboard"

View File

@@ -122,6 +122,13 @@ export const DashboardWidget = Loadable(
import(/* webpackChunkName: "DashboardWidgetPage" */ 'pages/DashboardWidget'),
);
export const DashboardPanelEditorPage = Loadable(
() =>
import(
/* webpackChunkName: "DashboardPanelEditorPage" */ 'pages/DashboardPageV2/PanelEditorPage/PanelEditorPage'
),
);
export const EditRulesPage = Loadable(
() => import(/* webpackChunkName: "Alerts Edit Page" */ 'pages/EditRules'),
);

View File

@@ -11,6 +11,7 @@ import {
ChannelsNew,
CreateNewAlerts,
DashboardPage,
DashboardPanelEditorPage,
DashboardsListPage,
DashboardWidget,
EditRulesPage,
@@ -196,6 +197,13 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'DASHBOARD_WIDGET',
},
{
path: ROUTES.DASHBOARD_PANEL_EDITOR,
exact: true,
component: DashboardPanelEditorPage,
isPrivate: true,
key: 'DASHBOARD_PANEL_EDITOR',
},
{
path: ROUTES.EDIT_ALERTS,
exact: true,

View File

@@ -4,14 +4,22 @@
* * regenerate with 'pnpm generate:api'
* SigNoz
*/
import { useMutation } from 'react-query';
import { useMutation, useQuery } from 'react-query';
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
GetChecks200,
GetChecksParams,
InframonitoringtypesPostableClustersDTO,
InframonitoringtypesPostableDaemonSetsDTO,
InframonitoringtypesPostableDeploymentsDTO,
@@ -39,7 +47,94 @@ import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* Checks whether the metrics and attributes required to power the infra-monitoring section selected by the 'type' query parameter (hosts, processes, pods, nodes, deployments, daemonsets, statefulsets, jobs, namespaces, clusters, volumes) are being received. For each collector receiver or processor that contributes required metrics or attributes, lists what is present and what is missing, with a prebuilt user-facing message and a docs link per missing component. Default-enabled metrics are those expected as soon as the receiver is configured; optional metrics require 'enabled: true' in receiver config. 'ready' is true only when every missing list is empty.
* @summary Run Infra Monitoring Setup Checks
*/
export const getChecks = (params: GetChecksParams, signal?: AbortSignal) => {
return GeneratedAPIInstance<GetChecks200>({
url: `/api/v2/infra_monitoring/checks`,
method: 'GET',
params,
signal,
});
};
export const getGetChecksQueryKey = (params?: GetChecksParams) => {
return [
`/api/v2/infra_monitoring/checks`,
...(params ? [params] : []),
] as const;
};
export const getGetChecksQueryOptions = <
TData = Awaited<ReturnType<typeof getChecks>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params: GetChecksParams,
options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getChecks>>, TError, TData>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetChecksQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getChecks>>> = ({
signal,
}) => getChecks(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getChecks>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetChecksQueryResult = NonNullable<
Awaited<ReturnType<typeof getChecks>>
>;
export type GetChecksQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Run Infra Monitoring Setup Checks
*/
export function useGetChecks<
TData = Awaited<ReturnType<typeof getChecks>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params: GetChecksParams,
options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof getChecks>>, TError, TData>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetChecksQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Run Infra Monitoring Setup Checks
*/
export const invalidateGetChecks = async (
queryClient: QueryClient,
params: GetChecksParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetChecksQueryKey(params) },
options,
);
return queryClient;
};
/**
* Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* @summary List Clusters for Infra Monitoring
*/
export const listClusters = (
@@ -122,7 +217,7 @@ export const useListClusters = <
return useMutation(getListClustersMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on) and currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on) and currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes) return -1 as a sentinel when no data is available for that field.
* @summary List DaemonSets for Infra Monitoring
*/
export const listDaemonSets = (
@@ -205,7 +300,7 @@ export const useListDaemonSets = <
return useMutation(getListDaemonSetsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes Deployments with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the deployment, plus average CPU/memory request and limit utilization (deploymentCPURequest, deploymentCPULimit, deploymentMemoryRequest, deploymentMemoryLimit). Each row also reports the latest known desiredPods (k8s.deployment.desired) and availablePods (k8s.deployment.available) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each deployment includes metadata attributes (k8s.deployment.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.deployment.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by deployments in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / available_pods, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit, deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods, availablePods) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes Deployments with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the deployment, plus average CPU/memory request and limit utilization (deploymentCPURequest, deploymentCPULimit, deploymentMemoryRequest, deploymentMemoryLimit). Each row also reports the latest known desiredPods (k8s.deployment.desired) and availablePods (k8s.deployment.available) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each deployment includes metadata attributes (k8s.deployment.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.deployment.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by deployments in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / available_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit, deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods, availablePods) return -1 as a sentinel when no data is available for that field.
* @summary List Deployments for Infra Monitoring
*/
export const listDeployments = (
@@ -288,7 +383,7 @@ export const useListDeployments = <
return useMutation(getListDeploymentsMutationOptions(options));
};
/**
* Returns a paginated list of hosts with key infrastructure metrics: CPU usage (%), memory usage (%), I/O wait (%), disk usage (%), and 15-minute load average. Each host includes its current status (active/inactive based on metrics reported in the last 10 minutes) and metadata attributes (e.g., os.type). Supports filtering via a filter expression, filtering by host status, custom groupBy to aggregate hosts by any attribute, ordering by any of the five metrics, and pagination via offset/limit. The response type is 'list' for the default host.name grouping or 'grouped_list' for custom groupBy keys. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (cpu, memory, wait, load15, diskUsage) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of hosts with key infrastructure metrics: CPU usage (%), memory usage (%), I/O wait (%), disk usage (%), and 15-minute load average. Each host includes its current status (active/inactive based on metrics reported in the last 10 minutes) and metadata attributes (e.g., os.type). Supports filtering via a filter expression, filtering by host status, custom groupBy to aggregate hosts by any attribute, ordering by any of the five metrics, and pagination via offset/limit. The response type is 'list' for the default host.name grouping or 'grouped_list' for custom groupBy keys. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (cpu, memory, wait, load15, diskUsage) return -1 as a sentinel when no data is available for that field.
* @summary List Hosts for Infra Monitoring
*/
export const listHosts = (
@@ -371,7 +466,7 @@ export const useListHosts = <
return useMutation(getListHostsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes Jobs with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the job, plus average CPU/memory request and limit utilization (jobCPURequest, jobCPULimit, jobMemoryRequest, jobMemoryLimit). Each row also reports the latest known job-level counters from kube-state-metrics: desiredSuccessfulPods (k8s.job.desired_successful_pods, the target completion count), activePods (k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative). It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value); note podCountsByPhase.failed (current pod-phase) is distinct from failedPods (cumulative job kube-state-metric). Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.job.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by jobs in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_successful_pods / active_pods / failed_pods / successful_pods, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes Jobs with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the job, plus average CPU/memory request and limit utilization (jobCPURequest, jobCPULimit, jobMemoryRequest, jobMemoryLimit). Each row also reports the latest known job-level counters from kube-state-metrics: desiredSuccessfulPods (k8s.job.desired_successful_pods, the target completion count), activePods (k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative). It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value); note podCountsByPhase.failed (current pod-phase) is distinct from failedPods (cumulative job kube-state-metric). Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.job.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by jobs in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_successful_pods / active_pods / failed_pods / successful_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods) return -1 as a sentinel when no data is available for that field.
* @summary List Jobs for Infra Monitoring
*/
export const listJobs = (
@@ -454,7 +549,7 @@ export const useListJobs = <
return useMutation(getListJobsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes namespaces with key aggregated pod metrics: CPU usage and memory working set (summed across pods in the group), plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes namespaces with key aggregated pod metrics: CPU usage and memory working set (summed across pods in the group), plus per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.
* @summary List Namespaces for Infra Monitoring
*/
export const listNamespaces = (
@@ -537,7 +632,7 @@ export const useListNamespaces = <
return useMutation(getListNamespacesMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes nodes with key metrics: CPU usage, CPU allocatable, memory working set, memory allocatable, per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready in the window) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } for pods scheduled on the listed nodes). Each node includes metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is 'list' for the default k8s.node.name grouping (each row is one node with its current condition string: ready / not_ready / no_data) or 'grouped_list' for custom groupBy keys (each row aggregates nodes in the group; condition stays no_data). Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes nodes with key metrics: CPU usage, CPU allocatable, memory working set, memory allocatable, per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready in the window) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } for pods scheduled on the listed nodes). Each node includes metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is 'list' for the default k8s.node.name grouping (each row is one node with its current condition string: ready / not_ready / no_data) or 'grouped_list' for custom groupBy keys (each row aggregates nodes in the group; condition stays no_data). Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* @summary List Nodes for Infra Monitoring
*/
export const listNodes = (
@@ -620,7 +715,7 @@ export const useListNodes = <
return useMutation(getListNodesMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes pods with key metrics: CPU usage, CPU request/limit utilization, memory working set, memory request/limit utilization, current pod phase (pending/running/succeeded/failed/unknown/no_data), and pod age (ms since start time). Each pod includes metadata attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob, cluster). Supports filtering via a filter expression, custom groupBy to aggregate pods by any attribute, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid grouping (each row is one pod with its current phase) or 'grouped_list' for custom groupBy keys (each row aggregates pods in the group with per-phase counts under podCountsByPhase: { pending, running, succeeded, failed, unknown } derived from each pod's latest phase in the window). Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes pods with key metrics: CPU usage, CPU request/limit utilization, memory working set, memory request/limit utilization, current pod phase (pending/running/succeeded/failed/unknown/no_data), and pod age (ms since start time). Each pod includes metadata attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob, cluster). Supports filtering via a filter expression, custom groupBy to aggregate pods by any attribute, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid grouping (each row is one pod with its current phase) or 'grouped_list' for custom groupBy keys (each row aggregates pods in the group with per-phase counts under podCountsByPhase: { pending, running, succeeded, failed, unknown } derived from each pod's latest phase in the window). Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no data is available for that field.
* @summary List Pods for Infra Monitoring
*/
export const listPods = (
@@ -703,7 +798,7 @@ export const useListPods = <
return useMutation(getListPodsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes persistent volume claims (PVCs) with key volume metrics: available bytes, capacity bytes, usage (capacity - available), inodes, free inodes, and used inodes. Each row also includes metadata attributes (k8s.persistentvolumeclaim.name, k8s.pod.uid, k8s.pod.name, k8s.namespace.name, k8s.node.name, k8s.statefulset.name, k8s.cluster.name). Supports filtering via a filter expression, custom groupBy to aggregate volumes by any attribute, ordering by any of the six metrics (available, capacity, usage, inodes, inodes_free, inodes_used), and pagination via offset/limit. The response type is 'list' for the default k8s.persistentvolumeclaim.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates volumes in the group. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (volumeAvailable, volumeCapacity, volumeUsage, volumeInodes, volumeInodesFree, volumeInodesUsed) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes persistent volume claims (PVCs) with key volume metrics: available bytes, capacity bytes, usage (capacity - available), inodes, free inodes, and used inodes. Each row also includes metadata attributes (k8s.persistentvolumeclaim.name, k8s.pod.uid, k8s.pod.name, k8s.namespace.name, k8s.node.name, k8s.statefulset.name, k8s.cluster.name). Supports filtering via a filter expression, custom groupBy to aggregate volumes by any attribute, ordering by any of the six metrics (available, capacity, usage, inodes, inodes_free, inodes_used), and pagination via offset/limit. The response type is 'list' for the default k8s.persistentvolumeclaim.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates volumes in the group. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (volumeAvailable, volumeCapacity, volumeUsage, volumeInodes, volumeInodesFree, volumeInodesUsed) return -1 as a sentinel when no data is available for that field.
* @summary List Volumes for Infra Monitoring
*/
export const listVolumes = (
@@ -786,7 +881,7 @@ export const useListVolumes = <
return useMutation(getListVolumesMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes StatefulSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the statefulset, plus average CPU/memory request and limit utilization (statefulSetCPURequest, statefulSetCPULimit, statefulSetMemoryRequest, statefulSetMemoryLimit). Each row also reports the latest known desiredPods (k8s.statefulset.desired_pods) and currentPods (k8s.statefulset.current_pods) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each statefulset includes metadata attributes (k8s.statefulset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.statefulset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by statefulsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / current_pods, and pagination via offset/limit. Also reports missing required metrics and whether the requested time range falls before the data retention boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes StatefulSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the statefulset, plus average CPU/memory request and limit utilization (statefulSetCPURequest, statefulSetCPULimit, statefulSetMemoryRequest, statefulSetMemoryLimit). Each row also reports the latest known desiredPods (k8s.statefulset.desired_pods) and currentPods (k8s.statefulset.current_pods) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each statefulset includes metadata attributes (k8s.statefulset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.statefulset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by statefulsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / current_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel when no data is available for that field.
* @summary List StatefulSets for Infra Monitoring
*/
export const listStatefulSets = (

View File

@@ -19,16 +19,15 @@ import type {
import type {
GetMetricAlerts200,
GetMetricAlertsPathParameters,
GetMetricAlertsParams,
GetMetricAttributes200,
GetMetricAttributesParams,
GetMetricAttributesPathParameters,
GetMetricDashboards200,
GetMetricDashboardsPathParameters,
GetMetricDashboardsParams,
GetMetricHighlights200,
GetMetricHighlightsPathParameters,
GetMetricHighlightsParams,
GetMetricMetadata200,
GetMetricMetadataPathParameters,
GetMetricMetadataParams,
GetMetricsOnboardingStatus200,
GetMetricsStats200,
GetMetricsTreemap200,
@@ -40,7 +39,6 @@ import type {
MetricsexplorertypesTreemapRequestDTO,
MetricsexplorertypesUpdateMetricMetadataRequestDTO,
RenderErrorResponseDTO,
UpdateMetricMetadataPathParameters,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
@@ -146,27 +144,26 @@ export const invalidateListMetrics = async (
* @summary Get metric alerts
*/
export const getMetricAlerts = (
{ metricName }: GetMetricAlertsPathParameters,
params: GetMetricAlertsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricAlerts200>({
url: `/api/v2/metrics/${metricName}/alerts`,
url: `/api/v2/metrics/alerts`,
method: 'GET',
params,
signal,
});
};
export const getGetMetricAlertsQueryKey = ({
metricName,
}: GetMetricAlertsPathParameters) => {
return [`/api/v2/metrics/${metricName}/alerts`] as const;
export const getGetMetricAlertsQueryKey = (params?: GetMetricAlertsParams) => {
return [`/api/v2/metrics/alerts`, ...(params ? [params] : [])] as const;
};
export const getGetMetricAlertsQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricAlerts>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricAlertsPathParameters,
params: GetMetricAlertsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricAlerts>>,
@@ -177,19 +174,13 @@ export const getGetMetricAlertsQueryOptions = <
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricAlertsQueryKey({ metricName });
const queryKey = queryOptions?.queryKey ?? getGetMetricAlertsQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof getMetricAlerts>>> = ({
signal,
}) => getMetricAlerts({ metricName }, signal);
}) => getMetricAlerts(params, signal);
return {
queryKey,
queryFn,
enabled: !!metricName,
...queryOptions,
} as UseQueryOptions<
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricAlerts>>,
TError,
TData
@@ -209,7 +200,7 @@ export function useGetMetricAlerts<
TData = Awaited<ReturnType<typeof getMetricAlerts>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricAlertsPathParameters,
params: GetMetricAlertsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricAlerts>>,
@@ -218,7 +209,7 @@ export function useGetMetricAlerts<
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricAlertsQueryOptions({ metricName }, options);
const queryOptions = getGetMetricAlertsQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
@@ -232,11 +223,11 @@ export function useGetMetricAlerts<
*/
export const invalidateGetMetricAlerts = async (
queryClient: QueryClient,
{ metricName }: GetMetricAlertsPathParameters,
params: GetMetricAlertsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricAlertsQueryKey({ metricName }) },
{ queryKey: getGetMetricAlertsQueryKey(params) },
options,
);
@@ -248,12 +239,11 @@ export const invalidateGetMetricAlerts = async (
* @summary Get metric attributes
*/
export const getMetricAttributes = (
{ metricName }: GetMetricAttributesPathParameters,
params?: GetMetricAttributesParams,
params: GetMetricAttributesParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricAttributes200>({
url: `/api/v2/metrics/${metricName}/attributes`,
url: `/api/v2/metrics/attributes`,
method: 'GET',
params,
signal,
@@ -261,21 +251,16 @@ export const getMetricAttributes = (
};
export const getGetMetricAttributesQueryKey = (
{ metricName }: GetMetricAttributesPathParameters,
params?: GetMetricAttributesParams,
) => {
return [
`/api/v2/metrics/${metricName}/attributes`,
...(params ? [params] : []),
] as const;
return [`/api/v2/metrics/attributes`, ...(params ? [params] : [])] as const;
};
export const getGetMetricAttributesQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricAttributes>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricAttributesPathParameters,
params?: GetMetricAttributesParams,
params: GetMetricAttributesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricAttributes>>,
@@ -287,19 +272,13 @@ export const getGetMetricAttributesQueryOptions = <
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetMetricAttributesQueryKey({ metricName }, params);
queryOptions?.queryKey ?? getGetMetricAttributesQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricAttributes>>
> = ({ signal }) => getMetricAttributes({ metricName }, params, signal);
> = ({ signal }) => getMetricAttributes(params, signal);
return {
queryKey,
queryFn,
enabled: !!metricName,
...queryOptions,
} as UseQueryOptions<
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricAttributes>>,
TError,
TData
@@ -319,8 +298,7 @@ export function useGetMetricAttributes<
TData = Awaited<ReturnType<typeof getMetricAttributes>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricAttributesPathParameters,
params?: GetMetricAttributesParams,
params: GetMetricAttributesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricAttributes>>,
@@ -329,11 +307,7 @@ export function useGetMetricAttributes<
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricAttributesQueryOptions(
{ metricName },
params,
options,
);
const queryOptions = getGetMetricAttributesQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
@@ -347,12 +321,11 @@ export function useGetMetricAttributes<
*/
export const invalidateGetMetricAttributes = async (
queryClient: QueryClient,
{ metricName }: GetMetricAttributesPathParameters,
params?: GetMetricAttributesParams,
params: GetMetricAttributesParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricAttributesQueryKey({ metricName }, params) },
{ queryKey: getGetMetricAttributesQueryKey(params) },
options,
);
@@ -364,27 +337,28 @@ export const invalidateGetMetricAttributes = async (
* @summary Get metric dashboards
*/
export const getMetricDashboards = (
{ metricName }: GetMetricDashboardsPathParameters,
params: GetMetricDashboardsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricDashboards200>({
url: `/api/v2/metrics/${metricName}/dashboards`,
url: `/api/v2/metrics/dashboards`,
method: 'GET',
params,
signal,
});
};
export const getGetMetricDashboardsQueryKey = ({
metricName,
}: GetMetricDashboardsPathParameters) => {
return [`/api/v2/metrics/${metricName}/dashboards`] as const;
export const getGetMetricDashboardsQueryKey = (
params?: GetMetricDashboardsParams,
) => {
return [`/api/v2/metrics/dashboards`, ...(params ? [params] : [])] as const;
};
export const getGetMetricDashboardsQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricDashboardsPathParameters,
params: GetMetricDashboardsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
@@ -396,18 +370,13 @@ export const getGetMetricDashboardsQueryOptions = <
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricDashboardsQueryKey({ metricName });
queryOptions?.queryKey ?? getGetMetricDashboardsQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricDashboards>>
> = ({ signal }) => getMetricDashboards({ metricName }, signal);
> = ({ signal }) => getMetricDashboards(params, signal);
return {
queryKey,
queryFn,
enabled: !!metricName,
...queryOptions,
} as UseQueryOptions<
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
TError,
TData
@@ -427,7 +396,7 @@ export function useGetMetricDashboards<
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricDashboardsPathParameters,
params: GetMetricDashboardsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
@@ -436,10 +405,7 @@ export function useGetMetricDashboards<
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricDashboardsQueryOptions(
{ metricName },
options,
);
const queryOptions = getGetMetricDashboardsQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
@@ -453,11 +419,11 @@ export function useGetMetricDashboards<
*/
export const invalidateGetMetricDashboards = async (
queryClient: QueryClient,
{ metricName }: GetMetricDashboardsPathParameters,
params: GetMetricDashboardsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricDashboardsQueryKey({ metricName }) },
{ queryKey: getGetMetricDashboardsQueryKey(params) },
options,
);
@@ -469,27 +435,28 @@ export const invalidateGetMetricDashboards = async (
* @summary Get metric highlights
*/
export const getMetricHighlights = (
{ metricName }: GetMetricHighlightsPathParameters,
params: GetMetricHighlightsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricHighlights200>({
url: `/api/v2/metrics/${metricName}/highlights`,
url: `/api/v2/metrics/highlights`,
method: 'GET',
params,
signal,
});
};
export const getGetMetricHighlightsQueryKey = ({
metricName,
}: GetMetricHighlightsPathParameters) => {
return [`/api/v2/metrics/${metricName}/highlights`] as const;
export const getGetMetricHighlightsQueryKey = (
params?: GetMetricHighlightsParams,
) => {
return [`/api/v2/metrics/highlights`, ...(params ? [params] : [])] as const;
};
export const getGetMetricHighlightsQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricHighlights>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricHighlightsPathParameters,
params: GetMetricHighlightsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricHighlights>>,
@@ -501,18 +468,13 @@ export const getGetMetricHighlightsQueryOptions = <
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricHighlightsQueryKey({ metricName });
queryOptions?.queryKey ?? getGetMetricHighlightsQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricHighlights>>
> = ({ signal }) => getMetricHighlights({ metricName }, signal);
> = ({ signal }) => getMetricHighlights(params, signal);
return {
queryKey,
queryFn,
enabled: !!metricName,
...queryOptions,
} as UseQueryOptions<
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricHighlights>>,
TError,
TData
@@ -532,7 +494,7 @@ export function useGetMetricHighlights<
TData = Awaited<ReturnType<typeof getMetricHighlights>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricHighlightsPathParameters,
params: GetMetricHighlightsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricHighlights>>,
@@ -541,10 +503,7 @@ export function useGetMetricHighlights<
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricHighlightsQueryOptions(
{ metricName },
options,
);
const queryOptions = getGetMetricHighlightsQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
@@ -558,219 +517,17 @@ export function useGetMetricHighlights<
*/
export const invalidateGetMetricHighlights = async (
queryClient: QueryClient,
{ metricName }: GetMetricHighlightsPathParameters,
params: GetMetricHighlightsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricHighlightsQueryKey({ metricName }) },
{ queryKey: getGetMetricHighlightsQueryKey(params) },
options,
);
return queryClient;
};
/**
* This endpoint returns metadata information like metric description, unit, type, temporality, monotonicity for a specified metric
* @summary Get metric metadata
*/
export const getMetricMetadata = (
{ metricName }: GetMetricMetadataPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricMetadata200>({
url: `/api/v2/metrics/${metricName}/metadata`,
method: 'GET',
signal,
});
};
export const getGetMetricMetadataQueryKey = ({
metricName,
}: GetMetricMetadataPathParameters) => {
return [`/api/v2/metrics/${metricName}/metadata`] as const;
};
export const getGetMetricMetadataQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricMetadataPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricMetadataQueryKey({ metricName });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricMetadata>>
> = ({ signal }) => getMetricMetadata({ metricName }, signal);
return {
queryKey,
queryFn,
enabled: !!metricName,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetMetricMetadataQueryResult = NonNullable<
Awaited<ReturnType<typeof getMetricMetadata>>
>;
export type GetMetricMetadataQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get metric metadata
*/
export function useGetMetricMetadata<
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ metricName }: GetMetricMetadataPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricMetadataQueryOptions({ metricName }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get metric metadata
*/
export const invalidateGetMetricMetadata = async (
queryClient: QueryClient,
{ metricName }: GetMetricMetadataPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricMetadataQueryKey({ metricName }) },
options,
);
return queryClient;
};
/**
* This endpoint helps to update metadata information like metric description, unit, type, temporality, monotonicity for a specified metric
* @summary Update metric metadata
*/
export const updateMetricMetadata = (
{ metricName }: UpdateMetricMetadataPathParameters,
metricsexplorertypesUpdateMetricMetadataRequestDTO?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/metrics/${metricName}/metadata`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: metricsexplorertypesUpdateMetricMetadataRequestDTO,
signal,
});
};
export const getUpdateMetricMetadataMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateMetricMetadata>>,
TError,
{
pathParams: UpdateMetricMetadataPathParameters;
data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateMetricMetadata>>,
TError,
{
pathParams: UpdateMetricMetadataPathParameters;
data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>;
},
TContext
> => {
const mutationKey = ['updateMetricMetadata'];
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 updateMetricMetadata>>,
{
pathParams: UpdateMetricMetadataPathParameters;
data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateMetricMetadata(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateMetricMetadataMutationResult = NonNullable<
Awaited<ReturnType<typeof updateMetricMetadata>>
>;
export type UpdateMetricMetadataMutationBody =
| BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>
| undefined;
export type UpdateMetricMetadataMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update metric metadata
*/
export const useUpdateMetricMetadata = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateMetricMetadata>>,
TError,
{
pathParams: UpdateMetricMetadataPathParameters;
data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateMetricMetadata>>,
TError,
{
pathParams: UpdateMetricMetadataPathParameters;
data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>;
},
TContext
> => {
return useMutation(getUpdateMetricMetadataMutationOptions(options));
};
/**
* Returns raw time series data points for a metric within a time range (max 30 minutes). Each series includes labels and timestamp/value pairs.
* @summary Inspect raw metric data points
@@ -854,6 +611,188 @@ export const useInspectMetrics = <
> => {
return useMutation(getInspectMetricsMutationOptions(options));
};
/**
* This endpoint returns metadata information like metric description, unit, type, temporality, monotonicity for a specified metric
* @summary Get metric metadata
*/
export const getMetricMetadata = (
params: GetMetricMetadataParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricMetadata200>({
url: `/api/v2/metrics/metadata`,
method: 'GET',
params,
signal,
});
};
export const getGetMetricMetadataQueryKey = (
params?: GetMetricMetadataParams,
) => {
return [`/api/v2/metrics/metadata`, ...(params ? [params] : [])] as const;
};
export const getGetMetricMetadataQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params: GetMetricMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricMetadataQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricMetadata>>
> = ({ signal }) => getMetricMetadata(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetMetricMetadataQueryResult = NonNullable<
Awaited<ReturnType<typeof getMetricMetadata>>
>;
export type GetMetricMetadataQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get metric metadata
*/
export function useGetMetricMetadata<
TData = Awaited<ReturnType<typeof getMetricMetadata>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params: GetMetricMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricMetadata>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricMetadataQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get metric metadata
*/
export const invalidateGetMetricMetadata = async (
queryClient: QueryClient,
params: GetMetricMetadataParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricMetadataQueryKey(params) },
options,
);
return queryClient;
};
/**
* This endpoint helps to update metadata information like metric description, unit, type, temporality, monotonicity for a specified metric
* @summary Update metric metadata
*/
export const updateMetricMetadata = (
metricsexplorertypesUpdateMetricMetadataRequestDTO?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/metrics/metadata`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: metricsexplorertypesUpdateMetricMetadataRequestDTO,
signal,
});
};
export const getUpdateMetricMetadataMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateMetricMetadata>>,
TError,
{ data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateMetricMetadata>>,
TError,
{ data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO> },
TContext
> => {
const mutationKey = ['updateMetricMetadata'];
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 updateMetricMetadata>>,
{ data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO> }
> = (props) => {
const { data } = props ?? {};
return updateMetricMetadata(data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateMetricMetadataMutationResult = NonNullable<
Awaited<ReturnType<typeof updateMetricMetadata>>
>;
export type UpdateMetricMetadataMutationBody =
| BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO>
| undefined;
export type UpdateMetricMetadataMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update metric metadata
*/
export const useUpdateMetricMetadata = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateMetricMetadata>>,
TError,
{ data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateMetricMetadata>>,
TError,
{ data?: BodyType<MetricsexplorertypesUpdateMetricMetadataRequestDTO> },
TContext
> => {
return useMutation(getUpdateMetricMetadataMutationOptions(options));
};
/**
* Lightweight endpoint that checks if any non-SigNoz metrics have been ingested, used for onboarding status detection
* @summary Check if non-SigNoz metrics have been received

View File

@@ -20,6 +20,7 @@ import type {
import type {
AuthtypesPatchableRoleDTO,
AuthtypesPostableRoleDTO,
AuthtypesUpdatableRoleDTO,
CoretypesPatchableObjectsDTO,
CreateRole201,
DeleteRolePathParameters,
@@ -31,6 +32,7 @@ import type {
PatchObjectsPathParameters,
PatchRolePathParameters,
RenderErrorResponseDTO,
UpdateRolePathParameters,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
@@ -365,6 +367,7 @@ export const invalidateGetRole = async (
/**
* This endpoint patches a role
* @deprecated
* @summary Patch role
*/
export const patchRole = (
@@ -436,6 +439,7 @@ export type PatchRoleMutationBody =
export type PatchRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Patch role
*/
export const usePatchRole = <
@@ -462,6 +466,105 @@ export const usePatchRole = <
> => {
return useMutation(getPatchRoleMutationOptions(options));
};
/**
* This endpoint updates a role
* @summary Update role
*/
export const updateRole = (
{ id }: UpdateRolePathParameters,
authtypesUpdatableRoleDTO?: BodyType<AuthtypesUpdatableRoleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/roles/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: authtypesUpdatableRoleDTO,
signal,
});
};
export const getUpdateRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateRole>>,
TError,
{
pathParams: UpdateRolePathParameters;
data?: BodyType<AuthtypesUpdatableRoleDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateRole>>,
TError,
{
pathParams: UpdateRolePathParameters;
data?: BodyType<AuthtypesUpdatableRoleDTO>;
},
TContext
> => {
const mutationKey = ['updateRole'];
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 updateRole>>,
{
pathParams: UpdateRolePathParameters;
data?: BodyType<AuthtypesUpdatableRoleDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateRole(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof updateRole>>
>;
export type UpdateRoleMutationBody =
| BodyType<AuthtypesUpdatableRoleDTO>
| undefined;
export type UpdateRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update role
*/
export const useUpdateRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateRole>>,
TError,
{
pathParams: UpdateRolePathParameters;
data?: BodyType<AuthtypesUpdatableRoleDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateRole>>,
TError,
{
pathParams: UpdateRolePathParameters;
data?: BodyType<AuthtypesUpdatableRoleDTO>;
},
TContext
> => {
return useMutation(getUpdateRoleMutationOptions(options));
};
/**
* Gets all objects connected to the specified role via a given relation type
* @summary Get objects for a role by relation
@@ -565,6 +668,7 @@ export const invalidateGetObjects = async (
/**
* Patches the objects connected to the specified role via a given relation type
* @deprecated
* @summary Patch objects for a role by relation
*/
export const patchObjects = (
@@ -636,6 +740,7 @@ export type PatchObjectsMutationBody =
export type PatchObjectsMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Patch objects for a role by relation
*/
export const usePatchObjects = <

View File

@@ -2094,6 +2094,45 @@ export interface AuthtypesGettableTokenDTO {
tokenType?: string;
}
export enum CoretypesKindDTO {
anonymous = 'anonymous',
organization = 'organization',
role = 'role',
serviceaccount = 'serviceaccount',
user = 'user',
'notification-channel' = 'notification-channel',
'route-policy' = 'route-policy',
'apdex-setting' = 'apdex-setting',
'auth-domain' = 'auth-domain',
session = 'session',
'cloud-integration' = 'cloud-integration',
'cloud-integration-service' = 'cloud-integration-service',
integration = 'integration',
dashboard = 'dashboard',
'public-dashboard' = 'public-dashboard',
'ingestion-key' = 'ingestion-key',
'ingestion-limit' = 'ingestion-limit',
pipeline = 'pipeline',
'user-preference' = 'user-preference',
'org-preference' = 'org-preference',
'quick-filter' = 'quick-filter',
'ttl-setting' = 'ttl-setting',
rule = 'rule',
'planned-maintenance' = 'planned-maintenance',
'saved-view' = 'saved-view',
'trace-funnel' = 'trace-funnel',
'factor-password' = 'factor-password',
'factor-api-key' = 'factor-api-key',
license = 'license',
subscription = 'subscription',
logs = 'logs',
traces = 'traces',
metrics = 'metrics',
'audit-logs' = 'audit-logs',
'meter-metrics' = 'meter-metrics',
'logs-field' = 'logs-field',
'traces-field' = 'traces-field',
}
export enum CoretypesTypeDTO {
user = 'user',
serviceaccount = 'serviceaccount',
@@ -2104,10 +2143,7 @@ export enum CoretypesTypeDTO {
telemetryresource = 'telemetryresource',
}
export interface CoretypesResourceRefDTO {
/**
* @type string
*/
kind: string;
kind: CoretypesKindDTO;
type: CoretypesTypeDTO;
}
@@ -2224,6 +2260,21 @@ export interface AuthtypesPostableEmailPasswordSessionDTO {
password?: string;
}
export interface CoretypesObjectGroupDTO {
resource: CoretypesResourceRefDTO;
/**
* @type array
*/
selectors: string[];
}
export interface AuthtypesTransactionGroupDTO {
objectGroup: CoretypesObjectGroupDTO;
relation: AuthtypesRelationDTO;
}
export type AuthtypesTransactionGroupsDTO = AuthtypesTransactionGroupDTO[];
export interface AuthtypesPostableRoleDTO {
/**
* @type string
@@ -2233,6 +2284,7 @@ export interface AuthtypesPostableRoleDTO {
* @type string
*/
name: string;
transactionGroups?: AuthtypesTransactionGroupsDTO;
}
export interface AuthtypesPostableRotateTokenDTO {
@@ -2242,6 +2294,32 @@ export interface AuthtypesPostableRotateTokenDTO {
refreshToken?: string;
}
export interface AuthtypesPostableUserRoleDTO {
/**
* @type string
*/
id: string;
}
export interface AuthtypesPostableUserDTO {
/**
* @type string
*/
displayName?: string;
/**
* @type string
*/
email: string;
/**
* @type string
*/
frontendBaseUrl?: string;
/**
* @type array
*/
userRoles?: AuthtypesPostableUserRoleDTO[];
}
export interface AuthtypesRoleDTO {
/**
* @type string
@@ -2275,6 +2353,40 @@ export interface AuthtypesRoleDTO {
updatedAt?: string;
}
export interface AuthtypesRoleWithTransactionGroupsDTO {
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
description: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
orgId: string;
transactionGroups: AuthtypesTransactionGroupsDTO;
/**
* @type string
*/
type: string;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
}
export interface AuthtypesSessionContextDTO {
/**
* @type boolean
@@ -2295,6 +2407,14 @@ export interface AuthtypesUpdatableAuthDomainDTO {
config?: AuthtypesAuthDomainConfigDTO;
}
export interface AuthtypesUpdatableRoleDTO {
/**
* @type string
*/
description: string;
transactionGroups: AuthtypesTransactionGroupsDTO;
}
export interface AuthtypesUserRoleDTO {
/**
* @type string
@@ -3065,14 +3185,6 @@ export interface CommonJSONRefDTO {
$ref?: string;
}
export interface CoretypesObjectGroupDTO {
resource: CoretypesResourceRefDTO;
/**
* @type array
*/
selectors: string[];
}
export interface CoretypesPatchableObjectsDTO {
/**
* @type array,null
@@ -5346,6 +5458,121 @@ export interface GlobaltypesConfigDTO {
mcp_url: string | null;
}
export enum InframonitoringtypesCheckComponentTypeDTO {
receiver = 'receiver',
processor = 'processor',
}
export interface InframonitoringtypesAssociatedComponentDTO {
/**
* @type string
*/
name: string;
type: InframonitoringtypesCheckComponentTypeDTO;
}
export interface InframonitoringtypesAttributesComponentEntryDTO {
associatedComponent: InframonitoringtypesAssociatedComponentDTO;
/**
* @type array,null
*/
attributes: string[] | null;
}
export enum InframonitoringtypesCheckTypeDTO {
hosts = 'hosts',
processes = 'processes',
pods = 'pods',
nodes = 'nodes',
deployments = 'deployments',
daemonsets = 'daemonsets',
statefulsets = 'statefulsets',
jobs = 'jobs',
namespaces = 'namespaces',
clusters = 'clusters',
volumes = 'volumes',
}
export interface InframonitoringtypesMissingMetricsComponentEntryDTO {
associatedComponent: InframonitoringtypesAssociatedComponentDTO;
/**
* @type string
*/
documentationLink: string;
/**
* @type string
*/
message: string;
/**
* @type array,null
*/
metrics: string[] | null;
}
export interface InframonitoringtypesMissingAttributesComponentEntryDTO {
associatedComponent: InframonitoringtypesAssociatedComponentDTO;
/**
* @type array,null
*/
attributes: string[] | null;
/**
* @type string
*/
documentationLink: string;
/**
* @type string
*/
message: string;
}
export interface InframonitoringtypesMetricsComponentEntryDTO {
associatedComponent: InframonitoringtypesAssociatedComponentDTO;
/**
* @type array,null
*/
metrics: string[] | null;
}
export interface InframonitoringtypesChecksDTO {
/**
* @type array,null
*/
missingDefaultEnabledMetrics:
| InframonitoringtypesMissingMetricsComponentEntryDTO[]
| null;
/**
* @type array,null
*/
missingOptionalMetrics:
| InframonitoringtypesMissingMetricsComponentEntryDTO[]
| null;
/**
* @type array,null
*/
missingRequiredAttributes:
| InframonitoringtypesMissingAttributesComponentEntryDTO[]
| null;
/**
* @type array,null
*/
presentDefaultEnabledMetrics:
| InframonitoringtypesMetricsComponentEntryDTO[]
| null;
/**
* @type array,null
*/
presentOptionalMetrics: InframonitoringtypesMetricsComponentEntryDTO[] | null;
/**
* @type array,null
*/
presentRequiredAttributes:
| InframonitoringtypesAttributesComponentEntryDTO[]
| null;
/**
* @type boolean
*/
ready: boolean;
type: InframonitoringtypesCheckTypeDTO;
}
export type InframonitoringtypesClusterRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -5423,13 +5650,6 @@ export interface InframonitoringtypesClusterRecordDTO {
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
}
export interface InframonitoringtypesRequiredMetricsCheckDTO {
/**
* @type array,null
*/
missingMetrics: string[] | null;
}
export enum InframonitoringtypesResponseTypeDTO {
list = 'list',
grouped_list = 'grouped_list',
@@ -5465,7 +5685,6 @@ export interface InframonitoringtypesClustersDTO {
* @type array
*/
records: InframonitoringtypesClusterRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -5543,7 +5762,6 @@ export interface InframonitoringtypesDaemonSetsDTO {
* @type array
*/
records: InframonitoringtypesDaemonSetRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -5621,7 +5839,6 @@ export interface InframonitoringtypesDeploymentsDTO {
* @type array
*/
records: InframonitoringtypesDeploymentRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -5707,7 +5924,6 @@ export interface InframonitoringtypesHostsDTO {
* @type array
*/
records: InframonitoringtypesHostRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -5793,7 +6009,6 @@ export interface InframonitoringtypesJobsDTO {
* @type array
*/
records: InframonitoringtypesJobRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -5843,7 +6058,6 @@ export interface InframonitoringtypesNamespacesDTO {
* @type array
*/
records: InframonitoringtypesNamespaceRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -5910,7 +6124,6 @@ export interface InframonitoringtypesNodesDTO {
* @type array
*/
records: InframonitoringtypesNodeRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -5994,7 +6207,6 @@ export interface InframonitoringtypesPodsDTO {
* @type array
*/
records: InframonitoringtypesPodRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -6342,7 +6554,6 @@ export interface InframonitoringtypesStatefulSetsDTO {
* @type array
*/
records: InframonitoringtypesStatefulSetRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -6411,7 +6622,6 @@ export interface InframonitoringtypesVolumesDTO {
* @type array
*/
records: InframonitoringtypesVolumeRecordDTO[];
requiredMetricsCheck: InframonitoringtypesRequiredMetricsCheckDTO;
/**
* @type integer
*/
@@ -9450,6 +9660,16 @@ export type ListLLMPricingRulesParams = {
* @description undefined
*/
limit?: number;
/**
* @type string
* @description undefined
*/
q?: string;
/**
* @type boolean,null
* @description undefined
*/
isOverride?: boolean | null;
};
export type ListLLMPricingRules200 = {
@@ -9559,7 +9779,7 @@ export type GetRolePathParameters = {
id: string;
};
export type GetRole200 = {
data: AuthtypesRoleDTO;
data: AuthtypesRoleWithTransactionGroupsDTO;
/**
* @type string
*/
@@ -9569,6 +9789,9 @@ export type GetRole200 = {
export type PatchRolePathParameters = {
id: string;
};
export type UpdateRolePathParameters = {
id: string;
};
export type GetObjectsPathParameters = {
id: string;
relation: string;
@@ -9833,7 +10056,7 @@ export type ListUsersDeprecated200 = {
status: string;
};
export type DeleteUserPathParameters = {
export type DeleteUserDeprecatedPathParameters = {
id: string;
};
export type GetUserDeprecatedPathParameters = {
@@ -10121,6 +10344,21 @@ export type Healthz503 = {
status: string;
};
export type GetChecksParams = {
/**
* @description undefined
*/
type: InframonitoringtypesCheckTypeDTO;
};
export type GetChecks200 = {
data: InframonitoringtypesChecksDTO;
/**
* @type string
*/
status: string;
};
export type ListClusters200 = {
data: InframonitoringtypesClustersDTO;
/**
@@ -10245,9 +10483,14 @@ export type ListMetrics200 = {
status: string;
};
export type GetMetricAlertsPathParameters = {
export type GetMetricAlertsParams = {
/**
* @type string
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
*/
metricName: string;
};
export type GetMetricAlerts200 = {
data: MetricsexplorertypesMetricAlertsResponseDTO;
/**
@@ -10256,18 +10499,20 @@ export type GetMetricAlerts200 = {
status: string;
};
export type GetMetricAttributesPathParameters = {
metricName: string;
};
export type GetMetricAttributesParams = {
/**
* @type string
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
*/
metricName: string;
/**
* @type integer,null
* @description undefined
* @description Start of the time range as a Unix timestamp in milliseconds.
*/
start?: number | null;
/**
* @type integer,null
* @description undefined
* @description End of the time range as a Unix timestamp in milliseconds.
*/
end?: number | null;
};
@@ -10280,9 +10525,14 @@ export type GetMetricAttributes200 = {
status: string;
};
export type GetMetricDashboardsPathParameters = {
export type GetMetricDashboardsParams = {
/**
* @type string
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
*/
metricName: string;
};
export type GetMetricDashboards200 = {
data: MetricsexplorertypesMetricDashboardsResponseDTO;
/**
@@ -10291,9 +10541,14 @@ export type GetMetricDashboards200 = {
status: string;
};
export type GetMetricHighlightsPathParameters = {
export type GetMetricHighlightsParams = {
/**
* @type string
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
*/
metricName: string;
};
export type GetMetricHighlights200 = {
data: MetricsexplorertypesMetricHighlightsResponseDTO;
/**
@@ -10302,22 +10557,24 @@ export type GetMetricHighlights200 = {
status: string;
};
export type GetMetricMetadataPathParameters = {
metricName: string;
};
export type GetMetricMetadata200 = {
data: MetricsexplorertypesMetricMetadataDTO;
export type InspectMetrics200 = {
data: MetricsexplorertypesInspectMetricsResponseDTO;
/**
* @type string
*/
status: string;
};
export type UpdateMetricMetadataPathParameters = {
export type GetMetricMetadataParams = {
/**
* @type string
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
*/
metricName: string;
};
export type InspectMetrics200 = {
data: MetricsexplorertypesInspectMetricsResponseDTO;
export type GetMetricMetadata200 = {
data: MetricsexplorertypesMetricMetadataDTO;
/**
* @type string
*/
@@ -10744,6 +11001,17 @@ export type ListUsers200 = {
status: string;
};
export type CreateUser201 = {
data: TypesIdentifiableDTO;
/**
* @type string
*/
status: string;
};
export type DeleteUserPathParameters = {
id: string;
};
export type GetUserPathParameters = {
id: string;
};

View File

@@ -18,9 +18,12 @@ import type {
} from 'react-query';
import type {
AuthtypesPostableUserDTO,
CreateInvite201,
CreateResetPasswordToken201,
CreateResetPasswordTokenPathParameters,
CreateUser201,
DeleteUserDeprecatedPathParameters,
DeleteUserPathParameters,
GetMyUser200,
GetMyUserDeprecated200,
@@ -169,6 +172,7 @@ export const invalidateGetResetPasswordTokenDeprecated = async (
/**
* This endpoint creates an invite for a user
* @deprecated
* @summary Create invite
*/
export const createInvite = (
@@ -230,6 +234,7 @@ export type CreateInviteMutationBody =
export type CreateInviteMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Create invite
*/
export const useCreateInvite = <
@@ -252,6 +257,7 @@ export const useCreateInvite = <
};
/**
* This endpoint creates a bulk invite for a user
* @deprecated
* @summary Create bulk invite
*/
export const createBulkInvite = (
@@ -313,6 +319,7 @@ export type CreateBulkInviteMutationBody =
export type CreateBulkInviteMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Create bulk invite
*/
export const useCreateBulkInvite = <
@@ -418,6 +425,7 @@ export const useResetPassword = <
};
/**
* This endpoint lists all users
* @deprecated
* @summary List users
*/
export const listUsersDeprecated = (signal?: AbortSignal) => {
@@ -463,6 +471,7 @@ export type ListUsersDeprecatedQueryResult = NonNullable<
export type ListUsersDeprecatedQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary List users
*/
@@ -486,6 +495,7 @@ export function useListUsersDeprecated<
}
/**
* @deprecated
* @summary List users
*/
export const invalidateListUsersDeprecated = async (
@@ -502,10 +512,11 @@ export const invalidateListUsersDeprecated = async (
/**
* This endpoint deletes the user by id
* @deprecated
* @summary Delete user
*/
export const deleteUser = (
{ id }: DeleteUserPathParameters,
export const deleteUserDeprecated = (
{ id }: DeleteUserDeprecatedPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
@@ -515,23 +526,23 @@ export const deleteUser = (
});
};
export const getDeleteUserMutationOptions = <
export const getDeleteUserDeprecatedMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteUser>>,
Awaited<ReturnType<typeof deleteUserDeprecated>>,
TError,
{ pathParams: DeleteUserPathParameters },
{ pathParams: DeleteUserDeprecatedPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteUser>>,
Awaited<ReturnType<typeof deleteUserDeprecated>>,
TError,
{ pathParams: DeleteUserPathParameters },
{ pathParams: DeleteUserDeprecatedPathParameters },
TContext
> => {
const mutationKey = ['deleteUser'];
const mutationKey = ['deleteUserDeprecated'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
@@ -541,46 +552,49 @@ export const getDeleteUserMutationOptions = <
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteUser>>,
{ pathParams: DeleteUserPathParameters }
Awaited<ReturnType<typeof deleteUserDeprecated>>,
{ pathParams: DeleteUserDeprecatedPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteUser(pathParams);
return deleteUserDeprecated(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteUserMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteUser>>
export type DeleteUserDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteUserDeprecated>>
>;
export type DeleteUserMutationError = ErrorType<RenderErrorResponseDTO>;
export type DeleteUserDeprecatedMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Delete user
*/
export const useDeleteUser = <
export const useDeleteUserDeprecated = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteUser>>,
Awaited<ReturnType<typeof deleteUserDeprecated>>,
TError,
{ pathParams: DeleteUserPathParameters },
{ pathParams: DeleteUserDeprecatedPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteUser>>,
Awaited<ReturnType<typeof deleteUserDeprecated>>,
TError,
{ pathParams: DeleteUserPathParameters },
{ pathParams: DeleteUserDeprecatedPathParameters },
TContext
> => {
return useMutation(getDeleteUserMutationOptions(options));
return useMutation(getDeleteUserDeprecatedMutationOptions(options));
};
/**
* This endpoint returns the user by id
* @deprecated
* @summary Get user
*/
export const getUserDeprecated = (
@@ -640,6 +654,7 @@ export type GetUserDeprecatedQueryResult = NonNullable<
export type GetUserDeprecatedQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Get user
*/
@@ -666,6 +681,7 @@ export function useGetUserDeprecated<
}
/**
* @deprecated
* @summary Get user
*/
export const invalidateGetUserDeprecated = async (
@@ -683,6 +699,7 @@ export const invalidateGetUserDeprecated = async (
/**
* This endpoint updates the user by id
* @deprecated
* @summary Update user
*/
export const updateUserDeprecated = (
@@ -755,6 +772,7 @@ export type UpdateUserDeprecatedMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Update user
*/
export const useUpdateUserDeprecated = <
@@ -783,6 +801,7 @@ export const useUpdateUserDeprecated = <
};
/**
* This endpoint returns the user I belong to
* @deprecated
* @summary Get my user
*/
export const getMyUserDeprecated = (signal?: AbortSignal) => {
@@ -828,6 +847,7 @@ export type GetMyUserDeprecatedQueryResult = NonNullable<
export type GetMyUserDeprecatedQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Get my user
*/
@@ -851,6 +871,7 @@ export function useGetMyUserDeprecated<
}
/**
* @deprecated
* @summary Get my user
*/
export const invalidateGetMyUserDeprecated = async (
@@ -1209,6 +1230,168 @@ export const invalidateListUsers = async (
return queryClient;
};
/**
* This endpoint creates a user for the organization
* @summary Create user
*/
export const createUser = (
authtypesPostableUserDTO?: BodyType<AuthtypesPostableUserDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateUser201>({
url: `/api/v2/users`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: authtypesPostableUserDTO,
signal,
});
};
export const getCreateUserMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createUser>>,
TError,
{ data?: BodyType<AuthtypesPostableUserDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createUser>>,
TError,
{ data?: BodyType<AuthtypesPostableUserDTO> },
TContext
> => {
const mutationKey = ['createUser'];
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 createUser>>,
{ data?: BodyType<AuthtypesPostableUserDTO> }
> = (props) => {
const { data } = props ?? {};
return createUser(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateUserMutationResult = NonNullable<
Awaited<ReturnType<typeof createUser>>
>;
export type CreateUserMutationBody =
| BodyType<AuthtypesPostableUserDTO>
| undefined;
export type CreateUserMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create user
*/
export const useCreateUser = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createUser>>,
TError,
{ data?: BodyType<AuthtypesPostableUserDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createUser>>,
TError,
{ data?: BodyType<AuthtypesPostableUserDTO> },
TContext
> => {
return useMutation(getCreateUserMutationOptions(options));
};
/**
* This endpoint deletes the user by id
* @summary Delete user
*/
export const deleteUser = (
{ id }: DeleteUserPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/users/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteUserMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteUser>>,
TError,
{ pathParams: DeleteUserPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteUser>>,
TError,
{ pathParams: DeleteUserPathParameters },
TContext
> => {
const mutationKey = ['deleteUser'];
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 deleteUser>>,
{ pathParams: DeleteUserPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteUser(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteUserMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteUser>>
>;
export type DeleteUserMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete user
*/
export const useDeleteUser = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteUser>>,
TError,
{ pathParams: DeleteUserPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteUser>>,
TError,
{ pathParams: DeleteUserPathParameters },
TContext
> => {
return useMutation(getDeleteUserMutationOptions(options));
};
/**
* This endpoint returns the user by id
* @summary Get user by user id

View File

@@ -274,4 +274,110 @@ describe('convertV5ResponseToLegacy', () => {
},
});
});
it('clickhouse_sql scalar keeps each value column distinct (regression: all-"A" collapse)', () => {
const scalar: ScalarData = {
columns: [
{
name: 'service.name',
queryName: 'A',
aggregationIndex: 0,
columnType: 'group',
} as unknown as ScalarData['columns'][number],
{
name: 'current_availability',
queryName: 'A',
aggregationIndex: 0,
columnType: 'aggregation',
} as unknown as ScalarData['columns'][number],
{
name: 'error_budget_remaining',
queryName: 'A',
aggregationIndex: 1,
columnType: 'aggregation',
} as unknown as ScalarData['columns'][number],
{
name: 'budget_status',
queryName: 'A',
aggregationIndex: 2,
columnType: 'group',
} as unknown as ScalarData['columns'][number],
{
name: 'total_requests',
queryName: 'A',
aggregationIndex: 4,
columnType: 'aggregation',
} as unknown as ScalarData['columns'][number],
],
data: [['kuja-api_gateway-service', 99.985, 0.985, 'Healthy ✅', 2181216]],
};
const v5Data: QueryRangeResponseV5 = {
type: 'scalar',
data: { results: [scalar] },
meta: { rowsScanned: 0, bytesScanned: 0, durationMs: 0, stepIntervals: {} },
};
// A clickhouse_sql envelope contributes no aggregation metadata.
const params = makeBaseParams('scalar', [
{
type: 'clickhouse_sql',
spec: {
name: 'A',
query: 'SELECT ...',
disabled: false,
},
} as unknown as QueryRangeRequestV5['compositeQuery']['queries'][number],
]);
const input: SuccessResponse<MetricRangePayloadV5, QueryRangeRequestV5> =
makeBaseSuccess({ data: v5Data }, params);
// formatForWeb=true is the table-panel path.
const result = convertV5ResponseToLegacy(input, { A: '' }, true);
const [tableEntry] = result.payload.data.result;
// Headers keep their real names instead of collapsing to "A".
expect(tableEntry.table?.columns).toStrictEqual([
{
name: 'service.name',
queryName: 'A',
isValueColumn: false,
id: 'service.name',
},
{
name: 'current_availability',
queryName: 'A',
isValueColumn: true,
id: 'current_availability',
},
{
name: 'error_budget_remaining',
queryName: 'A',
isValueColumn: true,
id: 'error_budget_remaining',
},
{
name: 'budget_status',
queryName: 'A',
isValueColumn: false,
id: 'budget_status',
},
{
name: 'total_requests',
queryName: 'A',
isValueColumn: true,
id: 'total_requests',
},
]);
// Ids are unique, so value columns don't overwrite each other in the row.
expect(tableEntry.table?.rows?.[0]).toStrictEqual({
data: {
'service.name': 'kuja-api_gateway-service',
current_availability: 99.985,
error_budget_remaining: 0.985,
budget_status: 'Healthy ✅',
total_requests: 2181216,
},
});
});
});

View File

@@ -15,6 +15,7 @@ function getColName(
col: ScalarData['columns'][number],
legendMap: Record<string, string>,
aggregationPerQuery: Record<string, any>,
clickhouseQueryNames: Set<string>,
): string {
if (col.columnType === 'group') {
return col.name;
@@ -39,16 +40,32 @@ function getColName(
return alias || expression || col.queryName;
}
// clickhouse_sql value columns carry their real SQL alias in col.name — use
// it so each value column keeps its own header instead of collapsing onto
// the query name. Formulas/promql use placeholder names, so they fall back
// to legend || queryName.
if (clickhouseQueryNames.has(col.queryName)) {
return col.name;
}
return legend || col.queryName;
}
function getColId(
col: ScalarData['columns'][number],
aggregationPerQuery: Record<string, any>,
clickhouseQueryNames: Set<string>,
): string {
if (col.columnType === 'group') {
return col.name;
}
// clickhouse_sql value columns are keyed by their real SQL alias so multiple
// value columns stay unique instead of all collapsing onto the query name
// (which would overwrite every cell in the row with the last column's value).
if (clickhouseQueryNames.has(col.queryName)) {
return col.name;
}
const aggregation =
aggregationPerQuery?.[col.queryName]?.[col.aggregationIndex];
const expression = aggregation?.expression || '';
@@ -141,6 +158,7 @@ function convertScalarDataArrayToTable(
scalarDataArray: ScalarData[],
legendMap: Record<string, string>,
aggregationPerQuery: Record<string, any>,
clickhouseQueryNames: Set<string>,
): QueryDataV3[] {
// If no scalar data, return empty structure
@@ -166,10 +184,10 @@ function convertScalarDataArrayToTable(
// Collect columns for this specific query
const columns = scalarData?.columns?.map((col) => ({
name: getColName(col, legendMap, aggregationPerQuery),
name: getColName(col, legendMap, aggregationPerQuery, clickhouseQueryNames),
queryName: col.queryName,
isValueColumn: col.columnType === 'aggregation',
id: getColId(col, aggregationPerQuery),
id: getColId(col, aggregationPerQuery, clickhouseQueryNames),
}));
// Process rows for this specific query
@@ -177,8 +195,13 @@ function convertScalarDataArrayToTable(
const rowData: Record<string, any> = {};
scalarData?.columns?.forEach((col, colIndex) => {
const columnName = getColName(col, legendMap, aggregationPerQuery);
const columnId = getColId(col, aggregationPerQuery);
const columnName = getColName(
col,
legendMap,
aggregationPerQuery,
clickhouseQueryNames,
);
const columnId = getColId(col, aggregationPerQuery, clickhouseQueryNames);
rowData[columnId || columnName] = dataRow[colIndex];
});
@@ -202,6 +225,7 @@ function convertScalarWithFormatForWeb(
scalarDataArray: ScalarData[],
legendMap: Record<string, string>,
aggregationPerQuery: Record<string, any>,
clickhouseQueryNames: Set<string>,
): QueryDataV3[] {
if (!scalarDataArray || scalarDataArray.length === 0) {
return [];
@@ -210,13 +234,18 @@ function convertScalarWithFormatForWeb(
return scalarDataArray.map((scalarData) => {
const columns =
scalarData.columns?.map((col) => {
const colName = getColName(col, legendMap, aggregationPerQuery);
const colName = getColName(
col,
legendMap,
aggregationPerQuery,
clickhouseQueryNames,
);
return {
name: colName,
queryName: col.queryName,
isValueColumn: col.columnType === 'aggregation',
id: getColId(col, aggregationPerQuery),
id: getColId(col, aggregationPerQuery, clickhouseQueryNames),
};
}) || [];
@@ -289,6 +318,7 @@ function convertV5DataByType(
v5Data: any,
legendMap: Record<string, string>,
aggregationPerQuery: Record<string, any>,
clickhouseQueryNames: Set<string>,
): MetricRangePayloadV3['data'] {
switch (v5Data?.type) {
case 'time_series': {
@@ -307,6 +337,7 @@ function convertV5DataByType(
scalarData,
legendMap,
aggregationPerQuery,
clickhouseQueryNames,
);
return {
resultType: 'scalar',
@@ -373,6 +404,15 @@ export function convertV5ResponseToLegacy(
{} as Record<string, any>,
) || {};
// clickhouse_sql queries have no aggregation metadata; their value columns
// are named/keyed by the real SQL alias the response carries (see getColId).
const clickhouseQueryNames = new Set<string>(
(params?.compositeQuery?.queries ?? [])
.filter((query) => query.type === 'clickhouse_sql')
.map((query) => (query.spec as { name?: string })?.name)
.filter((name): name is string => !!name),
);
// If formatForWeb is true, return as-is (like existing logic)
if (formatForWeb && v5Data?.type === 'scalar') {
const scalarData = v5Data.data.results as ScalarData[];
@@ -380,6 +420,7 @@ export function convertV5ResponseToLegacy(
scalarData,
legendMap,
aggregationPerQuery,
clickhouseQueryNames,
);
return {
@@ -402,6 +443,7 @@ export function convertV5ResponseToLegacy(
v5Data,
legendMap,
aggregationPerQuery,
clickhouseQueryNames,
);
// Create legacy-compatible response structure

View File

@@ -21,6 +21,8 @@ interface ErrorInPlaceProps {
width?: string | number;
/** Custom content instead of ErrorContent */
children?: ReactNode;
/** Test ID for testing */
'data-testid'?: string;
}
/**
@@ -44,6 +46,7 @@ function ErrorInPlace({
height = '100%',
width = '100%',
children,
'data-testid': dataTestId,
}: ErrorInPlaceProps): JSX.Element {
const containerStyle: React.CSSProperties = {
display: 'flex',
@@ -59,7 +62,11 @@ function ErrorInPlace({
};
return (
<div className={`error-in-place ${className}`.trim()} style={containerStyle}>
<div
className={`error-in-place ${className}`.trim()}
style={containerStyle}
data-testid={dataTestId}
>
{children || <ErrorContent error={error} />}
</div>
);

View File

@@ -5,13 +5,9 @@ describe('PermissionDeniedFullPage', () => {
it('renders the title and subtitle with the permissionName interpolated', () => {
render(<PermissionDeniedFullPage permissionName="serviceaccount:list" />);
expect(
screen.getByText("Uh-oh! You don't have permission to view this page."),
).toBeInTheDocument();
expect(screen.getByText('Uh-oh! You are not authorized')).toBeInTheDocument();
expect(screen.getByText(/serviceaccount:list/)).toBeInTheDocument();
expect(
screen.getByText(/Please ask your SigNoz administrator to grant access/),
).toBeInTheDocument();
expect(screen.getByText(/is not authorized to perform/)).toBeInTheDocument();
});
it('renders with a different permissionName', () => {

View File

@@ -2,6 +2,7 @@ import { CircleSlash2 } from '@signozhq/icons';
import styles from './PermissionDeniedFullPage.module.scss';
import { Style } from '@signozhq/design-tokens';
import { useAppContext } from 'providers/App/App';
interface PermissionDeniedFullPageProps {
permissionName: string;
@@ -10,18 +11,18 @@ interface PermissionDeniedFullPageProps {
function PermissionDeniedFullPage({
permissionName,
}: PermissionDeniedFullPageProps): JSX.Element {
const { user } = useAppContext();
return (
<div className={styles.container}>
<div className={styles.content}>
<span className={styles.icon}>
<CircleSlash2 color={Style.CALLOUT_WARNING_TITLE} size={14} />
</span>
<p className={styles.title}>
Uh-oh! You don&apos;t have permission to view this page.
</p>
<p className={styles.title}>Uh-oh! You are not authorized</p>
<p className={styles.subtitle}>
You need <code className={styles.permission}>{permissionName}</code> to
view this page. Please ask your SigNoz administrator to grant access.
<code className={styles.permission}>user/{user.id}</code> is not authorized
to perform <code className={styles.permission}>{permissionName}</code>
</p>
</div>
</div>

View File

@@ -1,7 +1,9 @@
import { useCallback } from 'react';
import { LockKeyhole } from '@signozhq/icons';
import { useCallback, useEffect, useState } from 'react';
import { Check, Copy, LockKeyhole } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { useCopyToClipboard } from 'react-use';
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import AuthZTooltip from 'components/AuthZTooltip/AuthZTooltip';
import RolesSelect from 'components/RolesSelect';
@@ -46,6 +48,23 @@ function OverviewTab({
saveErrors = [],
}: OverviewTabProps): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const [, copyToClipboard] = useCopyToClipboard();
const [hasCopiedId, setHasCopiedId] = useState(false);
const handleCopyId = useCallback((): void => {
if (account.id) {
copyToClipboard(account.id);
setHasCopiedId(true);
}
}, [account.id, copyToClipboard]);
useEffect(() => {
if (hasCopiedId) {
const timer = setTimeout(() => setHasCopiedId(false), 2000);
return (): void => clearTimeout(timer);
}
return undefined;
}, [hasCopiedId]);
const formatTimestamp = useCallback(
(ts: string | null | undefined): string => {
@@ -93,6 +112,17 @@ function OverviewTab({
</label>
<div className="sa-drawer__input-wrapper sa-drawer__input-wrapper--disabled">
<span className="sa-drawer__input-text">{account.id || '—'}</span>
{account.id && (
<Button
variant="link"
color="secondary"
onClick={handleCopyId}
className="sa-drawer__copy-btn"
data-testid="copy-id-btn"
>
{hasCopiedId ? <Check size={14} /> : <Copy size={14} />}
</Button>
)}
<LockKeyhole size={14} className="sa-drawer__lock-icon" />
</div>
</div>

View File

@@ -203,6 +203,19 @@
opacity: 0.6;
}
&__copy-btn {
flex-shrink: 0;
padding: 0;
height: auto;
min-height: auto;
color: var(--foreground);
opacity: 0.6;
&:hover {
opacity: 1;
}
}
&__disabled-roles {
display: flex;
flex-wrap: wrap;

View File

@@ -16,7 +16,6 @@ import {
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import { GuardAuthZ } from 'components/GuardAuthZ/GuardAuthZ';
import PermissionDeniedCallout from 'components/PermissionDeniedCallout/PermissionDeniedCallout';
import { useRoles } from 'components/RolesSelect';
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
@@ -477,15 +476,9 @@ function ServiceAccountDrawer({
!isAccountLoading &&
!isAccountError &&
selectedAccountId && (
<GuardAuthZ
relation="read"
object={`serviceaccount:${selectedAccountId}`}
fallbackOnNoPermissions={(): JSX.Element => (
<PermissionDeniedCallout permissionName="serviceaccount:read" />
)}
>
<>
{activeTab === ServiceAccountDrawerTab.Overview && account && (
<>
{activeTab === ServiceAccountDrawerTab.Overview &&
(canRead && account ? (
<OverviewTab
account={account}
localName={localName}
@@ -504,23 +497,24 @@ function ServiceAccountDrawer({
onRefetchRoles={refetchRoles}
saveErrors={saveErrors}
/>
)}
{activeTab === ServiceAccountDrawerTab.Keys &&
(canListKeys ? (
<KeysTab
keys={keys}
isLoading={keysLoading}
isDisabled={isDeleted}
canUpdate={canUpdate}
accountId={selectedAccountId}
currentPage={keysPage}
pageSize={PAGE_SIZE}
/>
) : (
<PermissionDeniedCallout permissionName="factor-api-key:list" />
))}
</>
</GuardAuthZ>
) : (
<PermissionDeniedCallout permissionName="serviceaccount:read" />
))}
{activeTab === ServiceAccountDrawerTab.Keys &&
(canListKeys ? (
<KeysTab
keys={keys}
isLoading={keysLoading}
isDisabled={isDeleted}
canUpdate={canUpdate}
accountId={selectedAccountId}
currentPage={keysPage}
pageSize={PAGE_SIZE}
/>
) : (
<PermissionDeniedCallout permissionName="factor-api-key:list" />
))}
</>
)}
</div>
</div>

View File

@@ -22,6 +22,7 @@ jest.mock('providers/Timezone', () => ({
},
updateTimezone: jest.fn(),
formatTimezoneAdjustedTimestamp: jest.fn(() => 'mock-date'),
formatTimezoneAdjustedTimestampOptional: jest.fn(() => 'mock-date'),
isAdaptationEnabled: true,
setIsAdaptationEnabled: jest.fn(),
}),

View File

@@ -12,4 +12,5 @@ export enum FeatureKeys {
USE_JSON_BODY = 'use_json_body',
USE_FINE_GRAINED_AUTHZ = 'use_fine_grained_authz',
USE_DASHBOARD_V2 = 'use_dashboard_v2',
EMABLE_AI_OBSERVABILITY = 'enable_ai_observability',
}

View File

@@ -43,4 +43,6 @@ export enum LOCALSTORAGE {
DASHBOARD_PREFERENCES = 'DASHBOARD_PREFERENCES',
ACTIVE_SIGNOZ_INSTANCE_URL = 'ACTIVE_SIGNOZ_INSTANCE_URL',
DASHBOARDS_LIST_VISIBLE_COLUMNS = 'DASHBOARDS_LIST_VISIBLE_COLUMNS',
DASHBOARDS_LIST_VIEWS = 'DASHBOARDS_LIST_VIEWS',
DASHBOARD_V2_PANEL_COLUMN_WIDTHS = 'DASHBOARD_V2_PANEL_COLUMN_WIDTHS',
}

View File

@@ -24,6 +24,7 @@ const ROUTES = {
ALL_DASHBOARD: '/dashboard',
DASHBOARD: '/dashboard/:dashboardId',
DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId',
DASHBOARD_PANEL_EDITOR: '/dashboard/:dashboardId/panel/:panelId',
EDIT_ALERTS: '/alerts/edit',
LIST_ALL_ALERT: '/alerts',
ALERTS_NEW: '/alerts/new',
@@ -55,7 +56,9 @@ const ROUTES = {
TRACE_EXPLORER: '/trace-explorer',
BILLING: '/settings/billing',
ROLES_SETTINGS: '/settings/roles',
ROLE_CREATE: '/settings/roles/new',
ROLE_DETAILS: '/settings/roles/:roleId',
ROLE_EDIT: '/settings/roles/:roleId/edit',
MEMBERS_SETTINGS: '/settings/members',
SUPPORT: '/support',
LOGS_SAVE_VIEWS: '/logs/saved-views',

View File

@@ -408,6 +408,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isPublicDashboard = pathname.startsWith('/public/dashboard/');
const isAIAssistantPage = pathname.startsWith('/ai-assistant/');
// The V2 panel editor is a chromeless full-page route (no side nav / top nav),
// like the onboarding and public-dashboard screens.
const isPanelEditorV2 = routeKey === 'DASHBOARD_PANEL_EDITOR';
const renderFullScreen =
pathname === ROUTES.GET_STARTED ||
@@ -418,7 +421,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
pathname === ROUTES.GET_STARTED_AWS_MONITORING ||
pathname === ROUTES.GET_STARTED_AZURE_MONITORING ||
isPublicDashboard;
isPublicDashboard ||
isPanelEditorV2;
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);

View File

@@ -7,15 +7,17 @@
&--legend-right {
flex-direction: row;
.chart-layout__legend-wrapper {
padding-left: 0 !important;
}
}
&__legend-wrapper {
// The inline height is the legend rectangle from calculateChartDimensions;
// border-box keeps the padding inside it so the wrapper doesn't grow past
// that height and steal space from the chart. overflow:hidden clips to the
// rectangle so the virtualized legend scrolls within it.
box-sizing: border-box;
min-height: 0;
overflow: hidden;
padding-left: 12px;
padding-bottom: 12px;
overflow: auto;
}
}

View File

@@ -1,10 +1,12 @@
.members-settings {
.members-settings-page {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
padding: var(--padding-4) var(--padding-2) var(--padding-6) var(--padding-4);
height: 100%;
}
.members-settings {
&__header {
display: flex;
flex-direction: column;

View File

@@ -160,7 +160,7 @@ function MembersSettings(): JSX.Element {
}, [refetchUsers]);
return (
<>
<div className="members-settings-page">
<div className="members-settings">
<div className="members-settings__header">
<h1 className="members-settings__title">Members</h1>
@@ -231,7 +231,7 @@ function MembersSettings(): JSX.Element {
onClose={handleDrawerClose}
onComplete={handleMemberEditComplete}
/>
</>
</div>
);
}

View File

@@ -191,9 +191,6 @@ function TimeSeries({
if (metrics[0] && yAxisUnit) {
updateMetricMetadata(
{
pathParams: {
metricName: metricNames[0],
},
data: buildUpdateMetricYAxisUnitPayload(
metricNames[0],
metrics[0],

View File

@@ -48,18 +48,14 @@ function AllAttributes({
isLoading: isLoadingAttributes,
isError: isErrorAttributes,
refetch: refetchAttributes,
} = useGetMetricAttributes(
{
metricName,
},
{
start: minTime ? Math.floor(minTime / 1000000) : undefined,
end: maxTime ? Math.floor(maxTime / 1000000) : undefined,
},
);
} = useGetMetricAttributes({
metricName,
start: minTime ? Math.floor(minTime / 1000000) : undefined,
end: maxTime ? Math.floor(maxTime / 1000000) : undefined,
});
const attributes = useMemo(
() => attributesData?.data?.attributes ?? [],
() => attributesData?.data.attributes ?? [],
[attributesData],
);

View File

@@ -237,9 +237,6 @@ function Metadata({
const handleSave = useCallback(() => {
updateMetricMetadata(
{
pathParams: {
metricName,
},
data: transformUpdateMetricMetadataRequest(metricName, metricMetadataState),
},
{

View File

@@ -56,7 +56,7 @@ function MetricDetails({
);
const metadata = useMemo(() => {
if (!metricMetadataResponse?.data) {
if (!metricMetadataResponse) {
return null;
}
const { type, description, unit, temporality, isMonotonic } =

View File

@@ -195,14 +195,12 @@ describe('Metadata', () => {
expect(mockUseUpdateMetricMetadata).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
metricName: MOCK_METRIC_NAME,
type: MetrictypesTypeDTO.sum,
temporality: MetrictypesTemporalityDTO.cumulative,
unit: 'By',
isMonotonic: true,
}),
pathParams: {
metricName: MOCK_METRIC_NAME,
},
}),
expect.objectContaining({
onSuccess: expect.any(Function),

View File

@@ -0,0 +1,107 @@
.createEditRolePage {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
padding: var(--spacing-8);
width: 100%;
max-width: 1400px;
margin: 0 auto;
height: 100%;
min-height: 0;
}
.createEditRolePageHeader {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-8);
}
.createEditRolePageHeaderLeft {
display: flex;
align-items: center;
gap: var(--spacing-6);
}
.backButton {
--button-padding: var(--spacing-3);
}
.createEditRolePageActions {
display: flex;
align-items: center;
gap: var(--spacing-6);
}
.unsavedIndicator {
display: flex;
align-items: center;
gap: var(--spacing-3);
margin-right: var(--spacing-4);
}
.unsavedDot {
display: block;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--primary);
box-shadow: 0px 0px 6px 0px
color-mix(in srgb, var(--primary-background) 40%, transparent);
flex-shrink: 0;
}
.unsavedText {
color: var(--primary);
}
.createEditRolePageContent {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
flex: 1;
min-height: 0;
}
.createEditRolePageForm {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
}
.formRow {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-8);
}
.formField {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);
--input-focus-background: var(--l2-background);
--input-disabled-background: var(--l2-background);
input::placeholder {
color: var(--l3-foreground);
}
}
.formLabel {
font-family: Inter;
font-size: 12px;
font-weight: var(--font-weight-medium);
line-height: var(--line-height-20);
letter-spacing: 0.48px;
text-transform: uppercase;
color: var(--l2-foreground);
}
.createEditRolePageDivider {
width: 100%;
height: 1px;
background: var(--l1-border);
}

View File

@@ -0,0 +1,291 @@
import { useCallback, useState } from 'react';
import { matchPath, useHistory, useLocation } from 'react-router-dom';
import { ArrowLeft, SolidAlertTriangle } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { ConfirmDialog } from '@signozhq/ui/dialog';
import { Input } from '@signozhq/ui/input';
import { Typography } from '@signozhq/ui/typography';
import { Skeleton } from 'antd';
import ErrorInPlace from 'components/ErrorInPlace/ErrorInPlace';
import PermissionDeniedFullPage from 'components/PermissionDeniedFullPage/PermissionDeniedFullPage';
import ROUTES from 'constants/routes';
import { useRolesFeatureGate } from 'hooks/useRolesFeatureGate';
import useUrlQuery from 'hooks/useUrlQuery';
import APIError from 'types/api/error';
import PermissionEditor from './components/PermissionEditor';
import { useCreateEditRolePageActions } from './useCreateEditRolePageActions';
import { useNavigationBlocker } from '../../../hooks/useNavigationBlocker';
import styles from './CreateEditRolePage.module.scss';
function CreateEditRolePage(): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
const urlQuery = useUrlQuery();
const match = matchPath<{ roleId: string }>(pathname, {
path: ROUTES.ROLE_DETAILS,
});
const roleId = match?.params?.roleId ?? 'new';
const roleName = urlQuery.get('name') ?? '';
const [hasJsonError, setHasJsonError] = useState(false);
const { isRolesEnabled, isLoading: isFeatureGateLoading } =
useRolesFeatureGate();
const {
formData,
editorMode,
setEditorMode,
resources,
setResources,
isLoading,
isSaving,
hasUnsavedChanges,
handleSave,
handleCancel,
handleFormChange,
saveError,
validationErrors,
isCreateMode,
hasRequiredPermission,
isAuthZLoading,
deniedPermission,
loadError,
} = useCreateEditRolePageActions(roleId, roleName);
const { isBlocked, confirmNavigation, cancelNavigation, allowNextNavigation } =
useNavigationBlocker(hasUnsavedChanges);
const handleSaveAndNavigate = useCallback(async (): Promise<void> => {
if (hasJsonError) {
return;
}
const success = await handleSave();
if (success) {
allowNextNavigation();
if (isCreateMode) {
history.push(ROUTES.ROLES_SETTINGS);
} else {
const viewUrl = `${ROUTES.ROLE_DETAILS.replace(':roleId', roleId)}?name=${encodeURIComponent(roleName)}`;
history.push(viewUrl);
}
}
}, [
handleSave,
allowNextNavigation,
history,
hasJsonError,
isCreateMode,
roleId,
roleName,
]);
if (!hasRequiredPermission && !isAuthZLoading) {
return <PermissionDeniedFullPage permissionName={deniedPermission} />;
}
if (!isRolesEnabled && !isFeatureGateLoading) {
return (
<div
className={styles.createEditRolePage}
data-testid="create-edit-role-page"
>
<div className={styles.createEditRolePageHeader}>
<div className={styles.createEditRolePageHeaderLeft}>
<Button
variant="ghost"
color="secondary"
onClick={handleCancel}
data-testid="cancel-button"
className={styles.backButton}
>
<ArrowLeft size={16} />
</Button>
<Typography.Title level={3}>
{isCreateMode ? 'Create Role' : 'Edit Role'}
</Typography.Title>
</div>
</div>
<ErrorInPlace
error={
new APIError({
httpStatusCode: 403,
error: {
code: 'FEATURE_DISABLED',
message:
'Custom roles feature is not available. Please check your license or feature configuration.',
url: '',
errors: [],
},
})
}
data-testid="feature-gate-error-banner"
/>
</div>
);
}
if (isAuthZLoading || (isLoading && !isCreateMode) || isFeatureGateLoading) {
return (
<div className={styles.createEditRolePage}>
<Skeleton active paragraph={{ rows: 8 }} />
</div>
);
}
if (loadError) {
return (
<div
className={styles.createEditRolePage}
data-testid="create-edit-role-page"
>
<div className={styles.createEditRolePageHeader}>
<div className={styles.createEditRolePageHeaderLeft}>
<Button
variant="ghost"
color="secondary"
onClick={handleCancel}
disabled={isSaving}
data-testid="cancel-button"
className={styles.backButton}
>
<ArrowLeft size={16} />
</Button>
<Typography.Title level={3}>Failed to load role</Typography.Title>
</div>
</div>
<ErrorInPlace error={loadError} data-testid="role-load-error-banner" />
</div>
);
}
return (
<div
className={styles.createEditRolePage}
data-testid="create-edit-role-page"
>
<div className={styles.createEditRolePageHeader}>
<div className={styles.createEditRolePageHeaderLeft}>
<Button
variant="ghost"
color="secondary"
onClick={handleCancel}
disabled={isSaving}
data-testid="cancel-button"
className={styles.backButton}
>
<ArrowLeft size={16} />
</Button>
<Typography.Title level={3}>
{isCreateMode
? 'Create Role'
: `Role - ${formData.name || 'Loading role...'}`}
</Typography.Title>
</div>
<div className={styles.createEditRolePageActions}>
{hasUnsavedChanges && (
<div className={styles.unsavedIndicator}>
<span className={styles.unsavedDot} />
<Typography as="span" size="base" className={styles.unsavedText}>
Unsaved changes
</Typography>
</div>
)}
<Button
variant="solid"
color="primary"
onClick={handleSaveAndNavigate}
loading={isSaving}
disabled={!hasUnsavedChanges || hasJsonError}
data-testid="save-button"
>
{isCreateMode ? 'Create role' : 'Save changes'}
</Button>
</div>
</div>
{saveError && (
<ErrorInPlace
error={saveError}
height="auto"
bordered
data-testid="save-error-banner"
/>
)}
<div className={styles.createEditRolePageContent}>
<div className={styles.createEditRolePageForm}>
<div className={styles.formRow}>
{isCreateMode ? (
<div className={styles.formField}>
<label htmlFor="role-name" className={styles.formLabel}>
Name
</label>
<Input
id="role-name"
value={formData.name}
onChange={(e): void => handleFormChange('name', e.target.value)}
placeholder="my-custom-role"
data-testid="role-name-input"
/>
</div>
) : null}
<div className={styles.formField}>
<label htmlFor="role-description" className={styles.formLabel}>
Description
</label>
<Input
id="role-description"
value={formData.description}
onChange={(e): void => handleFormChange('description', e.target.value)}
placeholder="Custom role for the support team"
data-testid="role-description-input"
/>
</div>
</div>
</div>
<div className={styles.createEditRolePageDivider} />
<PermissionEditor
resources={resources}
mode={editorMode}
onModeChange={setEditorMode}
onResourceChange={setResources}
onJsonValidityChange={setHasJsonError}
isLoading={isLoading}
validationErrors={validationErrors}
/>
</div>
<ConfirmDialog
open={isBlocked}
onOpenChange={(next): void => {
if (!next) {
cancelNavigation();
}
}}
title="Discard unsaved changes?"
titleIcon={<SolidAlertTriangle size={14} color="#fdd600" />}
confirmText="Discard"
confirmColor="destructive"
cancelText="Keep editing"
onConfirm={confirmNavigation}
onCancel={cancelNavigation}
data-testid="discard-changes-dialog"
>
<Typography>
{isCreateMode
? 'This new role will not be created.'
: 'Your unsaved changes will be lost.'}
</Typography>
</ConfirmDialog>
</div>
);
}
export default CreateEditRolePage;

View File

@@ -0,0 +1,139 @@
import { Route, Switch } from 'react-router-dom';
import ROUTES from 'constants/routes';
import { FeatureKeys } from 'constants/features';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import { defaultFeatureFlags, render, screen } from 'tests/test-utils';
import { invalidLicense, mockUseAuthZGrantAll } from 'tests/authz-test-utils';
import CreateEditRolePage from '../CreateEditRolePage';
jest.mock('hooks/useAuthZ/useAuthZ');
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
beforeEach(() => {
mockUseAuthZ.mockImplementation(mockUseAuthZGrantAll);
});
afterEach(() => {
jest.clearAllMocks();
});
function renderCreatePage(
appContextOverrides?: Record<string, unknown>,
): ReturnType<typeof render> {
return render(
<Switch>
<Route path={ROUTES.ROLES_SETTINGS} exact>
<div data-testid="roles-list-redirect" />
</Route>
<Route path={ROUTES.ROLE_CREATE}>
<CreateEditRolePage />
</Route>
</Switch>,
undefined,
{ initialRoute: '/settings/roles/new', appContextOverrides },
);
}
function renderEditPage(
roleId: string,
roleName: string,
appContextOverrides?: Record<string, unknown>,
): ReturnType<typeof render> {
return render(
<Switch>
<Route path={ROUTES.ROLES_SETTINGS} exact>
<div data-testid="roles-list-redirect" />
</Route>
<Route path={ROUTES.ROLE_EDIT}>
<CreateEditRolePage />
</Route>
</Switch>,
undefined,
{
initialRoute: `/settings/roles/${roleId}/edit?name=${encodeURIComponent(roleName)}`,
appContextOverrides,
},
);
}
describe('CreateEditRolePage - Feature Gate', () => {
describe('create mode - feature disabled', () => {
it('shows error when fine-grained authz flag is inactive', async () => {
renderCreatePage({
featureFlags: defaultFeatureFlags.map((f) =>
f.name === FeatureKeys.USE_FINE_GRAINED_AUTHZ
? { ...f, active: false }
: f,
),
});
expect(screen.getByTestId('feature-gate-error-banner')).toBeInTheDocument();
await expect(
screen.findByText(/Custom roles feature is not available/i),
).resolves.toBeInTheDocument();
});
it('shows error when license is invalid', async () => {
renderCreatePage({ activeLicense: invalidLicense });
expect(screen.getByTestId('feature-gate-error-banner')).toBeInTheDocument();
await expect(
screen.findByText(/Custom roles feature is not available/i),
).resolves.toBeInTheDocument();
});
it('shows Create Role title when feature disabled in create mode', async () => {
renderCreatePage({ activeLicense: invalidLicense });
await expect(screen.findByText('Create Role')).resolves.toBeInTheDocument();
});
it('shows back button when feature disabled', () => {
renderCreatePage({ activeLicense: invalidLicense });
expect(screen.getByTestId('cancel-button')).toBeInTheDocument();
});
it('back button is enabled when feature disabled', () => {
renderCreatePage({ activeLicense: invalidLicense });
expect(screen.getByTestId('cancel-button')).not.toBeDisabled();
});
});
describe('edit mode - feature disabled', () => {
const ROLE_ID = '019c24aa-3333-0001-aaaa-111111111111';
const ROLE_NAME = 'test-role';
it('shows error when fine-grained authz flag is inactive', async () => {
renderEditPage(ROLE_ID, ROLE_NAME, {
featureFlags: defaultFeatureFlags.map((f) =>
f.name === FeatureKeys.USE_FINE_GRAINED_AUTHZ
? { ...f, active: false }
: f,
),
});
expect(screen.getByTestId('feature-gate-error-banner')).toBeInTheDocument();
await expect(
screen.findByText(/Custom roles feature is not available/i),
).resolves.toBeInTheDocument();
});
it('shows error when license is invalid', async () => {
renderEditPage(ROLE_ID, ROLE_NAME, { activeLicense: invalidLicense });
expect(screen.getByTestId('feature-gate-error-banner')).toBeInTheDocument();
await expect(
screen.findByText(/Custom roles feature is not available/i),
).resolves.toBeInTheDocument();
});
it('shows Edit Role title when feature disabled in edit mode', async () => {
renderEditPage(ROLE_ID, ROLE_NAME, { activeLicense: invalidLicense });
await expect(screen.findByText('Edit Role')).resolves.toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,59 @@
import { Route, Switch } from 'react-router-dom';
import ROUTES from 'constants/routes';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import { render, screen } from 'tests/test-utils';
import { mockUseAuthZDenyAll } from 'tests/authz-test-utils';
import CreateEditRolePage from '../CreateEditRolePage';
jest.mock('hooks/useAuthZ/useAuthZ');
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
afterEach(() => {
jest.clearAllMocks();
});
function renderCreatePage(): ReturnType<typeof render> {
return render(
<Switch>
<Route path={ROUTES.ROLES_SETTINGS} exact>
<div data-testid="roles-list-redirect" />
</Route>
<Route path={ROUTES.ROLE_CREATE}>
<CreateEditRolePage />
</Route>
</Switch>,
undefined,
{ initialRoute: '/settings/roles/new' },
);
}
describe('CreateRolePage - AuthZ', () => {
describe('permission denied', () => {
it('shows PermissionDeniedFullPage when create permission denied', async () => {
mockUseAuthZ.mockImplementation(mockUseAuthZDenyAll);
renderCreatePage();
await expect(
screen.findByText(/You are not authorized/i),
).resolves.toBeInTheDocument();
});
});
describe('loading state', () => {
it('shows skeleton while checking permissions', () => {
mockUseAuthZ.mockReturnValue({
isLoading: true,
isFetching: true,
error: null,
permissions: null,
refetchPermissions: jest.fn(),
});
renderCreatePage();
expect(document.querySelector('.ant-skeleton')).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,284 @@
import { Route, Switch } from 'react-router-dom';
import ROUTES from 'constants/routes';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import { mockUseAuthZGrantAll } from 'tests/authz-test-utils';
import CreateEditRolePage from '../CreateEditRolePage';
jest.mock('hooks/useAuthZ/useAuthZ');
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
const rolesApiBase = '*/api/v1/roles';
beforeEach(() => {
mockUseAuthZ.mockImplementation(mockUseAuthZGrantAll);
});
afterEach(() => {
jest.clearAllMocks();
server.resetHandlers();
});
function renderCreatePage(): ReturnType<typeof render> {
return render(
<Switch>
<Route path={ROUTES.ROLES_SETTINGS} exact>
<div data-testid="roles-list-redirect" />
</Route>
<Route path={ROUTES.ROLE_CREATE}>
<CreateEditRolePage />
</Route>
</Switch>,
undefined,
{ initialRoute: '/settings/roles/new' },
);
}
describe('CreateRolePage', () => {
describe('initial render', () => {
it('renders create role page with testId', () => {
renderCreatePage();
expect(screen.getByTestId('create-edit-role-page')).toBeInTheDocument();
});
it('shows breadcrumb with "Create role" as current page', () => {
renderCreatePage();
const page = screen.getByTestId('create-edit-role-page');
const breadcrumbs = within(page).getAllByText('Create role');
expect(breadcrumbs.length).toBeGreaterThanOrEqual(1);
});
it('renders empty name input', () => {
renderCreatePage();
const nameInput = screen.getByTestId('role-name-input');
expect(nameInput).toHaveValue('');
});
it('renders empty description input', () => {
renderCreatePage();
const descInput = screen.getByTestId('role-description-input');
expect(descInput).toHaveValue('');
});
it('name input is enabled in create mode', () => {
renderCreatePage();
const nameInput = screen.getByTestId('role-name-input');
expect(nameInput).not.toBeDisabled();
});
it('save button shows "Create role" text', () => {
renderCreatePage();
const saveBtn = screen.getByTestId('save-button');
expect(saveBtn).toHaveTextContent('Create role');
});
it('save button is disabled when no changes', () => {
renderCreatePage();
const saveBtn = screen.getByTestId('save-button');
expect(saveBtn).toBeDisabled();
});
it('does not show unsaved indicator initially', () => {
renderCreatePage();
expect(screen.queryByText('Unsaved changes')).not.toBeInTheDocument();
});
});
describe('form interactions', () => {
it('enables save button when name is entered', async () => {
const user = userEvent.setup();
renderCreatePage();
const nameInput = screen.getByTestId('role-name-input');
await user.type(nameInput, 'test-role');
const saveBtn = screen.getByTestId('save-button');
expect(saveBtn).not.toBeDisabled();
});
it('shows unsaved indicator when form modified', async () => {
const user = userEvent.setup();
renderCreatePage();
const nameInput = screen.getByTestId('role-name-input');
await user.type(nameInput, 'my-role');
await expect(
screen.findByText('Unsaved changes'),
).resolves.toBeInTheDocument();
});
it('enables save button when description is entered', async () => {
const user = userEvent.setup();
renderCreatePage();
const descInput = screen.getByTestId('role-description-input');
await user.type(descInput, 'Some description');
const saveBtn = screen.getByTestId('save-button');
expect(saveBtn).not.toBeDisabled();
});
});
describe('cancel action', () => {
it('navigates to roles list on cancel', async () => {
const user = userEvent.setup();
renderCreatePage();
const cancelBtn = screen.getByTestId('cancel-button');
await user.click(cancelBtn);
await expect(
screen.findByTestId('roles-list-redirect'),
).resolves.toBeInTheDocument();
});
});
describe('create success flow', () => {
it('calls create API with form data and redirects', async () => {
const createSpy = jest.fn();
server.use(
rest.post(rolesApiBase, async (req, res, ctx) => {
createSpy(await req.json());
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: { id: 'new-role-id', name: 'my-custom-role' },
}),
);
}),
);
const user = userEvent.setup();
renderCreatePage();
const nameInput = screen.getByTestId('role-name-input');
await user.type(nameInput, 'my-custom-role');
const descInput = screen.getByTestId('role-description-input');
await user.type(descInput, 'Role for testing');
const saveBtn = screen.getByTestId('save-button');
await user.click(saveBtn);
await waitFor(() => {
expect(createSpy).toHaveBeenCalledWith(
expect.objectContaining({
name: 'my-custom-role',
description: 'Role for testing',
}),
);
});
await expect(
screen.findByTestId('roles-list-redirect'),
).resolves.toBeInTheDocument();
});
});
describe('create error flows', () => {
it('does not call API when name is empty', async () => {
const createSpy = jest.fn();
server.use(
rest.post(rolesApiBase, async (req, res, ctx) => {
createSpy();
return res(ctx.status(200), ctx.json({ status: 'success' }));
}),
);
const user = userEvent.setup();
renderCreatePage();
const descInput = screen.getByTestId('role-description-input');
await user.type(descInput, 'Description only');
const saveBtn = screen.getByTestId('save-button');
await user.click(saveBtn);
await waitFor(
() => {
expect(createSpy).not.toHaveBeenCalled();
},
{ timeout: 500 },
);
});
it('shows error banner when API fails', async () => {
server.use(
rest.post(rolesApiBase, (_req, res, ctx) =>
res(
ctx.status(400),
ctx.json({
error: { message: 'Role name already exists' },
}),
),
),
);
const user = userEvent.setup();
renderCreatePage();
const nameInput = screen.getByTestId('role-name-input');
await user.type(nameInput, 'duplicate-role');
const saveBtn = screen.getByTestId('save-button');
await user.click(saveBtn);
await expect(
screen.findByTestId('save-error-banner'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText('Role name already exists'),
).resolves.toBeInTheDocument();
});
});
describe('validation errors', () => {
it('shows validation error when Only Selected has no items', async () => {
const user = userEvent.setup();
renderCreatePage();
const nameInput = screen.getByTestId('role-name-input');
await user.type(nameInput, 'valid-role');
const apiKeysCard = screen.getByTestId('resource-card-factor-api-key');
const header = within(apiKeysCard).getByTestId(
'resource-card-header-factor-api-key',
);
await user.click(header);
const readToggle = within(apiKeysCard).getByTestId(
'action-toggle-factor-api-key-read',
);
const onlySelectedBtn = await within(readToggle).findByText('Only selected');
await user.click(onlySelectedBtn);
const saveBtn = screen.getByTestId('save-button');
await user.click(saveBtn);
await expect(
screen.findByTestId('save-error-banner'),
).resolves.toBeInTheDocument();
await expect(
screen.findByText(
'Please add at least one selector for each "Only selected" permission.',
),
).resolves.toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,88 @@
import { Route, Switch } from 'react-router-dom';
import ROUTES from 'constants/routes';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import { render, screen } from 'tests/test-utils';
import {
mockUseAuthZDenyAll,
mockUseAuthZGrantByPrefix,
} from 'tests/authz-test-utils';
import CreateEditRolePage from '../CreateEditRolePage';
jest.mock('hooks/useAuthZ/useAuthZ');
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
const EDIT_ROLE_ID = 'test-role-123';
const EDIT_ROLE_NAME = 'test-role';
afterEach(() => {
jest.clearAllMocks();
});
function renderEditPage(): ReturnType<typeof render> {
return render(
<Switch>
<Route path={ROUTES.ROLES_SETTINGS} exact>
<div data-testid="roles-list-redirect" />
</Route>
<Route path={ROUTES.ROLE_DETAILS}>
<CreateEditRolePage />
</Route>
</Switch>,
undefined,
{ initialRoute: `/settings/roles/${EDIT_ROLE_ID}?name=${EDIT_ROLE_NAME}` },
);
}
describe('EditRolePage - AuthZ', () => {
describe('permission denied', () => {
it('shows PermissionDeniedFullPage when read permission denied', async () => {
mockUseAuthZ.mockImplementation(mockUseAuthZDenyAll);
renderEditPage();
await expect(
screen.findByText(/You are not authorized/i),
).resolves.toBeInTheDocument();
});
it('shows PermissionDeniedFullPage when update permission denied but read granted', async () => {
mockUseAuthZ.mockImplementation(mockUseAuthZGrantByPrefix('read'));
renderEditPage();
await expect(
screen.findByText(/You are not authorized/i),
).resolves.toBeInTheDocument();
});
it('checks both read and update permissions for edit mode', () => {
mockUseAuthZ.mockImplementation(mockUseAuthZDenyAll);
renderEditPage();
expect(mockUseAuthZ).toHaveBeenCalledWith(
expect.arrayContaining([
expect.stringContaining('read'),
expect.stringContaining('update'),
]),
);
});
});
describe('loading state', () => {
it('shows skeleton while checking permissions', () => {
mockUseAuthZ.mockReturnValue({
isLoading: true,
isFetching: true,
error: null,
permissions: null,
refetchPermissions: jest.fn(),
});
renderEditPage();
expect(document.querySelector('.ant-skeleton')).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,398 @@
import { Route, Switch } from 'react-router-dom';
import ROUTES from 'constants/routes';
import { customRoleResponse } from 'mocks-server/__mockdata__/roles';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import { mockUseAuthZGrantAll } from 'tests/authz-test-utils';
import CreateEditRolePage from '../CreateEditRolePage';
jest.mock('hooks/useAuthZ/useAuthZ');
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
const CUSTOM_ROLE_ID = '019c24aa-3333-0001-aaaa-111111111111';
const rolesApiBase = '*/api/v1/roles';
const roleWithTransactionGroups = {
status: 'success',
data: {
...customRoleResponse.data,
transactionGroups: [
{
objectGroup: {
resource: { kind: 'role', type: 'role' },
selectors: ['*'],
},
relation: 'read',
},
],
},
};
beforeEach(() => {
mockUseAuthZ.mockImplementation(mockUseAuthZGrantAll);
server.use(
rest.get(`${rolesApiBase}/:id`, (_req, res, ctx) =>
res(ctx.status(200), ctx.json(roleWithTransactionGroups)),
),
);
});
afterEach(() => {
jest.clearAllMocks();
server.resetHandlers();
});
function renderEditPage(roleId = CUSTOM_ROLE_ID): ReturnType<typeof render> {
return render(
<Switch>
<Route path={ROUTES.ROLES_SETTINGS} exact>
<div data-testid="roles-list-redirect" />
</Route>
<Route path={ROUTES.ROLE_DETAILS} exact>
<div data-testid="role-details-redirect" />
</Route>
<Route path={ROUTES.ROLE_EDIT}>
<CreateEditRolePage />
</Route>
</Switch>,
undefined,
{ initialRoute: `/settings/roles/${roleId}/edit?name=Custom%20Role` },
);
}
describe('EditRolePage', () => {
describe('loading state', () => {
it('shows skeleton while fetching role data', () => {
server.use(
rest.get(`${rolesApiBase}/:id`, (_req, res, ctx) =>
res(ctx.delay(200), ctx.status(200), ctx.json(roleWithTransactionGroups)),
),
);
renderEditPage();
expect(document.querySelector('.ant-skeleton')).toBeInTheDocument();
});
});
describe('load error state', () => {
it('shows error banner when role load fails', async () => {
server.use(
rest.get(`${rolesApiBase}/:id`, (_req, res, ctx) =>
res(ctx.status(500), ctx.json({ error: { message: 'Server error' } })),
),
);
renderEditPage();
await waitFor(() => {
expect(document.querySelector('.error-in-place')).toBeInTheDocument();
});
});
it('shows Failed to load role title on load error', async () => {
server.use(
rest.get(`${rolesApiBase}/:id`, (_req, res, ctx) =>
res(ctx.status(404), ctx.json({ error: { message: 'Not found' } })),
),
);
renderEditPage();
await expect(
screen.findByText('Failed to load role'),
).resolves.toBeInTheDocument();
});
it('shows back button on load error', async () => {
server.use(
rest.get(`${rolesApiBase}/:id`, (_req, res, ctx) =>
res(ctx.status(500), ctx.json({ error: { message: 'Server error' } })),
),
);
renderEditPage();
await waitFor(() => {
expect(screen.getByTestId('cancel-button')).toBeInTheDocument();
});
});
it('navigates to view page when cancel clicked in error state', async () => {
const user = userEvent.setup();
server.use(
rest.get(`${rolesApiBase}/:id`, (_req, res, ctx) =>
res(ctx.status(500), ctx.json({ error: { message: 'Server error' } })),
),
);
renderEditPage();
await waitFor(async () => {
const cancelButton = await screen.findByTestId('cancel-button');
await user.click(cancelButton);
});
await expect(
screen.findByTestId('role-details-redirect'),
).resolves.toBeInTheDocument();
});
});
describe('initial render with loaded data', () => {
it('shows role name in page title', async () => {
renderEditPage();
await expect(
screen.findByText('Role - billing-manager'),
).resolves.toBeInTheDocument();
});
it('name input is not shown in edit mode', async () => {
renderEditPage();
await waitFor(() => {
expect(screen.queryByTestId('role-name-input')).not.toBeInTheDocument();
});
});
it('populates description input with existing value', async () => {
renderEditPage();
await waitFor(async () => {
const descInput = await screen.findByTestId('role-description-input');
expect(descInput).toHaveValue(
'Custom role for managing billing and invoices.',
);
});
});
it('description input is enabled in edit mode', async () => {
renderEditPage();
const descInput = await screen.findByTestId('role-description-input');
expect(descInput).not.toBeDisabled();
});
it('save button shows "Save changes" text', async () => {
renderEditPage();
const saveBtn = await screen.findByTestId('save-button');
expect(saveBtn).toHaveTextContent('Save changes');
});
it('save button is disabled when no unsaved changes', async () => {
renderEditPage();
await waitFor(async () => {
const descInput = await screen.findByTestId('role-description-input');
expect(descInput).toHaveValue(
'Custom role for managing billing and invoices.',
);
});
const saveBtn = screen.getByTestId('save-button');
expect(saveBtn).toBeDisabled();
});
});
describe('form interactions', () => {
it('enables save button when description is modified', async () => {
const user = userEvent.setup();
renderEditPage();
const descInput = await screen.findByTestId('role-description-input');
await user.clear(descInput);
await user.type(descInput, 'New description');
const saveBtn = screen.getByTestId('save-button');
expect(saveBtn).not.toBeDisabled();
});
it('shows unsaved indicator when description modified', async () => {
const user = userEvent.setup();
renderEditPage();
const descInput = await screen.findByTestId('role-description-input');
await user.type(descInput, ' updated');
await expect(
screen.findByText('Unsaved changes'),
).resolves.toBeInTheDocument();
});
it('disables save when changes reverted to original', async () => {
const user = userEvent.setup();
renderEditPage();
const descInput = await screen.findByTestId('role-description-input');
const originalValue = 'Custom role for managing billing and invoices.';
await user.clear(descInput);
await user.type(descInput, 'Temporary change');
expect(screen.getByTestId('save-button')).not.toBeDisabled();
await user.clear(descInput);
await user.type(descInput, originalValue);
await waitFor(() => {
expect(screen.getByTestId('save-button')).toBeDisabled();
});
});
});
describe('cancel action', () => {
it('navigates to view role page on cancel', async () => {
const user = userEvent.setup();
renderEditPage();
await screen.findByTestId('role-description-input');
const cancelBtn = screen.getByTestId('cancel-button');
await user.click(cancelBtn);
await expect(
screen.findByTestId('role-details-redirect'),
).resolves.toBeInTheDocument();
});
});
describe('update success flow', () => {
it('redirects to view page after successful update', async () => {
server.use(
rest.put(`${rolesApiBase}/:id`, (_req, res, ctx) =>
res(ctx.status(200), ctx.json({ status: 'success' })),
),
);
const user = userEvent.setup();
renderEditPage();
const descInput = await screen.findByTestId('role-description-input');
await user.clear(descInput);
await user.type(descInput, 'Updated description');
const saveBtn = screen.getByTestId('save-button');
await user.click(saveBtn);
await expect(
screen.findByTestId('role-details-redirect'),
).resolves.toBeInTheDocument();
});
it('calls update API when save clicked', async () => {
const updateSpy = jest.fn();
server.use(
rest.put(`${rolesApiBase}/:id`, async (req, res, ctx) => {
updateSpy();
return res(ctx.status(200), ctx.json({ status: 'success' }));
}),
);
const user = userEvent.setup();
renderEditPage();
const descInput = await screen.findByTestId('role-description-input');
await user.type(descInput, ' edited');
const saveBtn = screen.getByTestId('save-button');
await user.click(saveBtn);
await waitFor(() => {
expect(updateSpy).toHaveBeenCalled();
});
});
});
describe('update error flow', () => {
it('shows error banner when update fails with 500', async () => {
server.use(
rest.put(`${rolesApiBase}/:id`, (_req, res, ctx) => res(ctx.status(500))),
);
const user = userEvent.setup();
renderEditPage();
const descInput = await screen.findByTestId('role-description-input');
await user.type(descInput, ' changed');
const saveBtn = screen.getByTestId('save-button');
await user.click(saveBtn);
await expect(
screen.findByTestId('save-error-banner'),
).resolves.toBeInTheDocument();
});
it('shows error banner when update fails with 403', async () => {
server.use(
rest.put(`${rolesApiBase}/:id`, (_req, res, ctx) => res(ctx.status(403))),
);
const user = userEvent.setup();
renderEditPage();
const descInput = await screen.findByTestId('role-description-input');
await user.type(descInput, ' test');
const saveBtn = screen.getByTestId('save-button');
await user.click(saveBtn);
await expect(
screen.findByTestId('save-error-banner'),
).resolves.toBeInTheDocument();
});
it('shows error banner when update fails with 400', async () => {
server.use(
rest.put(`${rolesApiBase}/:id`, (_req, res, ctx) => res(ctx.status(400))),
);
const user = userEvent.setup();
renderEditPage();
const descInput = await screen.findByTestId('role-description-input');
await user.type(descInput, ' x');
const saveBtn = screen.getByTestId('save-button');
await user.click(saveBtn);
await expect(
screen.findByTestId('save-error-banner'),
).resolves.toBeInTheDocument();
});
});
describe('permission changes', () => {
it('detects permission change as unsaved', async () => {
const user = userEvent.setup();
renderEditPage();
await screen.findByTestId('permission-editor');
const apiKeysCard = await screen.findByTestId(
'resource-card-factor-api-key',
);
const header = within(apiKeysCard).getByTestId(
'resource-card-header-factor-api-key',
);
await user.click(header);
const createToggle = within(apiKeysCard).getByTestId(
'action-toggle-factor-api-key-create',
);
const allBtn = await within(createToggle).findByText('All');
await user.click(allBtn);
await expect(
screen.findByText('Unsaved changes'),
).resolves.toBeInTheDocument();
expect(screen.getByTestId('save-button')).not.toBeDisabled();
});
});
});

View File

@@ -0,0 +1,218 @@
import { Route, Switch } from 'react-router-dom';
import ROUTES from 'constants/routes';
import { render, screen, userEvent, within } from 'tests/test-utils';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import { mockUseAuthZGrantAll } from 'tests/authz-test-utils';
import CreateEditRolePage from '../CreateEditRolePage';
import { TooltipProvider } from '@signozhq/ui/tooltip';
jest.mock('hooks/useAuthZ/useAuthZ');
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
beforeEach(() => {
mockUseAuthZ.mockImplementation(mockUseAuthZGrantAll);
});
afterEach(() => {
jest.clearAllMocks();
});
function renderPage(): ReturnType<typeof render> {
return render(
<TooltipProvider>
<Switch>
<Route path={ROUTES.ROLES_SETTINGS} exact>
<div data-testid="roles-list-redirect" />
</Route>
<Route path={ROUTES.ROLE_CREATE}>
<CreateEditRolePage />
</Route>
</Switch>
</TooltipProvider>,
undefined,
{ initialRoute: '/settings/roles/new' },
);
}
async function switchToJsonMode(): Promise<void> {
const user = userEvent.setup();
const jsonRadio = screen.getByTestId('permission-editor-mode-json');
await user.click(jsonRadio);
}
async function switchToInteractiveMode(): Promise<void> {
const user = userEvent.setup();
const interactiveRadio = screen.getByTestId(
'permission-editor-mode-interactive',
);
await user.click(interactiveRadio);
}
describe('JsonEditor', () => {
describe('initial render', () => {
it('renders JSON editor when JSON mode selected', async () => {
renderPage();
await switchToJsonMode();
expect(screen.getByTestId('json-editor')).toBeInTheDocument();
});
it('renders JSON editor container div', async () => {
renderPage();
await switchToJsonMode();
const jsonEditor = screen.getByTestId('json-editor');
expect(jsonEditor.querySelector('div')).toBeInTheDocument();
});
});
describe('sync with interactive mode', () => {
it('syncs changes from interactive mode when switching to JSON', async () => {
const user = userEvent.setup();
renderPage();
await screen.findByTestId('permission-editor');
await switchToInteractiveMode();
const apiKeyCard = await screen.findByTestId('resource-card-factor-api-key');
const header = within(apiKeyCard).getByTestId(
'resource-card-header-factor-api-key',
);
await user.click(header);
const createToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-create',
);
await user.click(await within(createToggle).findByText('All'));
await switchToJsonMode();
const jsonEditor = screen.getByTestId('json-editor');
expect(jsonEditor).toBeInTheDocument();
});
it('preserves changes when switching back to interactive', async () => {
const user = userEvent.setup();
renderPage();
await screen.findByTestId('permission-editor');
await switchToInteractiveMode();
const apiKeyCard = await screen.findByTestId('resource-card-factor-api-key');
const header = within(apiKeyCard).getByTestId(
'resource-card-header-factor-api-key',
);
await user.click(header);
const createToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-create',
);
await user.click(await within(createToggle).findByText('All'));
await switchToJsonMode();
const interactiveRadio = screen.getByTestId(
'permission-editor-mode-interactive',
);
await user.click(interactiveRadio);
const scopeToggle = within(
screen.getByTestId('action-toggle-factor-api-key-create'),
).getByTestId('action-toggle-scope-factor-api-key-create');
expect(
within(scopeToggle).getByRole('radio', { name: 'All' }),
).toBeChecked();
});
});
describe('error handling', () => {
it('no error shown initially with valid JSON', async () => {
renderPage();
await switchToJsonMode();
expect(screen.queryByTestId('json-editor-error')).not.toBeInTheDocument();
});
});
describe('JSON structure', () => {
it('produces valid transactionGroups format', async () => {
const user = userEvent.setup();
renderPage();
await screen.findByTestId('permission-editor');
await switchToInteractiveMode();
const apiKeyCard = await screen.findByTestId('resource-card-factor-api-key');
const header = within(apiKeyCard).getByTestId(
'resource-card-header-factor-api-key',
);
await user.click(header);
const readToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-read',
);
await user.click(await within(readToggle).findByText('Only selected'));
const input = screen.getByTestId('item-input-selector-input');
await user.type(input, 'test-key-123{enter}');
await switchToJsonMode();
expect(screen.getByTestId('json-editor')).toBeInTheDocument();
});
it('handles wildcard selector for All scope', async () => {
const user = userEvent.setup();
renderPage();
await screen.findByTestId('permission-editor');
await switchToInteractiveMode();
const apiKeyCard = await screen.findByTestId('resource-card-factor-api-key');
const header = within(apiKeyCard).getByTestId(
'resource-card-header-factor-api-key',
);
await user.click(header);
const createToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-create',
);
await user.click(await within(createToggle).findByText('All'));
await switchToJsonMode();
expect(screen.getByTestId('json-editor')).toBeInTheDocument();
});
});
describe('mode switching', () => {
it('reinitializes JSON buffer on switch from interactive to JSON', async () => {
const user = userEvent.setup();
renderPage();
await screen.findByTestId('permission-editor');
await switchToJsonMode();
const interactiveRadio = screen.getByTestId(
'permission-editor-mode-interactive',
);
await user.click(interactiveRadio);
const apiKeyCard = await screen.findByTestId('resource-card-factor-api-key');
const header = within(apiKeyCard).getByTestId(
'resource-card-header-factor-api-key',
);
await user.click(header);
const readToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-read',
);
await user.click(await within(readToggle).findByText('All'));
await switchToJsonMode();
expect(screen.getByTestId('json-editor')).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,523 @@
import { Route, Switch } from 'react-router-dom';
import ROUTES from 'constants/routes';
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
import { useAuthZ } from 'hooks/useAuthZ/useAuthZ';
import { mockUseAuthZGrantAll } from 'tests/authz-test-utils';
import { TooltipProvider } from '@signozhq/ui/tooltip';
import CreateEditRolePage from '../CreateEditRolePage';
jest.mock('hooks/useAuthZ/useAuthZ');
const mockUseAuthZ = useAuthZ as jest.MockedFunction<typeof useAuthZ>;
async function expandAllCards(): Promise<void> {
const user = userEvent.setup();
const expandButton = screen.getByTestId('expand-all-button');
await user.click(expandButton);
}
beforeEach(() => {
mockUseAuthZ.mockImplementation(mockUseAuthZGrantAll);
});
afterEach(() => {
jest.clearAllMocks();
});
function renderPage(): ReturnType<typeof render> {
return render(
<TooltipProvider>
<Switch>
<Route path={ROUTES.ROLES_SETTINGS} exact>
<div data-testid="roles-list-redirect" />
</Route>
<Route path={ROUTES.ROLE_CREATE}>
<CreateEditRolePage />
</Route>
</Switch>
</TooltipProvider>,
undefined,
{ initialRoute: '/settings/roles/new' },
);
}
describe('PermissionEditor', () => {
describe('mode toggle', () => {
it('renders permission editor with testId', () => {
renderPage();
expect(screen.getByTestId('permission-editor')).toBeInTheDocument();
});
it('defaults to interactive mode', () => {
renderPage();
const interactiveRadio = screen.getByTestId(
'permission-editor-mode-interactive',
);
expect(interactiveRadio).toBeChecked();
});
it('switches to JSON mode when clicked', async () => {
const user = userEvent.setup();
renderPage();
const jsonRadio = screen.getByTestId('permission-editor-mode-json');
await user.click(jsonRadio);
expect(jsonRadio).toBeChecked();
expect(screen.getByTestId('json-editor')).toBeInTheDocument();
});
it('switches back to interactive mode', async () => {
const user = userEvent.setup();
renderPage();
const jsonRadio = screen.getByTestId('permission-editor-mode-json');
await user.click(jsonRadio);
const interactiveRadio = screen.getByTestId(
'permission-editor-mode-interactive',
);
await user.click(interactiveRadio);
expect(interactiveRadio).toBeChecked();
expect(screen.queryByTestId('json-editor')).not.toBeInTheDocument();
});
});
describe('resource cards', () => {
it('renders all resource cards', () => {
renderPage();
expect(
screen.getByTestId('resource-card-factor-api-key'),
).toBeInTheDocument();
expect(screen.getByTestId('resource-card-role')).toBeInTheDocument();
expect(
screen.getByTestId('resource-card-serviceaccount'),
).toBeInTheDocument();
});
it('resource cards are collapsed by default', () => {
renderPage();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const header = within(apiKeyCard).getByTestId(
'resource-card-header-factor-api-key',
);
expect(header).toHaveAttribute('aria-expanded', 'false');
});
it('expands resource card when header clicked', async () => {
const user = userEvent.setup();
renderPage();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const header = within(apiKeyCard).getByTestId(
'resource-card-header-factor-api-key',
);
await user.click(header);
expect(header).toHaveAttribute('aria-expanded', 'true');
});
it('collapses expanded resource card when header clicked again', async () => {
const user = userEvent.setup();
renderPage();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const header = within(apiKeyCard).getByTestId(
'resource-card-header-factor-api-key',
);
await user.click(header);
await user.click(header);
expect(header).toHaveAttribute('aria-expanded', 'false');
});
it('shows granted count in resource card header', async () => {
renderPage();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
await expect(
within(apiKeyCard).findByText(/0 \/ \d+ granted/),
).resolves.toBeInTheDocument();
});
});
describe('action toggles', () => {
it('renders action toggles for each available action', async () => {
renderPage();
await expandAllCards();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
expect(
within(apiKeyCard).getByTestId('action-toggle-factor-api-key-read'),
).toBeInTheDocument();
expect(
within(apiKeyCard).getByTestId('action-toggle-factor-api-key-read'),
).toBeInTheDocument();
expect(
within(apiKeyCard).getByTestId('action-toggle-factor-api-key-update'),
).toBeInTheDocument();
expect(
within(apiKeyCard).getByTestId('action-toggle-factor-api-key-delete'),
).toBeInTheDocument();
});
it('defaults all actions to None scope', async () => {
renderPage();
await expandAllCards();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const createToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-read',
);
const scopeToggle = within(createToggle).getByTestId(
'action-toggle-scope-factor-api-key-read',
);
expect(
within(scopeToggle).getByRole('radio', { name: 'None' }),
).toBeChecked();
});
it('changes scope to All when clicked', async () => {
const user = userEvent.setup();
renderPage();
await expandAllCards();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const createToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-read',
);
const allBtn = await within(createToggle).findByText('All');
await user.click(allBtn);
const scopeToggle = within(createToggle).getByTestId(
'action-toggle-scope-factor-api-key-read',
);
expect(
within(scopeToggle).getByRole('radio', { name: 'All' }),
).toBeChecked();
});
it('updates granted count when scope changed', async () => {
const user = userEvent.setup();
renderPage();
await expandAllCards();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const createToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-read',
);
await user.click(await within(createToggle).findByText('All'));
await expect(
within(apiKeyCard).findByText(/1 \/ \d+ granted/),
).resolves.toBeInTheDocument();
});
});
describe('Only Selected scope', () => {
it('shows item input selector when Only Selected is chosen', async () => {
const user = userEvent.setup();
renderPage();
await expandAllCards();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const createToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-read',
);
const onlySelectedBtn =
await within(createToggle).findByText('Only selected');
await user.click(onlySelectedBtn);
expect(screen.getByTestId('item-input-selector')).toBeInTheDocument();
});
it('adds item when typed and Enter pressed', async () => {
const user = userEvent.setup();
renderPage();
await expandAllCards();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const createToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-read',
);
await user.click(await within(createToggle).findByText('Only selected'));
const input = screen.getByTestId('item-input-selector-input');
await user.type(input, 'api-key-001{enter}');
await expect(screen.findByText('api-key-001')).resolves.toBeInTheDocument();
});
it('adds item when Add button clicked', async () => {
const user = userEvent.setup();
renderPage();
await expandAllCards();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const createToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-read',
);
await user.click(await within(createToggle).findByText('Only selected'));
const input = screen.getByTestId('item-input-selector-input');
await user.type(input, 'api-key-002');
const addBtn = screen.getByTestId('item-input-selector-add-btn');
await user.click(addBtn);
await expect(screen.findByText('api-key-002')).resolves.toBeInTheDocument();
});
it('adds multiple items separated by comma', async () => {
const user = userEvent.setup();
renderPage();
await expandAllCards();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const createToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-read',
);
await user.click(await within(createToggle).findByText('Only selected'));
const input = screen.getByTestId('item-input-selector-input');
await user.type(input, 'key-a, key-b, key-c{enter}');
await expect(screen.findByText('key-a')).resolves.toBeInTheDocument();
await expect(screen.findByText('key-b')).resolves.toBeInTheDocument();
await expect(screen.findByText('key-c')).resolves.toBeInTheDocument();
});
it('adds multiple items separated by space', async () => {
const user = userEvent.setup();
renderPage();
await expandAllCards();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const createToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-read',
);
await user.click(await within(createToggle).findByText('Only selected'));
const input = screen.getByTestId('item-input-selector-input');
await user.type(input, 'key-x key-y key-z{enter}');
await expect(screen.findByText('key-x')).resolves.toBeInTheDocument();
await expect(screen.findByText('key-y')).resolves.toBeInTheDocument();
await expect(screen.findByText('key-z')).resolves.toBeInTheDocument();
});
it('does not add duplicate items', async () => {
const user = userEvent.setup();
renderPage();
await expandAllCards();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const createToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-read',
);
await user.click(await within(createToggle).findByText('Only selected'));
const input = screen.getByTestId('item-input-selector-input');
await user.type(input, 'same-key{enter}');
await user.type(input, 'same-key{enter}');
const badges = screen.getAllByText('same-key');
expect(badges).toHaveLength(1);
});
it('removes item when X clicked', async () => {
const user = userEvent.setup();
renderPage();
await expandAllCards();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const createToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-read',
);
await user.click(await within(createToggle).findByText('Only selected'));
const input = screen.getByTestId('item-input-selector-input');
await user.type(input, 'removable-key{enter}');
const removeBtn = screen.getByRole('button', {
name: /remove removable-key/i,
});
await user.click(removeBtn);
expect(screen.queryByText('removable-key')).not.toBeInTheDocument();
});
it('shows Add button disabled when input is empty', async () => {
const user = userEvent.setup();
renderPage();
await expandAllCards();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const createToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-read',
);
await user.click(await within(createToggle).findByText('Only selected'));
const addBtn = screen.getByTestId('item-input-selector-add-btn');
expect(addBtn).toBeDisabled();
});
});
describe('scope change confirmation dialog', () => {
it('shows confirm dialog when leaving Only Selected with items', async () => {
const user = userEvent.setup();
renderPage();
await expandAllCards();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const createToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-read',
);
await user.click(await within(createToggle).findByText('Only selected'));
const input = screen.getByTestId('item-input-selector-input');
await user.type(input, 'will-be-cleared{enter}');
await user.click(await within(createToggle).findByText('All'));
await expect(
screen.findByText('Change permission scope?'),
).resolves.toBeInTheDocument();
});
it('clears items when confirmed', async () => {
const user = userEvent.setup();
renderPage();
await expandAllCards();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const createToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-read',
);
await user.click(await within(createToggle).findByText('Only selected'));
const input = screen.getByTestId('item-input-selector-input');
await user.type(input, 'to-be-cleared{enter}');
await user.click(await within(createToggle).findByText('All'));
const dialog = await screen.findByRole('dialog');
await user.click(
within(dialog).getByRole('button', { name: /change scope/i }),
);
await waitFor(() => {
expect(screen.queryByText('to-be-cleared')).not.toBeInTheDocument();
});
});
it('keeps items when cancelled', async () => {
const user = userEvent.setup();
renderPage();
await expandAllCards();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const createToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-read',
);
await user.click(await within(createToggle).findByText('Only selected'));
const input = screen.getByTestId('item-input-selector-input');
await user.type(input, 'preserved-key{enter}');
await user.click(await within(createToggle).findByText('None'));
const dialog = await screen.findByRole('dialog');
await user.click(within(dialog).getByRole('button', { name: /cancel/i }));
await expect(
screen.findByText('preserved-key'),
).resolves.toBeInTheDocument();
expect(screen.getByTestId('item-input-selector')).toBeInTheDocument();
});
it('does not show dialog when leaving Only Selected with no items', async () => {
const user = userEvent.setup();
renderPage();
await expandAllCards();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const createToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-read',
);
await user.click(await within(createToggle).findByText('Only selected'));
await user.click(await within(createToggle).findByText('All'));
expect(
screen.queryByText('Change permission scope?'),
).not.toBeInTheDocument();
});
});
describe('verbs without Only Selected option', () => {
it('does not show Only Selected for list verb', async () => {
renderPage();
await expandAllCards();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const listToggle = within(apiKeyCard).getByTestId(
'action-toggle-factor-api-key-list',
);
expect(
within(listToggle).queryByText('Only selected'),
).not.toBeInTheDocument();
await expect(
within(listToggle).findByText('None'),
).resolves.toBeInTheDocument();
await expect(
within(listToggle).findByText('All'),
).resolves.toBeInTheDocument();
});
});
describe('collapse/expand all resources', () => {
it('shows expand/collapse toggle group', () => {
renderPage();
expect(screen.getByTestId('toggle-all-group')).toBeInTheDocument();
expect(screen.getByTestId('expand-all-button')).toBeInTheDocument();
expect(screen.getByTestId('collapse-all-button')).toBeInTheDocument();
});
it('expands all cards when expand button clicked', async () => {
renderPage();
await expandAllCards();
const apiKeyCard = screen.getByTestId('resource-card-factor-api-key');
const header = within(apiKeyCard).getByTestId(
'resource-card-header-factor-api-key',
);
expect(header).toHaveAttribute('aria-expanded', 'true');
});
});
});

View File

@@ -0,0 +1,65 @@
.actionToggle {
position: relative;
display: flex;
flex-direction: column;
gap: var(--spacing-4);
padding: var(--spacing-6) var(--spacing-8);
&::after {
content: '';
position: absolute;
right: var(--spacing-8);
bottom: 0;
left: var(--spacing-8);
height: 1px;
background: var(--l2-border);
}
&:last-child::after {
display: none;
}
}
.actionToggleHeader {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-8);
}
.actionToggleScopeToggle {
flex-shrink: 0;
width: fit-content;
--toggle-group-item-size: 1.4rem;
--toggle-group-item-padding-right: 0.4rem;
--toggle-group-item-border-style: none;
--toggle-group-secondary-active-bg: var(--bg-robin-800);
--toggle-group-item-align-items: baseline;
gap: var(--spacing-2);
padding: var(--spacing-2);
button {
min-width: 46px;
font-weight: bold;
&[data-state='on'] {
color: var(--bg-robin-200);
}
&:focus-visible {
outline: 2px solid var(--bg-robin-500);
outline-offset: 2px;
border-radius: var(--radius-2);
}
}
}
.actionToggleSelectorWrapper {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
--divider-color: var(--l2-border);
}

View File

@@ -0,0 +1,160 @@
import { useCallback, useMemo, useState } from 'react';
import { ConfirmDialog } from '@signozhq/ui/dialog';
import { Divider } from '@signozhq/ui/divider';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Typography } from '@signozhq/ui/typography';
import { PermissionScope } from '../../types';
import { getResourcePanel } from '../../permissions.config';
import ItemInputSelector from './ItemInputSelector';
import styles from './ActionToggle.module.scss';
import { AuthZResource, AuthZVerb } from 'hooks/useAuthZ/types';
import { getActionLabel } from 'container/RolesSettings/ViewRolePage/components/permissionDisplay.utils';
const SCOPE_LABELS: Record<PermissionScope, string> = {
[PermissionScope.NONE]: 'None',
[PermissionScope.ALL]: 'All',
[PermissionScope.ONLY_SELECTED]: 'Only selected',
};
interface ActionToggleProps {
action: AuthZVerb;
scope: string;
selectedIds: string[];
resource: AuthZResource;
canSelectIndividually: boolean;
onScopeChange: (scope: PermissionScope) => void;
onSelectedIdsChange: (ids: string[]) => void;
hasError?: boolean;
}
function ActionToggle({
action,
scope,
selectedIds,
resource,
canSelectIndividually,
onScopeChange,
onSelectedIdsChange,
hasError = false,
}: ActionToggleProps): JSX.Element {
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
const [pendingScope, setPendingScope] = useState<PermissionScope | null>(null);
const displayLabel = getActionLabel(action);
const scopeItems: Array<{ value: PermissionScope; label: string }> =
useMemo(() => {
const items = [
{ value: PermissionScope.NONE, label: SCOPE_LABELS[PermissionScope.NONE] },
{ value: PermissionScope.ALL, label: SCOPE_LABELS[PermissionScope.ALL] },
];
if (canSelectIndividually) {
items.push({
value: PermissionScope.ONLY_SELECTED,
label: SCOPE_LABELS[PermissionScope.ONLY_SELECTED],
});
}
return items;
}, [canSelectIndividually]);
const handleToggleChange = useCallback(
(value: string): void => {
if (!value) {
return;
}
const isLeavingOnlySelected =
scope === PermissionScope.ONLY_SELECTED &&
value !== PermissionScope.ONLY_SELECTED;
const hasSelectedItems = selectedIds.length > 0;
if (isLeavingOnlySelected && hasSelectedItems) {
setPendingScope(value as PermissionScope);
setConfirmDialogOpen(true);
return;
}
onScopeChange(value as PermissionScope);
},
[scope, selectedIds.length, onScopeChange],
);
const handleConfirmScopeChange = useCallback((): void => {
if (pendingScope) {
onSelectedIdsChange([]);
onScopeChange(pendingScope);
}
setConfirmDialogOpen(false);
setPendingScope(null);
}, [pendingScope, onSelectedIdsChange, onScopeChange]);
const handleCancelScopeChange = useCallback((): void => {
setConfirmDialogOpen(false);
setPendingScope(null);
}, []);
return (
<>
<div
className={styles.actionToggle}
data-testid={`action-toggle-${resource}-${action}`}
>
<div className={styles.actionToggleHeader}>
<Typography as="span" size="base">
{displayLabel}
</Typography>
<ToggleGroupSimple
type="single"
size="sm"
value={scope}
onChange={handleToggleChange}
items={scopeItems}
className={styles.actionToggleScopeToggle}
testId={`action-toggle-scope-${resource}-${action}`}
/>
</div>
{scope === PermissionScope.ONLY_SELECTED && (
<div className={styles.actionToggleSelectorWrapper}>
<Divider />
<ItemInputSelector
placeholder={getResourcePanel(resource).selectorPlaceholder}
selectedIds={selectedIds}
onChange={onSelectedIdsChange}
docsAnchor={getResourcePanel(resource).docsAnchor}
hasError={hasError}
/>
</div>
)}
</div>
<ConfirmDialog
open={confirmDialogOpen}
onOpenChange={(next): void => {
if (!next) {
handleCancelScopeChange();
}
}}
title="Change permission scope?"
confirmText="Change scope"
cancelText="Cancel"
onConfirm={handleConfirmScopeChange}
onCancel={handleCancelScopeChange}
>
<Typography>
You have {selectedIds.length} item{selectedIds.length > 1 ? 's' : ''}{' '}
selected. Changing the scope will clear your current items.
<br />
<br />
Don&apos;t worry, this doesn&apos;t update this role yet, it only confirms
that you want to clear the items.
</Typography>
</ConfirmDialog>
</>
);
}
export default ActionToggle;

View File

@@ -0,0 +1,105 @@
.itemInputSelector {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
background: var(--l1-background);
border: 1px solid var(--l1-border);
border-radius: 4px;
padding: var(--spacing-4);
--input-suffix-padding: var(--spacing-2);
}
.itemInputSelectorError {
border-color: var(--destructive);
}
.itemInputSelectorFooter {
position: relative;
display: flex;
align-items: flex-start;
gap: var(--spacing-3);
padding-top: var(--spacing-3);
&::before {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
height: 1px;
background: var(--l1-border);
}
}
.itemInputSelectorBadges {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: var(--spacing-2);
max-height: 60px;
overflow-y: auto;
}
.itemInputSelectorInfoIcon {
flex-shrink: 0;
color: var(--l2-foreground);
cursor: pointer;
&:hover {
color: var(--l1-foreground);
}
}
.itemInputSelectorBadge {
display: inline-flex;
align-items: center;
gap: 4px;
max-width: 140px;
padding: 2px 4px 2px 6px;
background: var(--l2-background);
border: 1px solid var(--l2-border);
border-radius: 4px;
}
.itemInputSelectorBadgeLabel {
flex: 1;
min-width: 0;
}
.itemInputSelectorBadgeRemove {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
padding: 0;
background: transparent;
border: none;
border-radius: 2px;
color: var(--l2-foreground);
cursor: pointer;
transition:
background 0.15s ease,
color 0.15s ease;
&:hover {
background: var(--l1-background);
color: var(--l1-foreground);
}
}
.itemInputSelectorHint {
margin: 0;
color: var(--l2-foreground);
a {
color: var(--primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}

View File

@@ -0,0 +1,206 @@
import { useCallback, useRef, useState } from 'react';
import { Info, Plus, X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { Typography } from '@signozhq/ui/typography';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import cx from 'classnames';
import styles from './ItemInputSelector.module.scss';
const BASE_DOCS_URL =
'https://signoz.io/docs/manage/administrator-guide/iam/permissions/';
export interface ItemInputSelectorProps {
placeholder: string;
selectedIds: string[];
onChange: (ids: string[]) => void;
docsAnchor?: string;
hasError?: boolean;
}
function parseInputValues(input: string): string[] {
return input
.split(/[\s,]+/)
.map((v) => v.trim())
.filter(Boolean);
}
function ItemInputSelector({
placeholder,
selectedIds,
onChange,
docsAnchor = 'role',
hasError = false,
}: ItemInputSelectorProps): JSX.Element {
const [inputValue, setInputValue] = useState('');
const footerRef = useRef<HTMLDivElement>(null);
const addValues = useCallback(
(input: string): void => {
const values = parseInputValues(input);
if (values.length === 0) {
return;
}
const existingSet = new Set(selectedIds);
const newIds = values.filter((v) => !existingSet.has(v));
if (newIds.length > 0) {
onChange([...selectedIds, ...newIds]);
}
setInputValue('');
},
[selectedIds, onChange],
);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>): void => {
setInputValue(e.target.value);
},
[],
);
const handleInputKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
e.preventDefault();
addValues(inputValue);
}
},
[inputValue, addValues],
);
const handleInputBlur = useCallback((): void => {
addValues(inputValue);
}, [inputValue, addValues]);
const handleAddClick = useCallback((): void => {
addValues(inputValue);
}, [inputValue, addValues]);
const handleRemove = useCallback(
(itemId: string): void => {
onChange(selectedIds.filter((id) => id !== itemId));
},
[selectedIds, onChange],
);
const handleBadgeKeyDown = useCallback(
(
e: React.KeyboardEvent<HTMLButtonElement>,
itemId: string,
index: number,
): void => {
if (e.key !== 'Enter' && e.key !== ' ') {
return;
}
e.preventDefault();
handleRemove(itemId);
const targetIndex = index > 0 ? index - 1 : 0;
requestAnimationFrame(() => {
const buttons = footerRef.current?.querySelectorAll('button');
const targetButton = buttons?.[targetIndex] as
| HTMLButtonElement
| undefined;
targetButton?.focus();
});
},
[handleRemove],
);
const showError = hasError && selectedIds.length === 0;
return (
<div
className={cx(
styles.itemInputSelector,
showError ? styles.itemInputSelectorError : '',
)}
data-testid="item-input-selector"
>
<Input
placeholder={placeholder}
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleInputKeyDown}
onBlur={handleInputBlur}
data-testid="item-input-selector-input"
suffix={
<Button
variant="solid"
size="sm"
onClick={handleAddClick}
disabled={!inputValue.trim()}
data-testid="item-input-selector-add-btn"
>
<Plus size={14} />
Add
</Button>
}
/>
{selectedIds.length > 0 ? (
<div ref={footerRef} className={styles.itemInputSelectorFooter}>
<div className={styles.itemInputSelectorBadges}>
{selectedIds.map((id, index) => (
<span key={id} className={styles.itemInputSelectorBadge} title={id}>
<Typography
as="span"
size="small"
truncate={1}
className={styles.itemInputSelectorBadgeLabel}
>
{id}
</Typography>
<button
type="button"
className={styles.itemInputSelectorBadgeRemove}
onClick={(): void => handleRemove(id)}
onKeyDown={(e): void => handleBadgeKeyDown(e, id, index)}
aria-label={`Remove ${id}`}
>
<X size={10} />
</button>
</span>
))}
</div>
<TooltipSimple
title={
<Typography align="left">
Still not sure on how to add selectors? <br />
<Typography.Link
href={`${BASE_DOCS_URL}#${docsAnchor}`}
target="_blank"
rel="noopener noreferrer"
>
Check the docs
</Typography.Link>{' '}
to understand selectors for this resource.
</Typography>
}
>
<Info size={16} className={styles.itemInputSelectorInfoIcon} />
</TooltipSimple>
</div>
) : (
<Typography className={styles.itemInputSelectorHint}>
Not sure what to type here?{' '}
<Typography.Link
href={`${BASE_DOCS_URL}#${docsAnchor}`}
target="_blank"
rel="noopener noreferrer"
>
Check the docs
</Typography.Link>{' '}
to understand selectors for this resource.
</Typography>
)}
</div>
);
}
export default ItemInputSelector;

View File

@@ -0,0 +1,44 @@
.jsonEditor {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
flex: 1;
min-height: 0;
}
.jsonEditorContainer {
position: relative;
border: 1px solid var(--l2-border);
border-radius: 4px;
overflow: hidden;
flex: 1;
min-height: 200px;
}
.copyButton {
position: absolute;
top: 8px;
right: 24px;
z-index: 10;
}
.jsonEditorErrorWrapper {
min-height: 52px;
}
.jsonEditorError {
display: flex;
align-items: flex-start;
gap: var(--spacing-2);
padding: var(--spacing-4) var(--spacing-6);
background: color-mix(in srgb, var(--danger-background) 10%, transparent);
border: 1px solid var(--danger-background);
border-radius: 4px;
}
.jsonEditorErrorMessage {
font-family:
Geist Mono,
monospace;
word-break: break-word;
}

View File

@@ -0,0 +1,234 @@
import {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useState,
useRef,
} from 'react';
import { useCopyToClipboard } from 'react-use';
import MEditor, { Monaco, OnMount } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import { Check, Copy } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import type { AuthtypesTransactionGroupDTO } from 'api/generated/services/sigNoz.schemas';
import { useIsDarkMode } from 'hooks/useDarkMode';
import {
defineJsonTheme,
EDITABLE_EDITOR_OPTIONS,
JSON_THEME_DARK,
} from '../../monaco.config';
import {
transformResourcePermissionsToTransactionGroups,
transformTransactionGroupsToResourcePermissions,
} from '../../hooks/useRolePermissions';
import {
registerCompletionProvider,
registerJsonSchema,
ROLE_PERMISSIONS_MODEL_PATH,
} from './jsonSchema.config';
import styles from './JsonEditor.module.scss';
import { JsonEditorProps, JsonEditorRef } from './JsonEditor.types';
type MonacoEditor = Parameters<OnMount>[0];
const JsonEditor = forwardRef<JsonEditorRef, JsonEditorProps>(
function JsonEditor({ resources, mode, onChange, onValidityChange }, ref) {
const isDarkMode = useIsDarkMode();
const [copyState, copyToClipboard] = useCopyToClipboard();
const [copied, setCopied] = useState(false);
const [parseError, setParseError] = useState<string | null>(null);
const [schemaErrors, setSchemaErrors] = useState<string[]>([]);
const [jsonBuffer, setJsonBuffer] = useState<string>(() => {
const transactionGroups =
transformResourcePermissionsToTransactionGroups(resources);
return JSON.stringify(transactionGroups, null, 2);
});
const prevModeRef = useRef(mode);
const completionDisposableRef = useRef<{ dispose(): void } | null>(null);
const editorRef = useRef<MonacoEditor | null>(null);
const markersListenerRef = useRef<{ dispose(): void } | null>(null);
const hasError = parseError !== null || schemaErrors.length > 0;
useImperativeHandle(ref, () => ({
hasParseError: (): boolean => hasError,
}));
useEffect(() => {
onValidityChange?.(hasError);
}, [hasError, onValidityChange]);
// Reinitialize buffer when switching from interactive to json mode
useEffect(() => {
const wasInteractive = prevModeRef.current === 'interactive';
const isNowJson = mode === 'json';
if (wasInteractive && isNowJson) {
const transactionGroups =
transformResourcePermissionsToTransactionGroups(resources);
setJsonBuffer(JSON.stringify(transactionGroups, null, 2));
setParseError(null);
}
prevModeRef.current = mode;
}, [mode, resources]);
const handleEditorChange = useCallback(
(value: string | undefined): void => {
if (!value) {
return;
}
setJsonBuffer(value);
try {
const parsed = JSON.parse(value) as AuthtypesTransactionGroupDTO[];
const resourcePermissions =
transformTransactionGroupsToResourcePermissions(parsed);
setParseError(null);
onChange(resourcePermissions);
} catch (err) {
setParseError(err instanceof Error ? err.message : 'Invalid JSON format');
}
},
[onChange],
);
const configureMonaco = useCallback((monaco: Monaco): void => {
defineJsonTheme(monaco);
registerJsonSchema(monaco);
completionDisposableRef.current = registerCompletionProvider(monaco);
}, []);
const handleEditorMount: OnMount = useCallback((editorInstance, monaco) => {
editorRef.current = editorInstance;
type MonacoMarker = ReturnType<typeof monaco.editor.getModelMarkers>[number];
markersListenerRef.current = monaco.editor.onDidChangeMarkers(
(uris: readonly Parameters<typeof monaco.Uri.parse>[0][]) => {
const model = editorInstance.getModel();
if (!model) {
return;
}
const modelUri = model.uri.toString();
const hasRelevantChange = uris.some((uri) => uri.toString() === modelUri);
if (!hasRelevantChange) {
return;
}
const markers = monaco.editor.getModelMarkers({ resource: model.uri });
const errors = markers
.filter(
(marker: MonacoMarker) =>
marker.severity === monaco.MarkerSeverity.Error,
)
.map(
(marker: MonacoMarker) =>
`Line ${marker.startLineNumber}: ${marker.message}`,
);
setSchemaErrors(errors);
},
);
}, []);
useEffect(
() => (): void => {
completionDisposableRef.current?.dispose();
markersListenerRef.current?.dispose();
},
[],
);
useEffect(() => {
if (copyState.value) {
setCopied(true);
const timer = setTimeout(() => setCopied(false), 1500);
return (): void => clearTimeout(timer);
}
return undefined;
}, [copyState]);
const handleCopy = useCallback((): void => {
copyToClipboard(jsonBuffer);
}, [copyToClipboard, jsonBuffer]);
return (
<div className={styles.jsonEditor} data-testid="json-editor">
<div className={styles.jsonEditorContainer}>
<TooltipSimple title={copied ? 'Copied!' : 'Copy JSON'}>
<Button
variant="ghost"
size="sm"
className={styles.copyButton}
onClick={handleCopy}
>
{copied ? (
<Check size={14} color={Color.BG_FOREST_400} />
) : (
<Copy
size={14}
color={isDarkMode ? Color.BG_VANILLA_400 : Color.TEXT_INK_400}
/>
)}
</Button>
</TooltipSimple>
<MEditor
value={jsonBuffer}
language="json"
path={ROLE_PERMISSIONS_MODEL_PATH}
options={EDITABLE_EDITOR_OPTIONS}
onChange={handleEditorChange}
onMount={handleEditorMount}
height="100%"
theme={isDarkMode ? JSON_THEME_DARK : 'light'}
beforeMount={configureMonaco}
/>
</div>
<div className={styles.jsonEditorErrorWrapper}>
{parseError && (
<div className={styles.jsonEditorError} data-testid="json-editor-error">
<Typography as="span" size="base" weight="medium">
Parse Error:
</Typography>
<Typography
as="span"
size="base"
className={styles.jsonEditorErrorMessage}
>
{parseError}
</Typography>
</div>
)}
{!parseError && schemaErrors.length > 0 && (
<div
className={styles.jsonEditorError}
data-testid="json-editor-schema-error"
>
<Typography as="span" size="base" weight="medium">
Schema Error:
</Typography>
<Typography
as="span"
size="base"
className={styles.jsonEditorErrorMessage}
>
{schemaErrors[0]}
{schemaErrors.length > 1 && ` (+${schemaErrors.length - 1} more)`}
</Typography>
</div>
)}
</div>
</div>
);
},
);
export default JsonEditor;

View File

@@ -0,0 +1,14 @@
import { ResourcePermissions } from '../../types';
export type EditorMode = 'interactive' | 'json';
export interface JsonEditorProps {
resources: ResourcePermissions[];
mode: EditorMode;
onChange: (resources: ResourcePermissions[]) => void;
onValidityChange?: (hasError: boolean) => void;
}
export interface JsonEditorRef {
hasParseError: () => boolean;
}

View File

@@ -0,0 +1,155 @@
.permissionEditor {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
flex: 1;
min-height: 0;
}
.permissionEditorHeader {
display: flex;
align-items: center;
justify-content: space-between;
}
.permissionEditorTitle {
letter-spacing: 0.48px;
text-transform: uppercase;
position: relative;
}
.permissionEditorDivider {
height: 7px;
flex: 1 1 0%;
border-width: medium medium;
border-style: none none;
border-color: currentcolor currentcolor;
border-image: initial;
border-top: 2px dotted var(--l1-border);
border-bottom: 2px dotted var(--l1-border);
margin: 0px var(--spacing-4);
}
.permissionEditorModeToggle {
display: inline-flex;
grid-auto-flow: column;
gap: 0;
flex-shrink: 0;
border: 1px solid var(--l2-border);
border-radius: 2px;
}
.permissionEditorModeItem {
position: relative;
display: flex;
align-items: center;
&:not(:last-child) {
border-right: 1px solid var(--l2-border);
}
label {
display: flex;
align-items: center;
min-height: 24px;
padding: var(--spacing-3) var(--spacing-6);
font-family: Inter;
font-size: var(--periscope-font-size-base);
line-height: var(--line-height-20);
color: var(--l2-foreground);
white-space: nowrap;
cursor: pointer;
user-select: none;
}
}
.permissionEditorModeInput {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
* {
display: none;
}
&[data-state='checked'] + label {
background: var(--l3-background);
color: var(--l1-foreground);
font-weight: var(--font-weight-medium);
}
}
.permissionEditorContent {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
.permissionEditorResourceList {
display: flex;
flex-direction: column;
gap: var(--spacing-6);
padding-bottom: var(--spacing-8);
}
.permissionEditorCollapsedSection {
display: flex;
flex-direction: column;
}
.permissionEditorCollapsedHeader {
display: flex;
align-items: center;
gap: var(--spacing-4);
width: 100%;
padding: var(--spacing-4) var(--spacing-6);
background: var(--l3-background);
border: 1px dashed var(--l2-border);
border-radius: 4px;
cursor: pointer;
transition: background 0.15s ease;
text-align: left;
&:hover {
background: var(--l2-background);
}
&:focus {
outline: 2px solid var(--primary);
outline-offset: -2px;
}
}
.permissionEditorCollapsedLabel {
font-family: Inter;
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-normal);
line-height: var(--line-height-20);
color: var(--l2-foreground);
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.permissionEditorCollapsedCount {
font-family: Inter;
font-size: var(--periscope-font-size-base);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-20);
color: var(--primary);
flex-shrink: 0;
}
.permissionEditorCollapseAction {
display: flex;
justify-content: flex-start;
padding-bottom: var(--spacing-4);
}

View File

@@ -0,0 +1,252 @@
import { useCallback, useRef, useState } from 'react';
import { SolidAlertTriangle } from '@signozhq/icons';
import { Button, ButtonGroup } from '@signozhq/ui/button';
import { ConfirmDialog } from '@signozhq/ui/dialog';
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
import { Typography } from '@signozhq/ui/typography';
import { Skeleton } from 'antd';
import type { AuthZResource, AuthZVerb } from 'hooks/useAuthZ/types';
import { PermissionScope, ResourcePermissions } from '../../types';
import type { EditorMode, JsonEditorRef } from './JsonEditor.types';
import JsonEditor from './JsonEditor';
import ResourceCard from './ResourceCard';
import styles from './PermissionEditor.module.scss';
interface PermissionEditorProps {
resources: ResourcePermissions[];
mode: EditorMode;
onModeChange: (mode: EditorMode) => void;
onResourceChange: (resources: ResourcePermissions[]) => void;
onJsonValidityChange?: (hasError: boolean) => void;
isLoading?: boolean;
validationErrors?: Set<string>;
}
function PermissionEditor({
resources,
mode,
onModeChange,
onResourceChange,
onJsonValidityChange,
isLoading = false,
validationErrors,
}: PermissionEditorProps): JSX.Element {
const jsonEditorRef = useRef<JsonEditorRef>(null);
const handleJsonValidityChange = useCallback(
(hasError: boolean): void => {
onJsonValidityChange?.(mode === 'json' && hasError);
},
[mode, onJsonValidityChange],
);
const [showDiscardConfirm, setShowDiscardConfirm] = useState(false);
const [expandedResources, setExpandedResources] = useState<Set<string>>(
new Set(),
);
const handleExpandAll = useCallback((): void => {
setExpandedResources(new Set(resources.map((r) => r.resourceId)));
}, [resources]);
const handleCollapseAll = useCallback((): void => {
setExpandedResources(new Set());
}, []);
const handleExpandChange = useCallback(
(resourceId: string) =>
(expanded: boolean): void => {
setExpandedResources((prev) => {
const next = new Set(prev);
if (expanded) {
next.add(resourceId);
} else {
next.delete(resourceId);
}
return next;
});
},
[],
);
const handleActionChange = useCallback(
(
resourceId: AuthZResource,
action: AuthZVerb,
scope: PermissionScope,
selectedIds: string[],
): void => {
const updatedResources = resources.map((r) => {
if (r.resourceId !== resourceId) {
return r;
}
return {
...r,
actions: {
...r.actions,
[action]: {
scope: scope,
selectedIds,
},
},
};
});
onResourceChange(updatedResources);
},
[resources, onResourceChange],
);
const handleJsonChange = useCallback(
(updatedResources: ResourcePermissions[]): void => {
onResourceChange(updatedResources);
},
[onResourceChange],
);
const handleModeChange = useCallback(
(value: string): void => {
const newMode = value as EditorMode;
if (
newMode === 'interactive' &&
mode === 'json' &&
jsonEditorRef.current?.hasParseError()
) {
setShowDiscardConfirm(true);
return;
}
if (newMode === 'interactive') {
onJsonValidityChange?.(false);
}
onModeChange(newMode);
},
[mode, onModeChange, onJsonValidityChange],
);
const handleDiscardConfirm = useCallback(async (): Promise<boolean> => {
onJsonValidityChange?.(false);
onModeChange('interactive');
setShowDiscardConfirm(false);
return true;
}, [onModeChange, onJsonValidityChange]);
const handleDiscardCancel = useCallback((): void => {
setShowDiscardConfirm(false);
}, []);
if (isLoading) {
return (
<div className={styles.permissionEditor}>
<Skeleton active paragraph={{ rows: 6 }} />
</div>
);
}
return (
<div className={styles.permissionEditor} data-testid="permission-editor">
<div className={styles.permissionEditorHeader}>
<Typography
as="span"
size="small"
weight="medium"
color="muted"
className={styles.permissionEditorTitle}
>
Transaction Groups
</Typography>
<hr className={styles.permissionEditorDivider} />
<RadioGroup
className={styles.permissionEditorModeToggle}
value={mode}
onChange={handleModeChange}
testId="permission-editor-mode"
>
<RadioGroupItem
value="interactive"
containerClassName={styles.permissionEditorModeItem}
className={styles.permissionEditorModeInput}
testId="permission-editor-mode-interactive"
>
Interactive
</RadioGroupItem>
<RadioGroupItem
value="json"
containerClassName={styles.permissionEditorModeItem}
className={styles.permissionEditorModeInput}
testId="permission-editor-mode-json"
>
JSON
</RadioGroupItem>
</RadioGroup>
</div>
<div className={styles.permissionEditorContent}>
{mode === 'interactive' ? (
<>
<div className={styles.permissionEditorCollapseAction}>
<ButtonGroup
variant="outlined"
color="secondary"
size="sm"
testId="toggle-all-group"
>
<Button onClick={handleExpandAll} data-testid="expand-all-button">
Expand all
</Button>
<Button onClick={handleCollapseAll} data-testid="collapse-all-button">
Collapse all
</Button>
</ButtonGroup>
</div>
<div className={styles.permissionEditorResourceList}>
{resources.map((resource) => (
<ResourceCard
key={resource.resourceId}
resource={resource}
onActionChange={handleActionChange}
isExpanded={expandedResources.has(resource.resourceId)}
onExpandChange={handleExpandChange(resource.resourceId)}
validationErrors={validationErrors}
/>
))}
</div>
</>
) : (
<JsonEditor
ref={jsonEditorRef}
resources={resources}
mode={mode}
onChange={handleJsonChange}
onValidityChange={handleJsonValidityChange}
/>
)}
</div>
<ConfirmDialog
open={showDiscardConfirm}
onOpenChange={(next): void => {
if (!next) {
handleDiscardCancel();
}
}}
title="Discard JSON changes?"
titleIcon={<SolidAlertTriangle size={14} color="#fdd600" />}
confirmText="Discard"
confirmColor="destructive"
cancelText="Stay in JSON"
onConfirm={handleDiscardConfirm}
onCancel={handleDiscardCancel}
>
<Typography>
The JSON contains errors and cannot be parsed. Switching to Interactive
mode will discard your changes.
</Typography>
</ConfirmDialog>
</div>
);
}
export default PermissionEditor;

View File

@@ -0,0 +1,64 @@
.resourceCard {
display: flex;
flex-direction: column;
border: 1px solid var(--l2-border);
border-radius: 4px;
overflow: hidden;
background: var(--l2-background);
transition: border-color 0.15s ease;
}
.resourceCardHeader {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: var(--spacing-6) var(--spacing-8);
border: none;
background: transparent;
cursor: pointer;
transition: background 0.15s ease;
text-align: left;
&:hover {
background: var(--l1-background-hover);
}
&:focus {
outline: 2px solid var(--primary);
outline-offset: -2px;
}
}
.resourceCardHeaderLeft {
display: flex;
align-items: center;
gap: var(--spacing-4);
}
.resourceCardChevron {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
color: var(--l2-foreground);
flex-shrink: 0;
}
.resourceCardHeaderRight {
display: flex;
align-items: center;
}
.resourceCardBody {
display: flex;
flex-direction: column;
&::before {
content: '';
height: 1px;
margin: 0 var(--spacing-8);
background: var(--l2-border);
}
}

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