Compare commits

..

60 Commits

Author SHA1 Message Date
Naman Verma
dadcbfdb6d chore: shorten comments 2026-07-09 20:30:05 +05:30
Naman Verma
f8514482fa chore: move free text compilation to visitor 2026-07-09 20:09:25 +05:30
Naman Verma
c629cc40dc chore: support non-dsl search in dashboards v2 list apis 2026-07-09 19:13:38 +05:30
Nikhil Mantri
da250bfd3e fix: commented out container metrics to surface k8s.pod.start_time (#12042) 2026-07-09 12:09:53 +00:00
Nikhil Mantri
67007e2902 feat(infra-monitoring): Kubernetes Container Monitoring List API (#11946)
* chore: added types for containers

* chore: added querier query and constants

* chore: added helper queries

* chore: added wiring

* chore: added open api spec

* chore: endpoint and variable rename

* chore: use recency for container status.reason as well

* chore: pass orgID to getMetadata for containers (adapt to rebased main)

* chore: add metrics to the containers list for metadata and earliest time

* chore: corrected metrics list for metadata lookup

* chore: added changes to the checks API for new kube containers section

* chore: containers query modified

* chore: added integration tests

* chore: integration tests

* chore: goroutines in container monitoring

* chore: constants deduplication

* chore: inlined requests.Post call for this PR instead of wrapping in a function
2026-07-09 11:42:24 +00:00
Ashwin Bhatkal
ff3ec133b4 fix(dashboard-v2): stop false 'updated elsewhere' prompts on your own edits (#12035)
Some checks failed
build-staging / staging (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix(dashboard-v2): close the 'updated elsewhere' dialog on reload

Reload set dismissedAt to null and relied on the refetched loaded copy's updatedAt
converging with the freshness query's serverUpdatedAt to hide the prompt. Those are two
separate queries and the convergence is racy, so the dialog often stayed open across
repeated reloads until Dismiss was clicked. Make Reload acknowledge the current version
(like Dismiss) and then refetch, so it closes on click; a genuinely newer server version
still re-prompts.

* fix(dashboard-v2): only prompt when the server copy is strictly newer

The stale-check compared the loaded and freshness copies of updatedAt with !==. Your own
edits advance the loaded copy immediately (the optimistic patch writes the PATCH response's
new updatedAt into the render cache), while the freshness query only re-fetches on window
focus. So right after creating a panel, renaming, or editing a variable, loaded jumped ahead
while freshness lagged, and !== flagged your own change as 'updated elsewhere' — a false
prompt that persisted across in-app navigation until a real window-focus refetch.

Compare with a strictly-newer (>) check so the prompt only fires when the server has a
version we have not loaded. A genuine external change still trips it; our own edits (loaded
ahead of the lagging freshness copy) no longer do.

* fix(dashboard-v2): probe freshness on visibilitychange, not refetchOnWindowFocus

The freshness check used a separate react-query query with initialData + refetchOnMount:false
so the first load made no extra request. But a query that only holds seeded data and never
fetched isn't reliably eligible for react-query's focus refetch, so on tab return no request
fired and external changes went undetected.

Drop the separate query and probe the server's updatedAt explicitly on 'visibilitychange'
(guarded to the visible transition): zero requests on first load, exactly one per tab return,
no dependence on react-query focus heuristics or staleTime bookkeeping. Comparison and
reload/dismiss behaviour are unchanged (strictly-newer; reload = dismiss + refetch).

* fix(dashboard-v2): probe on window focus too, and gate stale prompt on spec content

Two fixes to the freshness probe:

- Also listen to window 'focus', not just document 'visibilitychange'. Switching to
  another app (e.g. the editor) and back keeps the browser tab 'visible', so
  visibilitychange never fires and no probe ran — the reported 'no call when coming
  back to the page'. focus covers that; visibilitychange still covers tab switches.

- Prompt only when the server's spec actually differs (deep-equal), gated by
  strictly-newer. updatedAt is bumped by metadata-only writes like lock/unlock that
  don't change what the viewer sees; keying on spec makes those a non-event without
  needing the (void) lock/unlock call to return the new updatedAt. Hook now takes the
  loaded dashboard so it has both updatedAt and spec to compare.

* docs(dashboard-v2): clarify stale-check probes on both tab switch and app focus

* docs(dashboard-v2): trim useDashboardStaleCheck comments

* fix(dashboard-v2): detect external tag and lock changes too

Widen the stale-check content comparison from spec-only to spec + tags + lock state, so an
external tag edit or lock/unlock by another user surfaces the reload prompt. Own actions still
don't trip it: the render cache advances with them (optimistic patch / cache sync), so loaded
matches server.
2026-07-09 04:32:00 +00:00
Ashwin Bhatkal
0ea5399c15 feat(dashboard-v2): bug bash fixes II (list, dashboard page, create modal) (#12040)
* fix(dashboard-v2): source Home recent dashboards from the v2 list under the flag

* fix(dashboard-v2): keep same-key:value tags from collapsing to one word in the list

* fix(dashboard-v2): commit the inline dashboard-title edit on outside click

* fix(dashboard-v2): cap variable multi-select pills so the bar doesn't overflow

* fix(dashboard-v2): persist the variables-bar expand state and show "Less" when it loads expanded

* feat(dashboard-v2): reusable dashboard image picker for details + create modal

Extract the icon/image picker into a shared DashboardImagePicker used by both the dashboard settings form and the create-dashboard modal. Patch `/image` with add (not replace) when the field is absent, and lift the inline dropdown above the create-modal form fields.

* feat(dashboard-v2): simplify the create-modal templates tab to browse-and-request

Drop the placeholder gallery (search + category list + preview) for a clean, centered browse-and-request block: link out to the published template library, and let cloud users request a template we haven't built yet.

* fix(dashboard-v2): contain Escape/Enter and pass Cmd+Enter through the tag editor

Escape/Enter no longer bubble to close the host drawer/modal; Cmd/Ctrl+Enter passes through so a host form can submit on it (plain Enter still adds/commits a tag).

* feat(dashboard-v2): add/edit a dashboard's tags from the list

Add an "Add Tags"/"Edit Tags" kebab action (labelled by whether the dashboard has tags) opening the shared key:value editor, saving via an `add /tags` patch, with Cmd/Ctrl+Enter save. Gated on edit permission, disabled while locked. Also guard row navigation so clicks inside portaled overlays (menu/modals) don't open the dashboard.
2026-07-09 04:27:43 +00:00
Abhi kumar
f7aa3cee9c fix(dashboard-v2): view-modal, pie chart, and panel fixes (#12033)
* fix(dashboard-v2): tint chart-legend checkbox with its series color

The @signozhq/ui Checkbox declares --checkbox-checked-background on the
element itself via its data-color rule, so a value set on an ancestor was
shadowed and the per-series tint never applied. Feed the color through a
private --series-color var and override the vars on the element with a
selector that out-specifies the library rule.

* fix(dashboard-v2): make antd popups usable inside the view panel modal

The editor's antd Selects/pickers portal to document.body, outside the
modal Radix dialog, where Radix disables pointer events and traps focus, so
their menus opened but couldn't be clicked. Wrap the modal body in an antd
ConfigProvider whose getPopupContainer points at the dialog content, so all
antd popups render inside the interactive, focus-trapped layer.

* feat(dashboard-v2): add the list columns editor and pagination loader to the view modal

Surface the List panel's columns editor in the View modal (as in the full
editor) by exposing setSpec from useViewPanelMode and wiring it into the
query-builder footer. Its add-column combobox portals to body, so inside
the modal it opened behind the dialog — lift its z-index above the
query-builder popups so it stacks on top.

Also thread isPreviousData into PreviewPane so the List panel's page-change
loader shows during pagination, matching the full editor and dashboard.

* fix(dashboard-v2): measure chart dimensions before first paint

useResizeObserver seeded its size to 0 and only reported real dimensions
after paint via its debounced observer, so charts rendered once at 0 then
jumped — most visibly the pie legend, which rendered left-aligned then
re-centered once it learned it fit on one row. Add a useLayoutEffect that
measures the element synchronously before paint. In jsdom clientWidth is 0,
so tests are unaffected.

* feat(dashboard-v2): support per-slice color overrides for pie charts

Enable the legend colors control for pie charts. The control is fed by
useLegendSeries, which only handled time-series data, so pie returned no
series. Resolve pie legend series from the same scalar slices the renderer
draws (without overrides, for default colors) so the color keys match what
the chart looks up.

Extract the series resolution into utils/legendSeries.ts (a shared dedupe
primitive plus a pie and a time-series resolver) and reduce the hook to a
kind switch that returns none for kinds without a colors control.

* feat(dashboard-v2): offer dashboard root and every section in the move menu

Rework the "Move to section" submenu to list the dashboard root plus every
titled section (minus the panel's own), instead of only titled sections with
a separate "move out of section" entry. Hide it entirely when there is
nowhere to move.

* fix(dashboard-v2): hide the empty panel header in editor preview

When a panel has no title, description, or status and the actions menu is
suppressed (editor preview), render nothing instead of an empty header bar.
Add a min-height so the header keeps a stable size when it does render.

* fix(dashboard-v2): lazy-load panels per-panel instead of per-section

Move the viewport intersection observer down from Section to a new
SectionGridItem wrapper so each panel gates its own fetch. A board with
many panels now only queries the ones actually on screen, instead of
loading every panel in any section that scrolls into view.

* fix(dashboard-v2): hide "Extend time range" for panels with a fixed time preference

A panel locked to a non-global timePreference queries a fixed relative span
anchored to the dashboard end, so widening the ambient window can't surface
data for it — the "Extend time range" CTA was a no-op. NoData now takes the
panel and, via panelHasFixedTimePreference, drops the global extender for such
panels (only Retry remains). The View modal's local extender still wins.

* refactor(dashboard-v2): read panel time preference via shared helper

Panel.tsx read visualization.timePreference through an inline cast; replace it
with getPanelTimePreference so the header pill and NoData's extend gate share
one reader.

* feat(dashboard-v2): support per-series legend colors for bar and histogram panels

Route BarChartPanel and HistogramPanel through the time-series legend path in
useLegendSeries and enable the Legend `colors` control in both kinds' sections,
extending the per-series color overrides to these kinds.

* feat(dashboard-v2): pin the query-type tabs to the top of the editor scroll area

Make the query-type tab nav + Run Query button sticky so they stay visible
while the query body scrolls underneath; move the top padding onto the nav so
it keeps its spacing at rest and when pinned.
2026-07-09 04:23:37 +00:00
Tushar Vats
cd8bf27da7 chore: refactor qb to lossen tight coupling of querybuilder with other modules (#11911)
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-07-08 18:07:03 +00:00
Vinicius Lourenço
dde4a3abc7 feat(infra-monitoring): add v2 initial files (#12034)
* feat(infra-monitoring-v2): duplicate current infra monitoring code

* feat(infra-monitoring-v2): enable v2 via feature flag
2026-07-08 18:05:35 +00:00
Nikhil Mantri
1d441b63cf chore: added infraMonitoringV2 feature flag (#12031) 2026-07-08 16:59:08 +00:00
Aditya Singh
f26c8adb3b feat(trace-details): Span Details overhaul — width-driven layout, percentile & shared metadata (#11914)
* feat(trace-details): full-width span percentile panel with visible borders

* refactor(trace-details): give TraceIdField its own CSS module

* fix(trace-details): single borders for docked span details

* feat(trace-details): rework span details panel

Extract the summary into a SpanSummary component, place it by panel width
(above the tabs when narrow, inside Overview when wide), single-scroll sticky
tabs, and fix service/status-message badge overflow + truncation.

* feat(trace-details): share metadata row across header and span summary

* test: update test cases
2026-07-08 15:11:34 +00:00
Vinicius Lourenço
f5a395f229 fix(infra-monitoring-details): remove process/containers tab on hosts & better color for http status on traces (#12019)
* refactor(infra-monitoring): remove tabs for containers/processes from hosts

* fix(traces): parse http code to int instead of just render sakura color

* test(entity-details): add tests for entity traces

* fix(pr): address comments
2026-07-08 14:40:26 +00:00
Gaurav Tewari
b1dcbe87fe feat(llm-attribute-mapping): foundation — route, permission, page shell [1/5] (#11778)
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(llm-pricing): add model pricing foundation (route, permission, page shell)

* feat(llm-pricing): add listing page and table

* chore(llm-pricing): drop search + source filters from list request

The list API does not honour the q (search) and source params yet, so
the controls did nothing. Remove the search input and source dropdown
along with the params we sent, and trim useModelPricingFilters to the
URL-backed page state that pagination still needs. Currency dropdown,
tabs, table and pagination are unchanged. Filters will return once the
backend supports them.

* refactor(llm-pricing): extract getRelativeTime helper in utils

Pull the relative-time formatting out of getRelativeLastSeen into a
small local getRelativeTime helper. Kept feature-local (not in the
shared utils/timeUtils) so the LLM pricing module owns its own dayjs
config; the local relativeTime extend stays for test self-sufficiency.

* refactor(llm-pricing): drop dead NaN guard in formatPricePerMillion

Pricing fields are typed as required numbers and JSON can't carry NaN,
so Number.isNaN was unreachable. Keep the null/undefined guard as API
defensiveness (toFixed on a missing value would crash the row). Also
trims the now-redundant dayjs.extend comment.

* refactor(llm-pricing): centralize constants and shared types

Extract PAGE_SIZE, PAGE_KEY, COLUMN_COUNT and CURRENCY_OPTIONS into a
new constants.ts, and move the ModelPricingFilters contract into
types.ts. Component prop interfaces stay colocated with their
components, matching the convention in the drawer PR.

* refactor(llm-pricing): use nuqs for list pagination URL state

Replace the hand-rolled useHistory + URLSearchParams plumbing in
useModelPricingFilters with nuqs useQueryState, matching the convention
used by the dashboards, alerts and k8s list pages. Behaviour is
unchanged: parseAsInteger.withDefault(1) keeps ?page=1 out of the URL
and history:'replace' avoids polluting the back-stack.

* refactor(llm-pricing): inline pagination, drop useModelPricingFilters

The hook had shrunk to a one-line nuqs wrapper after search/source were
removed, so inline the useQueryState call into the container and remove
the hook file plus the now-unused ModelPricingFilters type. When the
filters return (once the API honours them) they can move back into a
dedicated hook.

* feat(llm-pricing): disable currency selector (USD-only for now)

Only USD is priced today, so render the currency SelectSimple in a
disabled state pinned to USD. A disabled select can't fire onChange, so
the currency useState is dead — drop it (and the now-unused useState
import).

* refactor(llm-pricing): render model costs inside its tab + tab URL param

The listing was rendered outside the Tabs, so the tab was decorative.
Move all model-cost content (currency control, list query, table,
pagination, footer) into a ModelCostsTab component rendered as the
'Model costs' tab's children, and drive the active tab from a 'tab' URL
query param (nuqs). The container is now just the page shell. Unpriced
models stays a disabled placeholder for a later PR.

* style(llm-pricing): target @signozhq table slots, drop dead antd/leftover rules

The component uses @signozhq/ui Table/Tabs (Radix-based), not antd, so the
.ant-table-* and .ant-tabs-nav selectors never matched — the intended
uppercase/muted header styling wasn't applied. Retarget header/cell rules to
[data-slot='table-head'|'table-cell'] (no !important needed). Also remove dead
rules left over from the removed search/source/add UI (.filters-bar__search,
__source, __add, .page-header__actions) and the unused .source-badge--auto/
--override modifiers.

* fix(llm-pricing): constrain currency dropdown width, drop tab URL param

- Currency SelectSimple stretched to fill the filters bar; give it a fixed
  160px width (min-width couldn't cap the trigger).
- Model costs is the only enabled tab for now, so use Tabs defaultValue
  instead of a URL-backed param. Removes the nuqs tab state plus the now-unused
  TAB_KEYS/TAB_QUERY_KEY constants and TabKey type.

* chore: self review changes

* fix: add skeleton loading

* refactor: self review changes

* refactor: initial prop

* fix: update styling

* fix: add comments in utils

* feat(llm-pricing): add model cost drawer and wire into listing page

* fix(llm-pricing): restrict pricing management to admins

Align the frontend write gate with the backend, which protects the
LLM pricing create/update/delete endpoints with AdminAccess (admin
only). Previously manage_llm_pricing allowed EDITOR/AUTHOR, so those
roles saw the Add/Save affordances but their writes were rejected with
a 403. Also removes the AUTHOR entry, which could never reach the page
(the route gate excludes it).

* fix(llm-pricing): read-only drawer shows View title, hides source picker

Non-managers open the drawer in view mode (write APIs are Admin-only), so:
- the heading reads "View model cost" instead of "Edit model cost"
- the Source (auto vs. override) picker is hidden, since switching source is
  a manager-only action with nothing actionable for a viewer.

* refactor: form in edit / add modal

* chore: update color tokens

* fix: add error handling

* chore: update more self review changes

* chore: self review changes

* chore: self review changes

* fix: minor grammer thing

* fix: route thing

* refactor: migrate to css moduel

* refactor: migrate to css module

* refactor: migrate to css module

* refactor: migrate to tanstack table

* docs: clarify price precision comment

* chore: remove comment

* feat(llm-attribute-mapping): add attribute mapping foundation (route, permission, page shell)

* fix: css styling

* refactor: css module

* chore: remove comment

* fix: disable isDirty in case of llm pricing

* refactor: number

* feat: add search , dropdown and flag

* feat: feature flag on entire route and add mode costs tabs

* fix: add isFetchingFeatureFlags

* chore: update flag

* refactor: shell

* fix: add key to route

* feat: add flags

* chore: additional refactor

* chore: add commet in utis

* chore: self review changes

* refactor: types and other things

* refactor: types and other things

* chore: add disable on source id

* empty commit

* chore: empty commit

* fix: add demo side nav on sidenav

* chore: remove demo side nav

* refactor: update routes

* chore: remove usd selector for now

* fix: layout shift

* refactor: styles

* refactor: typography component

* refactor: more changes

* refactor: typograhy

* refactor(llm-pricing): break model-cost drawer into per-component files + tokens

Apply the CSS-module/component conventions to the drawer that came from
drawer-3:
- Move the drawer under ModelCostTabPanel/components/ModelCostDrawer/ to mirror
  the ModelCostsTable structure
- Split the single 395-LOC ModelCostDrawer.module.scss into per-component
  co-located modules; cross-component selectors live in shared.module.scss and
  are pulled in via CSS-modules `composes`
- shared.module.scss is a composes target (parsed as plain CSS), so it is kept
  flat with block comments — no SCSS nesting or // comments
- Use --text-vanilla-* (not --bg-vanilla-*) for text colors, matching the
  listing code

* refactor: more changes

* refactor: styling and components

* refactor: styling and components

* chore: add a tooltip on hover

* feat: add delete confirm modal

* fix: update title

* refactor: css variables

* refactor: use signoz button and minor css update

* chore: sync table

* chore: remove extra comment

* chore: use typograpgy test in table config

* fix: minior issues

* fix: llm pricing listing

* refactor: remove extra classes

* refactor: side nav changes

* fix: update missing styles

* chore: update edit and delete options

* chore: remove extra comment

* chore: revert env changes

* chore: add enable check

* chore: remove divider

* refactor: use delete confirm dialog

* chore: remove scss file

* feat: move ui to easily accessable tabs

* feat: update test cases

* chore: update text

* chore: self review changes

* chore: self review refactor

* chore: self review changes

* chore: remove worktree

* chore: revert env.ts

* chore: add attribute mapping foundation

* refactor: basic components

* chore: update env.ts

* chore: update tests

* chore: self review changes

* chore: update test cases

* chore: remove extra comments

* refactor(llm-pricing): share toast copy via constants

* chore: use constants

* chore: redclared constants

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-07-08 14:12:49 +00:00
Vinicius Lourenço
a36081b00c fix(query-builder): ensure contrast/border/colors are consistent (#11992)
* refactor(query-builder-v2): ensure query builder has consistent colors and allow customization

* fix(query-builder-v2-usages): ensure each place using query builder v2 has consistent colors

* fix(alert): revert l3 for v2 ui
2026-07-08 13:57:05 +00:00
Aditya Singh
48d2460c8e feat(trace-details): waterfall timeline crosshair and VQA styling fixes (#11926)
* fix(trace): remove double border at flamegraph/waterfall juncture

* fix(trace): square accordion header corners

* fix(trace-waterfall): scope span hover-card to the span-name column

* feat(trace): move missing-spans banner to header using Callout

* feat(trace): use Lucide chevrons for accordion expand icons

* fix(trace-waterfall): keep hovered rows at full opacity when dimmed

* fix(trace-waterfall): strengthen selected/hover highlight into one continuous band

* fix(trace-waterfall): soften row-action button gradient

* fix(trace-waterfall): extend span-name divider to the top of the container

* fix(trace): use neutral info icon for flamegraph sampling notice

* fix(trace): tighten timeline ruler ticks and labels

* feat(trace-waterfall): enable timeline crosshair aligned to padded ruler

* fix(trace): match crosshair badge unit to ruler ticks
2026-07-08 12:57:52 +00:00
Tushar Vats
14d9bbfce9 chore: refactor querier integration tests (#12006)
* test(querier): move querier tests into per-signal suites

Pure git renames into querier{logs,traces,metrics,scalar,common} + CI matrix;
monoliths are split in the next commit. Preserves history (git log --follow).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rt2Xi6TdBDBAc3mhYZAb9q

* test(querier): split querier monoliths into theme files

Split 01_logs/04_traces/03_metrics/06_order_by_table into theme files
(functions moved verbatim, 249 tests unchanged); scalar helper -> fixtures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rt2Xi6TdBDBAc3mhYZAb9q

* test(querier): add coverage for filter/function/aggregation gaps

hasToken + has/hasToken misuse errors, ILIKE/NOT LIKE/NOT CONTAINS,
key:number, key-not-found (logs+traces), min/max, HAVING, metric reduceTo.
hasAny/hasAll over body arrays are marked xfail: they 500 in legacy body
mode (ClickHouse type error) and work only with use_json_body on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rt2Xi6TdBDBAc3mhYZAb9q

* test(querier_json_body): add hasAny/hasAll positive coverage

Body-JSON array functions hasAny/hasAll (string + numeric arrays) succeed in
JSON-body mode (body_v2 -> ClickHouse Array via dynamicElement), complementing
the legacy-mode xfail in querierlogs/09. Verified on ClickHouse 25.12.5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rt2Xi6TdBDBAc3mhYZAb9q

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 11:09:37 +00:00
Ashwin Bhatkal
9c48d17fed fix(dashboards): render logs/traces list panels on public dashboards (#11937)
* fix(dashboards): render logs/traces list panels on public dashboards

Public list panels rendered nothing even though the query returned data:
ListPanelWrapper bails with an empty fragment when setRequestData is
undefined, and the public Panel never forwarded one.

Back requestData with state and forward the setter to WidgetGraphComponent,
mirroring the authenticated GridCard. The per-panel request builder is
extracted to utils.ts.

Fixes SigNoz/engineering-pod#3646

* fix(dashboards): hide pagination controls on public list panels

Public widget data is fetched by index and the payload redacts each
widget's limit, so LogsPanelComponent/TracesTableComponent always showed a
pager that can't page (the endpoint takes no pagination params). Thread an
optional hidePagination flag from the public Panel through
WidgetGraphComponent -> PanelWrapper -> ListPanelWrapper to both list
components; default off, so authenticated dashboards are unchanged.

* fix(dashboards): guard against missing orderBy in useLogsData

Public dashboards redact the widget query (orderBy/filter/limit are
stripped), so a LIST query can reach useLogsData with no orderBy. The
optional chain guarded listQuery but not orderBy, so listQuery?.orderBy.find
threw once the public logs panel started rendering. Guard orderBy with ?..
2026-07-08 09:23:00 +00:00
Ashwin Bhatkal
33c6cbecda feat(dashboard-v2): variables table redesign + apply dynamic variables to panels (#12009)
* feat(dashboard-v2): patch builders to apply/sync a dynamic variable's filter across panels

* feat(dashboard-v2): two-column variables table with dynamic apply-to-panels

* fix(dashboard-v2): normalize dynamic variable signal so the source field always shows

* fix(dashboard-v2): keep field-key options during search refetch (smooth typing)

* fix(dashboard-v2): center and show the default-value clear icon

* fix(dashboard-v2): keep the add-variable + on one line when the bar is collapsed

* fix(dashboard-v2): variable selection flow correctness (#12013)

* fix(dashboard-v2): correct "ALL" variable selection end-to-end

- Materialize a query/custom ALL selection to the full option array so it reaches
  the query payload (was stored as value:null → dropped); tracks option growth.
- Default a multi-select allow-ALL variable (and the __ALL__ sentinel) to ALL,
  matching V1.
- ValueSelector hands CustomMultiSelect the expanded option set when all-selected,
  so it no longer renders a literal __ALL__ row.
- Keep a still-valid multi-select subset when options re-scope (was collapsing to
  the first option).
- Fix the dead configuredDefault fallback (read defaultValue as string|string[]).

* fix(dashboard-v2): variable selector robustness

- Surface option-fetch errors with a retry in the variable bar (Query/Dynamic) —
  a first-fetch failure is no longer a silent, stuck-empty selector.
- Custom variables now auto-select their default/first option (via a CustomSelector
  that runs useAutoSelect) instead of rendering blank.

* fix(dashboard-v2): variable fetch-engine correctness

- Ignore a stale/late fetch settle for a variable no longer actively fetching
  (restores V1's active-state guard).
- Unblock a waiting query child only once ALL its parents are settled (diamond
  dependencies no longer fetch against a stale parent).
- A value change no longer refetches the changed variable's own options, and only
  a dynamic change refreshes the other dynamics (query/custom/text changes don't
  touch dynamics — they don't depend on those values).
- Enqueue cycle-dropped query variables as best-effort roots instead of leaving
  them (and any waiting dynamics) silently stuck.

* fix(dashboard-v2): variable selection edge cases

- Read the __ALL__ URL sentinel as "ALL" only for variables that support it, so a
  literal "__ALL__" value (e.g. a text variable) isn't misread.
- Prune URL selections for variables that no longer exist (rename/remove), so a
  shared link can't leak a stale value into a later variable.
- Bound option-query cache churn with a cacheTime on the query/dynamic fetches.
2026-07-08 09:14:55 +00:00
Ashwin Bhatkal
9429c9e632 feat(dashboard-v2): show empty sections & clone a section with its panels (#12023)
* fix(dashboard-v2): render a newly-created empty titled section instead of the dashboard empty state

* feat(dashboard-v2): clone a section with all its panels
2026-07-08 07:21:40 +00:00
Ashwin Bhatkal
7821961a6f feat(dashboard-v2): detect external dashboard changes on tab focus and prompt to reload (#12022) 2026-07-08 07:21:08 +00:00
Abhi kumar
08de39cdbd fix(dashboard-v2): panel drilldown fixes + New Panel modal one-click create (#12026)
* fix(dashboard-v2): disable panel drilldown for non-builder queries

Drilldown was armed whenever the panel had at least one builder query,
even for PromQL / ClickHouse panels (a mixed CompositeQuery, or the
QUERY_BUILDER fallback of deriveQueryType). Those modes carry no builder
context to refine, so the click produced an empty/invalid drilldown.

Gate enableDrillDown on getPanelQueryType(panel) === QUERY_BUILDER in
addition to the existing capability + builder-query-count checks.

* fix(dashboard-v2): hide "Click to drilldown" hint when drilldown is disarmed

TooltipFooter's canDrilldown defaults to true, and the time-series / bar
renderers never passed it — so the hint showed even in the panel editor
preview (where drilldown is off) and for non-builder query types. Passing
the prop as-is wasn't enough: enableDrillDown is undefined in edit mode, and
canDrilldown={undefined} falls back to the true default.

Pass canDrilldown={!!enableDrillDown} (coerced) and add enableDrillDown to
the useCallback deps (also fixes a stale-closure in the bar renderer). The
hint now shows only when a click will actually open the drilldown menu.

* fix(dashboard-v2): make pie panel drilldown fire

preparePieData built slices with only label/value/color, never the
queryName and labels that enrichPieClick needs. Every slice therefore had
queryName === undefined, so enrichPieClick hit isValidQueryName('') and
returned null — the slice click was a silent no-op and pie drilldown never
worked.

Carry the value column's queryName and the row's group-by key→value labels
(keyed by field name) onto each slice, mirroring how enrichTableClick reads
them off the shared PanelTable shape. Adds regression tests for both.

* fix(dashboard-v2): one-click panel create when the dashboard has one section

The New Panel modal always used a select-then-confirm flow with a footer
section picker. When the dashboard has a single section there is nothing to
pick, so the footer degraded to a lone confirm button and the extra click
was pointless.

Hide the footer entirely for the single-section case and create the panel
directly on tile click; keep select-then-confirm when multiple sections
exist. The section-picker guard moves up to the modal so the footer renders
only when a picker is actually needed.
2026-07-08 07:19:32 +00:00
dependabot[bot]
98178795ec chore(deps): bump golang.org/x/crypto from 0.49.0 to 0.52.0 (#12012)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.49.0 to 0.52.0.
- [Commits](https://github.com/golang/crypto/compare/v0.49.0...v0.52.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.52.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-08 06:42:13 +00:00
Ashwin Bhatkal
0797b26cb2 feat(dashboard-v2): variable editor form, add-variable UX & styling (#11942)
* fix(dashboard-v2): cap the variable preview-values box height

The 'Preview of Values' box only had a min-height, so a field with many values
grew the box tall. Add a max-height so it scrolls instead.

* feat(dashboard-v2): restore the V1 combined field/source row for dynamic variables

Replace the separate Source and Attribute rows with the single V1-style row:
the telemetry field on the left, then "from" and the source select on the right.

* feat(dashboard-v2): add-variable button in the variables bar

Add a dashed outlined 'Add variable' button on the left of the variables bar
(shown for editors even with no variables). It deep-links into the settings
drawer at the Variables tab with the add-variable form open, via a transient
settingsRequest store slice; the drawer destroys on close so it re-initializes
cleanly each open.

* feat(dashboard-v2): collapse the add-variable button once variables exist

Full labelled button when there are no variables; once some exist it shrinks to
a + icon (label on hover) placed after the collapsed +N. Dashed l3 border.

* style(dashboard-v2): use the l3 border on toolbar buttons and the empty state

Give Actions/Configure/Edit-as-JSON and the dashboard empty-state dashed border
the more visible --l3-border.

* fix(dashboard-v2): keep add-variable button out of collapsed bar + flat toolbar borders

- Move the add-variable + icon inside the strip and only show it when the bar
  isn't collapsed (no overflow) or is expanded, so it never breaks the layout
  mid-wrap; expand to reveal it.
- Make the Actions/Configure/Edit-as-JSON border a flat l3 outline with no
  box-shadow/depth.

* feat(dashboard-v2): raise dynamic-variable attribute search debounce to 500ms

* feat(dashboard-v2): add a Configure step to the dashboard empty state

* fix(dashboard-v2): flow the variables bar under the time selector + reposition add button

Revert the bar wrapper to block so the strip wraps around the floated time
selector (one line beside it collapsed; full-width below it when expanded) — a
flex wrapper had made it a BFC that trapped the rows on the left. The add-variable
"+" now sits after the +N/Less trigger, kept inline and always mounted so the
overflow measurement no longer toggles it (which caused a layout shift), with the
collapse math reserving space for it.

* feat(dashboard-v2): hide dashboard chrome in full screen

Show only the sections and panels in full screen — the header and toolbar
(actions, variables bar, time selector) are hidden for a clean presentation
view; exit with Esc. Also drops a stray blank line that failed the format check.

* refactor(dashboard-v2): extract add-variable buttons into components
2026-07-08 06:37:40 +00:00
Vinicius Lourenço
f8846026fb fix(authz-devtools): drop global listener for modal keyboard (#12018)
Co-authored-by: Yunus M <myounis.ar@live.com>
2026-07-08 06:31:25 +00:00
dependabot[bot]
5ec6560613 chore(deps): bump starlette from 1.0.0 to 1.3.1 in /tests (#11737)
Bumps [starlette](https://github.com/Kludex/starlette) from 1.0.0 to 1.3.1.
- [Release notes](https://github.com/Kludex/starlette/releases)
- [Changelog](https://github.com/Kludex/starlette/blob/main/docs/release-notes.md)
- [Commits](https://github.com/Kludex/starlette/compare/1.0.0...1.3.1)

---
updated-dependencies:
- dependency-name: starlette
  dependency-version: 1.3.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-08 05:30:13 +00:00
dependabot[bot]
46e092b2c9 chore(deps): bump cryptography from 46.0.7 to 48.0.1 in /tests (#11736)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / prepare (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
Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.7 to 48.0.1.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/46.0.7...48.0.1)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 48.0.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-08 05:24:02 +00:00
Ashwin Bhatkal
fe660e27a2 feat(dashboard-v2): gate list-page edit actions on edit permission (#12021) 2026-07-08 05:13:03 +00:00
Ashwin Bhatkal
465620fbbb feat(dashboard-v2): enforce read-only behavior when a dashboard is locked (#12017)
* feat(dashboard-v2): lock-aware edit context, mutation guard, tooltip primitives & locked indicator, skip chart refetch on lock toggle

* feat(dashboard-v2): hide toolbar edit actions (incl. clone) in view mode, disable with a hover reason when locked

* feat(dashboard-v2): hide panel & section edit actions in view mode, disable when locked

* feat(dashboard-v2): open the JSON editor read-only when not editable

* feat(dashboard-v2): disable panel editor save when not editable
2026-07-08 05:12:31 +00:00
Aditya Singh
db2e29dde9 feat(trace-details): Pretty View interaction polish (hover, search, actions, z-index) (#11940)
* feat(trace-details): restyle Pretty View attributes search bar

* feat(trace-details): full-width row hover in Pretty View

* feat(trace-details): keep Pretty View row active and brighten value on hover/menu-open

* feat(trace-details): move parent-row ellipsis to the right edge

* fix(cmdk): raise command palette above the floating span-details panel

* feat: use toggle group for data viewer instead of dropdown

* fix(trace-details): disable Monaco sticky scroll in JSON view to stop overlap
2026-07-08 04:55:24 +00:00
Ashwin Bhatkal
cec72e25fd feat(dashboard-v2): link variables to panels (#12003)
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): scope panel refetch to referenced variables

A panel now refetches only when a variable its queries reference changes (the cache key is scoped to referenced variables; the full payload is still substituted), and stays in its loading state on first load until those variables resolve.

* feat(dashboard-v2): surface dashboard variables in query builder suggestions

Publish the dashboard's variables into the shared store the query builder autocomplete reads, so $variable suggestions appear in the dashboards-page builder and the panel editor.
2026-07-07 20:01:15 +00:00
Abhi kumar
82c833044d feat(dashboard-v2): extend time window from empty panel state (#11999)
* feat(dashboard-v2): add extend-time-window logic for empty panels

Add the empty-state "extend the time window" building block: useExtendTimeWindow
widens the dashboard's global time via the shared zoom-out ladder (getNextZoomOutRange
/ useZoomOut), and extendWindow.ts exposes the ExtendTimeWindow type plus a thin
buildExtendWindow adapter over the ladder result.

* feat(dashboard-v2): add view-session store slice for the modal extender

The View modal owns a local time window; add a transient dashboard-store
slice so it can publish its extend-window behaviour for the deeply-nested
NoData empty state to read, instead of threading it through every renderer.

* feat(dashboard-v2): offer an extend-window action in the panel no-data state

A panel with no data in the selected window showed only a Retry, which can't
help when the query succeeded and there's just nothing in range. Add an
"Extend time range" action that widens the window (global time on the
grid/editor; the View modal's local window in place, read from the store),
shown alongside the retained Retry.

Also fix the stale empty state: keepPreviousData retained the empty response so
NoData lingered through a refetch then snapped to the chart — a refetch over
empty data now shows the shared PanelLoader instead.

PanelMessage gains a secondaryAction so the empty state can render both buttons;
refetch stays on the renderer contract to drive Retry (standalone call sites
like the editor preview may still omit it, showing Extend only).

* chore: pr review fixes

* chore: pr review changes

* chore: pr review changes
2026-07-07 19:06:38 +00:00
Ashwin Bhatkal
f98b8bb1fd feat(dashboard-v2): runtime variable fetch engine & selectors (#12002)
* feat(dashboard-v2): add runtime variable fetch-state engine

Port V1's variable fetch orchestration natively onto the dashboard store: a fetch-state slice (idle/loading/revalidating/waiting/error + per-variable cycle ids) plus the dependency context derivation (query/dynamic order) needed to schedule fetches. Query variables load in dependency order; dynamics wait for query values. enqueueFetchAll drives first load/time change, enqueueDescendants drives a single value change.

* feat(dashboard-v2): drive variable selectors off the fetch engine

Gate the Query/Dynamic option fetches on the engine's per-variable state and key them by cycle id (so a parent change refetches only its dependents, in order, with no duplicate calls), and report completion/failure back. Wire the selection lifecycle: full fetch cycle on load/time change, descendant cascade on value change.

* fix(dashboard-v2): read a variable's default value as string, not { value }

defaultValue is a string | string[] on the wire, but the editor and runtime read
it as { value }, so a saved default showed blank in the form (and couldn't be
cleared) and never auto-applied. Read it directly in all three sites.

* feat(dashboard-v2): show hidden variable values on the collapsed +N hover

When the variables bar collapses the overflow behind +N, hovering it now lists
the hidden variables and their current selected values.

* feat(dashboard-v2): batch initial variable auto-selections into one fetch

Auto-selection now flows through a dedicated onAutoSelect path, separate from
user changes. The initial load burst of fills (each selector picks a value as
its options resolve, at different times) is coalesced across one animation frame
into a single store write plus one enqueueDescendantsBatch, so dependent
variables re-fetch once with the settled parent values instead of once per fill.

* refactor(dashboard-v2): use enum for VariableFetchState
2026-07-07 19:01:32 +00:00
Ashwin Bhatkal
2114fbddfe feat(dashboard-v2): enforce name-length limits on dashboard inputs (#12015)
* feat(dashboard-v2): cap dashboard, section, panel and variable name inputs at 128 chars

* feat(dashboard-v2): cap saved-view names at 64 chars
2026-07-07 18:51:20 +00:00
Yunus M
b2214cf5a3 feat: implement resizable quick filters across multiple components with persistent width settings (#12014) 2026-07-07 18:10:39 +00:00
Abhi kumar
db4219e0f1 feat(dashboards-v2): download panel as PNG/SVG/CSV (#11803)
* chore(dashboards-v2): add html-to-image for panel image export

Dependency used to capture a panel's rendered DOM node as PNG/SVG.

* feat(dashboards-v2): declare per-kind download capability

Adds the DownloadFormat enum and a per-kind `actions.download` map. Tables allow CSV/PNG/SVG, charts PNG/SVG; CSV is table-only (V1 parity).

* feat(dashboards-v2): capture a panel as a PNG/SVG image

downloadElementAsImage captures the panel's rendered node via html-to-image (dropping the hover chrome); useDownloadPanelImage locates it by its data-panel-root marker and surfaces failures as a toast. toSafeFileName sanitizes the filename.

* feat(dashboards-v2): export table panel data as CSV

getTableCsvRows flattens the prepared scalar table (reusing the on-screen cell formatting) into rows, and downloadCsv serializes them via papaparse.

* feat(dashboards-v2): add the Download action to the panel menu

useDownloadPanelMenuItem composes the submenu, dispatching CSV to the query response (useDownloadPanelCsv) and PNG/SVG to the DOM capture; the response is threaded to the menu via props. buildDownloadMenuItem renders the format options; usePanelActionItems consumes the ready item.

* chore: pr review fixes
2026-07-07 16:23:22 +00:00
Gaurav Tewari
165c945511 feat(frontend): LLM pricing UI update and tests [5/6] (#11910)
* feat(llm-pricing): add model pricing foundation (route, permission, page shell)

* feat(llm-pricing): add listing page and table

* chore(llm-pricing): drop search + source filters from list request

The list API does not honour the q (search) and source params yet, so
the controls did nothing. Remove the search input and source dropdown
along with the params we sent, and trim useModelPricingFilters to the
URL-backed page state that pagination still needs. Currency dropdown,
tabs, table and pagination are unchanged. Filters will return once the
backend supports them.

* refactor(llm-pricing): extract getRelativeTime helper in utils

Pull the relative-time formatting out of getRelativeLastSeen into a
small local getRelativeTime helper. Kept feature-local (not in the
shared utils/timeUtils) so the LLM pricing module owns its own dayjs
config; the local relativeTime extend stays for test self-sufficiency.

* refactor(llm-pricing): drop dead NaN guard in formatPricePerMillion

Pricing fields are typed as required numbers and JSON can't carry NaN,
so Number.isNaN was unreachable. Keep the null/undefined guard as API
defensiveness (toFixed on a missing value would crash the row). Also
trims the now-redundant dayjs.extend comment.

* refactor(llm-pricing): centralize constants and shared types

Extract PAGE_SIZE, PAGE_KEY, COLUMN_COUNT and CURRENCY_OPTIONS into a
new constants.ts, and move the ModelPricingFilters contract into
types.ts. Component prop interfaces stay colocated with their
components, matching the convention in the drawer PR.

* refactor(llm-pricing): use nuqs for list pagination URL state

Replace the hand-rolled useHistory + URLSearchParams plumbing in
useModelPricingFilters with nuqs useQueryState, matching the convention
used by the dashboards, alerts and k8s list pages. Behaviour is
unchanged: parseAsInteger.withDefault(1) keeps ?page=1 out of the URL
and history:'replace' avoids polluting the back-stack.

* refactor(llm-pricing): inline pagination, drop useModelPricingFilters

The hook had shrunk to a one-line nuqs wrapper after search/source were
removed, so inline the useQueryState call into the container and remove
the hook file plus the now-unused ModelPricingFilters type. When the
filters return (once the API honours them) they can move back into a
dedicated hook.

* feat(llm-pricing): disable currency selector (USD-only for now)

Only USD is priced today, so render the currency SelectSimple in a
disabled state pinned to USD. A disabled select can't fire onChange, so
the currency useState is dead — drop it (and the now-unused useState
import).

* refactor(llm-pricing): render model costs inside its tab + tab URL param

The listing was rendered outside the Tabs, so the tab was decorative.
Move all model-cost content (currency control, list query, table,
pagination, footer) into a ModelCostsTab component rendered as the
'Model costs' tab's children, and drive the active tab from a 'tab' URL
query param (nuqs). The container is now just the page shell. Unpriced
models stays a disabled placeholder for a later PR.

* style(llm-pricing): target @signozhq table slots, drop dead antd/leftover rules

The component uses @signozhq/ui Table/Tabs (Radix-based), not antd, so the
.ant-table-* and .ant-tabs-nav selectors never matched — the intended
uppercase/muted header styling wasn't applied. Retarget header/cell rules to
[data-slot='table-head'|'table-cell'] (no !important needed). Also remove dead
rules left over from the removed search/source/add UI (.filters-bar__search,
__source, __add, .page-header__actions) and the unused .source-badge--auto/
--override modifiers.

* fix(llm-pricing): constrain currency dropdown width, drop tab URL param

- Currency SelectSimple stretched to fill the filters bar; give it a fixed
  160px width (min-width couldn't cap the trigger).
- Model costs is the only enabled tab for now, so use Tabs defaultValue
  instead of a URL-backed param. Removes the nuqs tab state plus the now-unused
  TAB_KEYS/TAB_QUERY_KEY constants and TabKey type.

* chore: self review changes

* fix: add skeleton loading

* refactor: self review changes

* refactor: initial prop

* fix: update styling

* fix: add comments in utils

* feat(llm-pricing): add model cost drawer and wire into listing page

* fix(llm-pricing): restrict pricing management to admins

Align the frontend write gate with the backend, which protects the
LLM pricing create/update/delete endpoints with AdminAccess (admin
only). Previously manage_llm_pricing allowed EDITOR/AUTHOR, so those
roles saw the Add/Save affordances but their writes were rejected with
a 403. Also removes the AUTHOR entry, which could never reach the page
(the route gate excludes it).

* fix(llm-pricing): read-only drawer shows View title, hides source picker

Non-managers open the drawer in view mode (write APIs are Admin-only), so:
- the heading reads "View model cost" instead of "Edit model cost"
- the Source (auto vs. override) picker is hidden, since switching source is
  a manager-only action with nothing actionable for a viewer.

* refactor: form in edit / add modal

* chore: update color tokens

* fix: add error handling

* chore: update more self review changes

* chore: self review changes

* chore: self review changes

* fix: minor grammer thing

* fix: route thing

* refactor: migrate to css moduel

* refactor: migrate to css module

* refactor: migrate to css module

* refactor: migrate to tanstack table

* docs: clarify price precision comment

* chore: remove comment

* chore: remove comment

* fix: disable isDirty in case of llm pricing

* refactor: number

* feat: add search , dropdown and flag

* feat: feature flag on entire route and add mode costs tabs

* fix: add isFetchingFeatureFlags

* chore: update flag

* refactor: shell

* fix: add key to route

* feat: add flags

* chore: additional refactor

* chore: add commet in utis

* chore: self review changes

* refactor: types and other things

* refactor: types and other things

* chore: add disable on source id

* empty commit

* chore: empty commit

* fix: add demo side nav on sidenav

* chore: remove demo side nav

* refactor: update routes

* chore: remove usd selector for now

* fix: layout shift

* refactor: styles

* refactor: typography component

* refactor: more changes

* refactor: typograhy

* refactor(llm-pricing): break model-cost drawer into per-component files + tokens

Apply the CSS-module/component conventions to the drawer that came from
drawer-3:
- Move the drawer under ModelCostTabPanel/components/ModelCostDrawer/ to mirror
  the ModelCostsTable structure
- Split the single 395-LOC ModelCostDrawer.module.scss into per-component
  co-located modules; cross-component selectors live in shared.module.scss and
  are pulled in via CSS-modules `composes`
- shared.module.scss is a composes target (parsed as plain CSS), so it is kept
  flat with block comments — no SCSS nesting or // comments
- Use --text-vanilla-* (not --bg-vanilla-*) for text colors, matching the
  listing code

* refactor: more changes

* refactor: styling and components

* refactor: styling and components

* chore: add a tooltip on hover

* feat: add delete confirm modal

* fix: update title

* refactor: css variables

* refactor: use signoz button and minor css update

* chore: sync table

* chore: remove extra comment

* chore: use typograpgy test in table config

* fix: minior issues

* fix: llm pricing listing

* refactor: remove extra classes

* refactor: side nav changes

* fix: update missing styles

* chore: update edit and delete options

* chore: remove extra comment

* chore: revert env changes

* chore: add enable check

* chore: remove divider

* refactor: use delete confirm dialog

* chore: remove scss file

* feat: move ui to easily accessable tabs

* feat: update test cases

* chore: update text

* chore: self review changes

* chore: self review refactor

* chore: self review changes

* chore: remove worktree

* chore: revert env.ts

* chore: update tests

* chore: self review changes

* chore: update test cases

* chore: remove extra comments

* chore: use constants

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-07-07 15:05:23 +00:00
Aditya Singh
ff19aedaa8 feat(trace-details): UI polish + reusable CopyButton (#11958)
* refactor(trace-details): use ArrowRightFromLine for filter collapse icon

* refactor(trace-details): square header back button with l1 border, tabular-nums trace ID

* refactor(trace-details): show events count as an l3 badge

* feat(periscope): add reusable CopyButton with copy-to-check animation

* refactor(trace-details): use CopyButton in DataViewer and filter query popover

* fix(traces): address PR review on CopyButton and events badge

- Remove unused onCopy prop from CopyButton; it was wired to fire
  unconditionally (even on failed clipboard writes), contradicting its
  documented "on successful copy" contract, and no caller used it. Will
  be re-added gated on success when actually needed.
- Only render the Events tab count badge when eventsCount > 0 so spans
  with no events don't show a "0" badge.

* style(traces): tokenize events badge margin per review

Use var(--spacing-3) for the events badge margin-left instead of a raw
6px. Leave the 18px badge box and 5px padding off-grid (no matching
token; nearest values would change the single-digit circle).
2026-07-07 15:01:58 +00:00
Abhi kumar
ef8319585b feat(dashboard-v2): View-modal drill-down + editor "Switch to View Mode" (#12004)
* fix(dashboard-v2): only mark a new panel savable once it has a query

A new panel was always treated as dirty, so Save was enabled even with no
query to run. Track dirtiness off spec/query edits and require a seeded query
(List auto-seeds one; other kinds open query-less) before a new panel is
savable.

* feat(dashboard-v2): drill down from the panel View modal

Wire the shared drill-down orchestration into the View modal so filter-by-value
and breakout refine the expanded view in place (URL-persisted, re-runs the
preview) instead of opening a nested View modal. Adds an OpenDrilldownView
handoff type, an optional openDrilldownView override on useDrilldown, and the
PreviewPane onClick/enableDrillDown pass-through the modal uses. Cells and log
rows get a pointer/hover affordance for the click.

* fix(dashboard-v2): keep the dashboard spec cache patch-driven

The spec cache is kept fresh by optimistic patches, so auto-refetching flashed
the grid back to server state as observers mounted across the panel tree. Set
staleTime: Infinity + refetchOnMount: false; explicit refetch() still works.

* feat(dashboard-v2): add "Switch to View Mode" to the panel editor

Add the V1 "Switch to View Mode" button to the panel editor header. It leaves
the full-page editor for the dashboard with this panel expanded in the View
modal, seeded with the live (un-saved) query via the same expandedWidgetId +
graphType + compositeQuery URL contract the modal hydrates from. Shown for
existing panels only — a new panel isn't saved to the dashboard spec yet.
2026-07-07 14:20:00 +00:00
Abhi kumar
14ba332935 fix: ui fixes for panel selection modal (#11944)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix: ui fixes for panel selection modal

* feat(dashboards-v2): choose target section when adding a panel

Add a section picker to the New Panel modal so a panel can be placed in
any section (or the untitled root), not just the one the "Add panel"
action was triggered from. Sections are read from the cached dashboard
query via useDashboardSections (no refetch, no prop-drilling).

Picking a panel type now selects it (highlighted card) and reveals a
footer holding the section dropdown and an "Add Panel" button split
50/50; confirming shows a brief loader before navigating to the editor.
The chosen section's layoutIndex is threaded through
useCreatePanel.createPanel.

* fix(dashboards-v2): refine New Panel modal footer + section labels

- Keep the footer visible at all times; the "Add Panel" confirm is disabled
  until a panel type is selected (instead of hiding the whole footer).
- Footer layout: the section selector fills the available width and the
  confirm button takes its natural width (no more 50/50 split).
- Add a "Select panel type" label above the panel-type grid, matching the
  existing "Add panel to" label.

* chore: pr review fixes
2026-07-07 12:28:15 +00:00
Vinicius Lourenço
fda7b66d49 feat(authz): add devtools for authz (#11933)
* feat(authz): add devtools for authz

* fix(authz): add missing allowed/deniedPermissions to test mocks

* fix(pnpm-lock): keep updated

* fix(pr): address comments

* fix(pr): use our button component

* fix(pr): split modal in smaller components
2026-07-07 12:11:05 +00:00
Srikanth Chekuri
de2212e8b9 chore(querier): address impl gap in max_concurrent_queries (#11985) 2026-07-07 08:18:20 +00:00
Yunus M
ff4f18985c Feat/quick filters resizable (#11998)
* feat: add resizable quick filters panel with persistent width settings

* feat: add tooltip to checkbox value display for better user experience

* feat: add grip handle to resizable box for improved user interaction
2026-07-07 08:13:55 +00:00
Ashwin Bhatkal
7d555b643a feat(dashboard-v2): linkify URLs in dashboard description tooltip (#11996)
* feat(dashboard-v2): linkify URLs in dashboard description tooltip

Parse http(s) and www. links in the dashboard description and render
them as clickable anchors opening in a new tab. Add a reusable
linkifyText util under src/utils. Preserve author line breaks and show
the tooltip below the info icon.

* feat(dashboard): linkify URLs in V1 dashboard description

Apply the linkifyText util to the V1 dashboard description section so
pasted URLs become clickable, matching the V2 behaviour. Preserve line
breaks and wrap long URLs.
2026-07-07 06:11:34 +00:00
Ashwin Bhatkal
7839e806fd fix(dashboard-v2): warn about orphaned panels & surface JSON save errors (#11989)
Some checks failed
build-staging / staging (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix(dashboard-v2): surface the real API error when a JSON save fails

updateDashboardV2 throws the raw generated error; it was passed to the error
modal cast as APIError, so a rejected save (e.g. a locked dashboard) rendered no
message. Run it through toAPIError so the actual error is shown.

* feat(dashboard-v2): warn when JSON edits desync panels and layouts

Editing the dashboard JSON can leave a panel in spec.panels that no layout
places (orphaned — renders nowhere), or a layout item referencing a panel that
no longer exists. Detect both from the draft (findPanelLayoutIssues, typed off
the spec DTO) and show a footer warning with a triangle icon; hovering lists the
exact panel ids. The tooltip sits above the drawer.

* fix(dashboard-v2): keep editor keystrokes from triggering global shortcuts

Stop keydown propagation out of the JSON drawer so Shift-selection and typing in
the editor don't fire app-level keyboard shortcuts.
2026-07-07 05:35:35 +00:00
Ashwin Bhatkal
2dcd51391d feat(dashboard-v2): rename saved views, org-wide creator filter & visible Locked-view query (#11982)
* feat(dashboard-v2): rename saved views via a shared 128-char name popover

Add a per-row rename action wired to renameView() (existing PUT endpoint), and
extract the save/rename name input into a shared ViewNamePopover capped at 128
chars (replaces SaveViewPopover). Group the rename/delete row actions so they no
longer overlap, patch the renamed view into the list cache inline instead of
refetching, and make the active-view 'Reset' restore the view's opening filters.

* feat(dashboard-v2): show locked = true in the query for the Locked view

The Locked built-in view applied its filter as an invisible server-only clause;
seed it into the search box instead so the constraint is visible and editable,
and drop the now-unused viewQuery plumbing.

* feat(dashboard-v2): source the created-by filter from the org user list

The creator filter only listed authors present on the loaded page, so a user who
hadn't authored a visible dashboard couldn't be selected. Source options from the
v2 List-users API via useCreatorOptions (page authors kept as a fallback until it
resolves), labelled with the user's display name.

* feat(dashboard-v2): show the unsaved-changes dot on the results header

Mirror the views rail's dirty indicator next to the active view name in the
results header when the current filters diverge from the view.
2026-07-07 04:42:20 +00:00
Ashwin Bhatkal
6251616d9d fix(dashboard-v2): add (not replace) description when it is empty (#11983)
A dashboard with no description has no /spec/display/description path, so a
JSON-Patch replace failed and the description update silently no-op-d. Use add
(create-or-replace) when the current description is empty.
2026-07-07 04:41:35 +00:00
Nikhil Mantri
652a9044f4 feat(infra-monitoring): goroutines wrapping custom queries for concurrency (#11981)
* chore: added types and open api spec changes

* chore: added method to calculate reason

* chore: per group pod status counts with req metric checks method added

* chore: wired up pod status counts

* chore: pod restarts type added

* chore: added restart counts for the group

* chore: bug in query fix

* chore: onboarding API changes

* chore: integration tests added

* chore: added podcountsbyphase in other entities

* chore: added pod status counts for other entities

* chore: added integration tests for other entities

* chore: added checks api changes for other entities

* chore: rearrangement

* chore: removed succeeded status and mark it as completed

* chore: query beautified

* chore: corrected metrics list for metadata lookup

* chore: removed dead constants

* chore: goroutines for ListHosts

* chore: goroutines for ListPods

* chore: goroutines for ListNodes

* chore: goroutines for ListNamespaces

* chore: goroutines for ListClusters

* chore: goroutines for ListDeployments

* chore: goroutines for ListStatefulsets

* chore: added goroutines for ListStatefulsets, ListJobs and ListDaemonsets
2026-07-07 04:39:08 +00:00
Nityananda Gohain
d4646657ca fix: initalize querier in llmpricing (#11993) 2026-07-07 04:04:29 +00:00
Nikhil Mantri
29681572db feat(infra-monitoring): pod status and restart counts. (#11885)
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / prepare (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: added types and open api spec changes

* chore: added method to calculate reason

* chore: per group pod status counts with req metric checks method added

* chore: wired up pod status counts

* chore: pod restarts type added

* chore: added restart counts for the group

* chore: bug in query fix

* chore: onboarding API changes

* chore: integration tests added

* chore: added podcountsbyphase in other entities

* chore: added pod status counts for other entities

* chore: added integration tests for other entities

* chore: added checks api changes for other entities

* chore: rearrangement

* chore: removed succeeded status and mark it as completed

* chore: query beautified

* chore: corrected metrics list for metadata lookup

* chore: removed dead constants
2026-07-06 20:11:40 +00:00
Vikrant Gupta
a3fa475e88 docs: add contributing guidelines for authz (#11986)
* docs: add contributing guidelines for authz

* docs: address review comments on authz contributing guide
2026-07-06 19:08:50 +00:00
Vinicius Lourenço
64d0ada495 feat(infrastructure-monitoring): add disk usage chart for hosts (#11990) 2026-07-06 18:29:51 +00:00
Yunus M
60886fd906 fix: opening /ai-assistant opens 2 new conversations instead of one (#11923) 2026-07-06 16:58:00 +00:00
Yunus M
d8954da307 fix: context not being passed when user click suggestions in empty state (#11925) 2026-07-06 16:57:48 +00:00
Shivam Gupta
7476a2cacb feat(onboarding): add NGINX Ingress, FluxCD, Cassandra, Cloudflare Workers, PlanetScale, and Hermes Agent datasources (#11884)
* feat(onboarding): add NGINX Ingress, FluxCD, Cassandra, Cloudflare Workers, PlanetScale, and Hermes Agent datasources

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

* chore(onboarding): optimize new datasource logo SVGs with svgo

Run svgo over cassandra.svg, fluxcd.svg, and planetscale.svg
(viewBox and <title> preserved). Trims coordinate precision and
metadata; fluxcd.svg shrinks ~24%.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 15:18:54 +00:00
Vinicius Lourenço
e1dd7d52eb feat(authz): add new authz components (#11970)
* refactor(authz): drop create guarded route component

* refactor(authz): drop guarded authz component

* refactor(authz): support list of permissions on denied callout

* refactor(authz): support list of permissions on denied page

* feat(authz): add guard authz button

* feat(authz): add guard authz with page/content variants

* feat(authz): add withAuthZ HOC component to guard pages/components

* feat(authz): add docs for authz + agents update
2026-07-06 14:13:35 +00:00
Vikrant Gupta
1644e35d9c feat(authz): make serviceaccount_role and user_role first class citizens (#11977)
* feat(authz): add serviceaccount and user role handlers

* feat(authz): add support for role and service account extractors

* feat(authz): make the user apis v2

* feat(authz): return the existing service account role id
2026-07-06 13:48:46 +00:00
Abhi kumar
379331ee77 feat(dashboards-v2): panel drilldown — View in Logs/Traces (#11895)
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(dashboards-v2): add the panel View-modal state and hooks

URL-driven open state (useViewPanel off expandedWidgetId, V1 parity), a per-view
time window isolated from the dashboard (useViewPanelTimeWindow), and
useViewPanelEditor which layers the drilldown reset + type-selector signal/query
type on top of the shared usePanelEditSession.

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

Naming/convention:
- UsePanelEditSessionApi -> UsePanelEditSessionReturn; fullKind -> panelKind
- useViewPanelEditor -> useViewPanelMode (hook, types, file, refs, test)
- useOpenPanelEditor `state` param -> `handoffState` (view -> edit handoff)

Types tightened:
- useViewPanelMode: panelDefinition is required (registry always resolves)
- signal is required: fold the default-signal fallback into the hook so the
  query builder + type selector always get a signal; drop the now-dead
  defaultSignal from the return and the `signal ?? defaultSignal` call site.
  Behavior-preserving: only List restricts signals and it's already gated by
  query type under PromQL/ClickHouse.

Misc:
- PanelEditorPage: drop redundant optional chaining on getPanelDefinition
- ViewPanelModal: Typography.Text title with ellipsis truncation
- ViewPanelModalHeader: clearer queryType JSDoc

* feat(dashboards-v2): add panel drilldown click-enrichment utils

* feat(dashboards-v2): declare drilldown as a per-kind panel capability

* feat(dashboards-v2): open a drilldown context menu with View in Logs/Traces

* feat(dashboards-v2): substitute dashboard variables in drilldown View-in-X

Clicking View in Logs/Traces from a panel drilldown now resolves the
panel query's $var/dashboard-variable references before building the
explorer URL (V1 parity with useBaseAggregateOptions). New
useResolvedDrilldownQuery hook fires a /substitute_vars round-trip
(useReplaceVariables + buildQueryRangeRequest) when the aggregate menu
opens and the dashboard has selections, mirroring the V2-native path in
useCreateAlertFromPanel. While resolving, both actions show a spinner
and are disabled; no-variable dashboards skip the round-trip.

* refactor(dashboards-v2): address drilldown review comments

- stepClickTimeRange takes a single stepInterval (defaulted in props);
  callers do the per-query lookup from exec-stats stepIntervals
- move DrilldownAggregateMenu static inline styles into a CSS module,
  keeping only the dynamic series color inline

* feat(dashboards-v2): drill down filter-by-value into the View modal (#11959)

* feat(dashboards-v2): drill down filter-by-value into the View modal

A table group-cell click opens the filter-by-value menu (Is this / Is
not this); picking an operator adds the filter to the panel query and
opens the refined result in the View modal, persisted in the URL (V1
parity). Reuses V1's read-only getGroupContextMenuConfig; no V1 files
modified.

* refactor(dashboards-v2): type the filter-by-value ClickedData without a double-cast

Address review: build a genuinely-typed ClickedData for the group-cell
filter menu instead of an `as unknown as ClickedData` escape hatch.
getGroupContextMenuConfig reads only column.dataIndex, so column carries
just that field and record gets a minimal valid RowData.

* refactor(dashboards-v2): render the filter-by-value menu from a component

Reshape useDrilldownFilter to return data (isGroupColumnClick + onFilter)
instead of JSX, and move the operator-menu rendering into a new
DrilldownFilterMenu component wired in useDrilldown. Keeps hooks to
logic/state and rendering in components (PR review preference).

* refactor(dashboards-v2): strongly-type the drilldown popover coordinates

Replace the drilldown's use of V1's loosely-typed useCoordinates (clickedData:
any) with a V2-local useDrilldownCoordinates hook generic over the click
payload, so DrilldownContext flows through without a cast. Extract the pure
viewport-clamp math into calculatePopoverPosition with unit tests.

* feat(dashboards-v2): panel drilldown — Breakout by .. (#11896)

* feat(dashboards-v2): gate the panel editor through the capability guard

Route the panel editor's query builder and visualization type switcher through
the capability guard instead of V1's PANEL_TYPE_TO_QUERY_TYPES (now no longer
imported by any V2 file):

- PanelEditorQueryBuilder is keyed on PanelKind; its query-type tabs and
  query-builder field visibility come from the guard.
- Switching the panel kind coerces the active query type via the guard.
- The visualization type switcher disables a kind when the active query type or
  datasource is incompatible with it (e.g. List under ClickHouse/PromQL, or List
  with a metrics query). The live query type is read from the query-builder
  provider so a not-yet-staged new panel still gates correctly, and a tooltip
  explains why a type is disabled. ConfigSelect gains opt-in per-option tooltips.

* feat(dashboards-v2): create alerts from a dashboard panel

Wire the panel actions menu's "Create Alerts" item to seed a new alert
from the panel's query. `buildCreateAlertUrl` translates the panel's V5
queries into the V1 compositeQuery the alert page reads (tagged with the
panel type, v5 version and a dashboards source), and
`useCreateAlertFromPanel` opens /alerts/new in a new tab and logs the
action. Available regardless of edit access (V1 parity: create-alert
works on locked dashboards too).

To reach the query, the full panel is threaded through
Panel -> PanelHeader -> PanelActionsMenu -> usePanelActionItems instead
of just its kind; the header now derives its name/description from the
panel as well.

* refactor(dashboards-v2): ready ChartManager for the standalone view

Prep the shared graph-manager so it renders cleanly inside the View
modal: swap the legacy useNotifications save toast for the sonner toast,
and let ChartLayout grow to its content height (auto) so the manager's
legend table isn't clipped under the chart.

* feat(dashboards-v2): add the panel View modal

The View modal UI — a compact, single-host drilldown editor matching V1's
FullView: a draft preview over the per-view window, a toolbar (Reset
Query, Switch to Edit Mode, panel-type selector, time picker, refresh),
and the query-builder-only drilldown editor (no ClickHouse/PromQL tabs,
per V1). Edits are temporary — they never touch the saved dashboard.

* feat(dashboards-v2): open a drilldown context menu with View in Logs/Traces

* feat(dashboards-v2): add the Breakout by .. drilldown submenu

* chore: pr review fixes

* refactor(dashboards-v2): render the Breakout by .. menu from a component

Reshape useDrilldownBreakout to return data (queryData + onBreakout) instead
of JSX, and move the picker rendering into a new DrilldownBreakoutMenu
component (with a CSS module) wired in useDrilldown. Keeps hooks to
logic/state and rendering in components (PR review preference).

* feat(dashboards-v2): panel drilldown — context links (#11897)

* feat(dashboards-v2): resolve panel context links in the drilldown menu

* feat(dashboards-v2): panel drilldown — dashboard variables + trace link (#11964)

* feat(dashboards-v2): complete drilldown V1 parity — dashboard variables + trace link

Add the last two V1 drilldown features to V2 panels:

- Dashboard Variables submenu: from a grouped click, the aggregate menu now
  offers, per group-by field, Set/Unset a matching dynamic variable to the
  clicked value, or Create a new dynamic variable from it. Set/Unset are
  runtime-only (store + URL, matching V2 selections); Create is the one
  persisted path (appends to spec.variables via the optimistic patch).
  New Panel/hooks/useDrilldownDashboardVariables.tsx mirrors V1's
  useDashboardVarConfig; wired into useDrilldown as a new submenu.

- "View Trace Details" data link: the aggregate menu shows a /trace/{id}
  link when the clicked point carries a trace_id (V1 getDataLinks parity).
  New Panels/utils/drilldown/getDataLinks.ts.

Exports variablesUrlParser from useVariableSelection so the submenu reuses the
runtime-selection URL contract without re-running its seeding effect.

* fix(dashboards-v2): restrict drilldown filters to group-by dimensions

The pure-V5 flattening backfills an ungrouped series' labels with
{queryName: queryName} for its legend; the chart/pie enrichers turned every
label into an equality filter, leaking a bogus `A=A` into the drilldown
context. That broke the "filters are the clicked point's group-by labels,
empty when ungrouped" contract — surfacing a spurious Dashboard Variables
entry and a corrupt View-in-Logs query that V1 never shows.

Add getGroupByFilters to intersect the clicked labels with the query's
groupBy, and use it in enrichChartClick/enrichPieClick.

* refactor(dashboards-v2): dashboard-variables drilldown review fixes

Address PR review on the Dashboard Variables submenu:
- render it from a DrilldownDashboardVariablesMenu component; the hook now
  returns data (hasFieldVariables + actions) instead of JSX, taking a required
  filters array + signal (rendering belongs in the component, not the hook)
- identify dynamic variables by plugin kind on the DTO, not by mapping every
  variable to a form model first
- drop the redundant useGetDashboardV2 enabled guard (the generated hook
  already gates on !!id)
- reference the DrilldownSubMenu enum instead of a raw string
- move inline styles to a CSS module

* refactor(dashboards-v2): read drilldown variables via useDashboardFetchRequired

Use the shared useDashboardFetchRequired in useDrilldownDashboardVariables
instead of calling useGetDashboardV2 directly, so it resolves through the one
dashboard cache entry and drops the `?? []` guards. Filter dynamic variables on
the form model's `type` (via dtoToFormModel) rather than casting the raw spec.

That hook throws on a missing dashboard id, and the id was seeded into the store
in a useEffect — undefined on the first Panel render. Seed it synchronously
during render in DashboardContainer instead, and make setEditContext idempotent
so the render-phase call is a safe no-op once seeded.

* chore: fix failing lint
2026-07-06 13:18:39 +00:00
Ashwin Bhatkal
4d079a8338 refactor(dashboard-v2): unify dashboard tag chips into one TagBadge component (#11984)
* fix(dashboard-v2): render tag chips with the sienna Badge component

The key:value tag editor rendered chips as a hand-rolled div + ghost buttons with
sienna colors mixed in by hand. Use the @signozhq Badge (color=sienna, closable)
so the chips match the design system and the rest of the dashboard tag styling.

* fix(dashboard-v2): keep key:key tags intact in the settings tag editor

tagsToStrings collapsed a tag whose key equals its value down to a bare key, so
the settings tag editor showed `env` for an `env:env` tag. Always render both
sides so key:key tags round-trip through the editor unchanged.

* refactor(dashboard-v2): single TagBadge component for all dashboard tags

The list rows, details header, and tag editors each rendered tag chips their own
way (two hand-rolled sienna variants + an inline Badge), so they drifted. Extract
one TagBadge (sienna Badge, optional closable) and use it everywhere, so tag
styling changes in one place. List-row chips were not even sienna before.

* fix(dashboard-v2): tag label inherits the Badge sienna color

The chip label button used --button-variant-ghost-color: inherit, but for a CSS
custom property inherit takes the parent's value of that same variable (unset),
not the parent color — so the label fell back to the muted ghost default. Use
currentColor so it picks up the Badge's sienna foreground.
2026-07-06 13:05:23 +00:00
Vinicius Lourenço
91938925ec feat(infra-monitoring): rework the category selection (#11957) 2026-07-06 12:04:07 +00:00
787 changed files with 62020 additions and 12151 deletions

View File

@@ -48,7 +48,11 @@ jobs:
- logspipelines
- passwordauthn
- preference
- querier
- querierlogs
- queriertraces
- queriermetrics
- querierscalar
- queriercommon
- rawexportdata
- role
- rootuser

View File

@@ -155,8 +155,8 @@ querier:
cache_ttl: 168h
# The interval for recent data that should not be cached.
flux_interval: 5m
# The maximum number of concurrent queries for missing ranges.
max_concurrent_queries: 4
# The maximum number of queries a single query range request runs at once.
max_concurrent_queries: 8
# When filtering logs by trace_id, clamp the query window to the trace time
# range with padding to include slightly delayed log exports. Logs only; set
# to 0 to disable.

View File

@@ -515,6 +515,13 @@ components:
url:
type: string
type: object
AuthtypesDeprecatedPostableUserRole:
properties:
id:
type: string
required:
- id
type: object
AuthtypesGettableAuthDomain:
properties:
authNProviderInfo:
@@ -660,17 +667,20 @@ components:
type: string
userRoles:
items:
$ref: '#/components/schemas/AuthtypesPostableUserRole'
$ref: '#/components/schemas/AuthtypesDeprecatedPostableUserRole'
type: array
required:
- email
type: object
AuthtypesPostableUserRole:
properties:
id:
roleId:
type: string
userId:
type: string
required:
- id
- userId
- roleId
type: object
AuthtypesRelation:
enum:
@@ -4179,6 +4189,7 @@ components:
- namespaces
- clusters
- volumes
- kube_containers
type: string
InframonitoringtypesChecks:
properties:
@@ -4251,6 +4262,8 @@ components:
$ref: '#/components/schemas/InframonitoringtypesNodeCountsByReadiness'
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
required:
- clusterName
- clusterCPU
@@ -4259,6 +4272,7 @@ components:
- clusterMemoryAllocatable
- nodeCountsByReadiness
- podCountsByPhase
- podCountsByStatus
- meta
type: object
InframonitoringtypesClusters:
@@ -4281,6 +4295,158 @@ components:
- total
- endTimeBeforeRetention
type: object
InframonitoringtypesContainerCountsByReady:
properties:
notReady:
type: integer
ready:
type: integer
required:
- ready
- notReady
type: object
InframonitoringtypesContainerCountsByStatus:
properties:
completed:
type: integer
containerCannotRun:
type: integer
containerCreating:
type: integer
crashLoopBackOff:
type: integer
createContainerConfigError:
type: integer
errImagePull:
type: integer
error:
type: integer
imagePullBackOff:
type: integer
oomKilled:
type: integer
running:
type: integer
terminated:
type: integer
unknown:
type: integer
waiting:
type: integer
required:
- running
- waiting
- terminated
- crashLoopBackOff
- imagePullBackOff
- errImagePull
- createContainerConfigError
- containerCreating
- oomKilled
- completed
- error
- containerCannotRun
- unknown
type: object
InframonitoringtypesContainerReady:
enum:
- ready
- not_ready
- no_data
type: string
InframonitoringtypesContainerRecord:
properties:
containerCountsByReady:
$ref: '#/components/schemas/InframonitoringtypesContainerCountsByReady'
containerCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesContainerCountsByStatus'
containerName:
type: string
cpu:
format: double
type: number
cpuLimitUtilization:
format: double
type: number
cpuRequestUtilization:
format: double
type: number
memory:
format: double
type: number
memoryLimitUtilization:
format: double
type: number
memoryRequestUtilization:
format: double
type: number
meta:
additionalProperties:
type: string
nullable: true
type: object
podUID:
type: string
ready:
$ref: '#/components/schemas/InframonitoringtypesContainerReady'
restarts:
format: int64
type: integer
status:
$ref: '#/components/schemas/InframonitoringtypesContainerStatus'
required:
- podUID
- containerName
- status
- containerCountsByStatus
- ready
- containerCountsByReady
- restarts
- cpu
- cpuRequestUtilization
- cpuLimitUtilization
- memory
- memoryRequestUtilization
- memoryLimitUtilization
- meta
type: object
InframonitoringtypesContainerStatus:
enum:
- running
- waiting
- terminated
- crashloopbackoff
- imagepullbackoff
- errimagepull
- createcontainerconfigerror
- containercreating
- oomkilled
- completed
- error
- containercannotrun
- unknown
- no_data
type: string
InframonitoringtypesContainers:
properties:
endTimeBeforeRetention:
type: boolean
records:
items:
$ref: '#/components/schemas/InframonitoringtypesContainerRecord'
type: array
total:
type: integer
type:
$ref: '#/components/schemas/InframonitoringtypesResponseType'
warning:
$ref: '#/components/schemas/Querybuildertypesv5QueryWarnData'
required:
- type
- records
- total
- endTimeBeforeRetention
type: object
InframonitoringtypesDaemonSetRecord:
properties:
currentNodes:
@@ -4314,6 +4480,8 @@ components:
type: object
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
required:
- daemonSetName
- daemonSetCPU
@@ -4325,6 +4493,7 @@ components:
- desiredNodes
- currentNodes
- podCountsByPhase
- podCountsByStatus
- meta
type: object
InframonitoringtypesDaemonSets:
@@ -4380,6 +4549,8 @@ components:
type: object
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
required:
- deploymentName
- deploymentCPU
@@ -4391,6 +4562,7 @@ components:
- desiredPods
- availablePods
- podCountsByPhase
- podCountsByStatus
- meta
type: object
InframonitoringtypesDeployments:
@@ -4523,6 +4695,8 @@ components:
type: object
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
successfulPods:
type: integer
required:
@@ -4538,6 +4712,7 @@ components:
- failedPods
- successfulPods
- podCountsByPhase
- podCountsByStatus
- meta
type: object
InframonitoringtypesJobs:
@@ -4628,11 +4803,14 @@ components:
type: string
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
required:
- namespaceName
- namespaceCPU
- namespaceMemory
- podCountsByPhase
- podCountsByStatus
- meta
type: object
InframonitoringtypesNamespaces:
@@ -4698,11 +4876,14 @@ components:
type: string
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
required:
- nodeName
- condition
- nodeCountsByReadiness
- podCountsByPhase
- podCountsByStatus
- nodeCPU
- nodeCPUAllocatable
- nodeMemory
@@ -4748,6 +4929,64 @@ components:
- failed
- unknown
type: object
InframonitoringtypesPodCountsByStatus:
properties:
completed:
type: integer
containerCannotRun:
type: integer
containerCreating:
type: integer
crashLoopBackOff:
type: integer
createContainerConfigError:
type: integer
errImagePull:
type: integer
error:
type: integer
evicted:
type: integer
failed:
type: integer
imagePullBackOff:
type: integer
nodeAffinity:
type: integer
nodeLost:
type: integer
oomKilled:
type: integer
pending:
type: integer
running:
type: integer
shutdown:
type: integer
unexpectedAdmissionError:
type: integer
unknown:
type: integer
required:
- pending
- running
- failed
- unknown
- crashLoopBackOff
- imagePullBackOff
- errImagePull
- createContainerConfigError
- containerCreating
- oomKilled
- completed
- error
- containerCannotRun
- evicted
- nodeAffinity
- nodeLost
- shutdown
- unexpectedAdmissionError
type: object
InframonitoringtypesPodPhase:
enum:
- pending
@@ -4778,6 +5017,8 @@ components:
type: number
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
podMemory:
format: double
type: number
@@ -4789,6 +5030,11 @@ components:
type: number
podPhase:
$ref: '#/components/schemas/InframonitoringtypesPodPhase'
podRestarts:
format: int64
type: integer
podStatus:
$ref: '#/components/schemas/InframonitoringtypesPodStatus'
podUID:
type: string
required:
@@ -4801,9 +5047,34 @@ components:
- podMemoryLimit
- podPhase
- podCountsByPhase
- podStatus
- podCountsByStatus
- podRestarts
- podAge
- meta
type: object
InframonitoringtypesPodStatus:
enum:
- pending
- running
- failed
- unknown
- crashloopbackoff
- imagepullbackoff
- errimagepull
- createcontainerconfigerror
- containercreating
- oomkilled
- completed
- error
- containercannotrun
- evicted
- nodeaffinity
- nodelost
- shutdown
- unexpectedadmissionerror
- no_data
type: string
InframonitoringtypesPods:
properties:
endTimeBeforeRetention:
@@ -4850,6 +5121,32 @@ components:
- end
- limit
type: object
InframonitoringtypesPostableContainers:
properties:
end:
format: int64
type: integer
filter:
$ref: '#/components/schemas/Querybuildertypesv5Filter'
groupBy:
items:
$ref: '#/components/schemas/Querybuildertypesv5GroupByKey'
nullable: true
type: array
limit:
type: integer
offset:
type: integer
orderBy:
$ref: '#/components/schemas/Querybuildertypesv5OrderBy'
start:
format: int64
type: integer
required:
- start
- end
- limit
type: object
InframonitoringtypesPostableDaemonSets:
properties:
end:
@@ -5102,6 +5399,8 @@ components:
type: object
podCountsByPhase:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByPhase'
podCountsByStatus:
$ref: '#/components/schemas/InframonitoringtypesPodCountsByStatus'
statefulSetCPU:
format: double
type: number
@@ -5133,6 +5432,7 @@ components:
- desiredPods
- currentPods
- podCountsByPhase
- podCountsByStatus
- meta
type: object
InframonitoringtypesStatefulSets:
@@ -7430,6 +7730,13 @@ components:
enum:
- basic
type: string
ServiceaccounttypesDeprecatedPostableServiceAccountRole:
properties:
id:
type: string
required:
- id
type: object
ServiceaccounttypesGettableFactorAPIKey:
properties:
createdAt:
@@ -7486,10 +7793,13 @@ components:
type: object
ServiceaccounttypesPostableServiceAccountRole:
properties:
id:
roleId:
type: string
serviceAccountId:
type: string
required:
- id
- serviceAccountId
- roleId
type: object
ServiceaccounttypesServiceAccount:
properties:
@@ -12252,6 +12562,188 @@ paths:
summary: Update route policy
tags:
- routepolicies
/api/v1/service_account_roles:
post:
deprecated: false
description: This endpoint assigns a role to a service account
operationId: CreateServiceAccountRole
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ServiceaccounttypesPostableServiceAccountRole'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TypesIdentifiable'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- serviceaccount:attach
- role:attach
- tokenizer:
- serviceaccount:attach
- role:attach
summary: Create service account role
tags:
- serviceaccount
/api/v1/service_account_roles/{id}:
delete:
deprecated: false
description: This endpoint revokes a role from a service account
operationId: DeleteServiceAccountRole
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- serviceaccount:detach
- role:detach
- tokenizer:
- serviceaccount:detach
- role:detach
summary: Delete service account role
tags:
- serviceaccount
get:
deprecated: false
description: This endpoint gets an existing service account role
operationId: GetServiceAccountRole
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/ServiceaccounttypesServiceAccountRole'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- serviceaccount:read
- tokenizer:
- serviceaccount:read
summary: Get service account role
tags:
- serviceaccount
/api/v1/service_accounts:
get:
deprecated: false
@@ -12821,9 +13313,9 @@ paths:
tags:
- serviceaccount
post:
deprecated: false
deprecated: true
description: This endpoint assigns a role to a service account
operationId: CreateServiceAccountRole
operationId: CreateServiceAccountRoleDeprecated
parameters:
- in: path
name: id
@@ -12834,7 +13326,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/ServiceaccounttypesPostableServiceAccountRole'
$ref: '#/components/schemas/ServiceaccounttypesDeprecatedPostableServiceAccountRole'
responses:
"201":
content:
@@ -12886,9 +13378,9 @@ paths:
- serviceaccount
/api/v1/service_accounts/{id}/roles/{rid}:
delete:
deprecated: false
deprecated: true
description: This endpoint revokes a role from service account
operationId: DeleteServiceAccountRole
operationId: DeleteServiceAccountRoleDeprecated
parameters:
- in: path
name: id
@@ -15786,6 +16278,86 @@ paths:
summary: List Jobs for Infra Monitoring
tags:
- inframonitoring
/api/v2/infra_monitoring/kube_containers:
post:
deprecated: false
description: 'Returns a paginated list of Kubernetes containers with key kubeletstats
metrics: CPU usage (cores), CPU request/limit utilization, memory working
set, and memory request/limit utilization. Each container also reports health
signals from the k8s_cluster receiver: status (kubectl-style display status
derived from k8s.container.status.state + k8s.container.status.reason), restarts
(absolute count from k8s.container.restarts), and ready (ready/not_ready from
k8s.container.ready). The row identity is (k8s.pod.uid, k8s.container.name),
stable across container restarts. Each container includes metadata attributes
(k8s.container.name, k8s.pod.name, container.image.name, container.image.tag,
k8s.namespace.name, k8s.node.name, k8s.cluster.name, and workload owner such
as deployment/statefulset/daemonset/job). The response type is ''list'' for
the default (k8s.pod.uid, k8s.container.name) grouping (each row is one container
with its current status and ready state) or ''grouped_list'' for custom groupBy
keys (each row aggregates containers in the group with per-status counts under
containerCountsByStatus, per-readiness counts under containerCountsByReady,
and restarts as the group sum). Status requires the optional k8s.container.status.state
and k8s.container.status.reason metrics; when either is missing, status is
omitted and a warning is returned while restarts and ready are still computed.
Supports filtering via a filter expression, custom groupBy, ordering by any
of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit),
and pagination via offset/limit. Also reports whether the requested time range
falls before the data retention boundary. Numeric metric fields (cpu, cpuRequestUtilization,
cpuLimitUtilization, memory, memoryRequestUtilization, memoryLimitUtilization)
and restarts return -1 as a sentinel when no data is available for that field.'
operationId: ListContainers
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/InframonitoringtypesPostableContainers'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/InframonitoringtypesContainers'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List Kubernetes Containers for Infra Monitoring
tags:
- inframonitoring
/api/v2/infra_monitoring/namespaces:
post:
deprecated: false
@@ -21990,6 +22562,184 @@ paths:
summary: Rotate session
tags:
- sessions
/api/v2/user_roles:
post:
deprecated: false
description: This endpoint assigns a role to a user
operationId: CreateUserRole
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/AuthtypesPostableUserRole'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TypesIdentifiable'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Create user role
tags:
- users
/api/v2/user_roles/{id}:
delete:
deprecated: false
description: This endpoint revokes a role from a user
operationId: DeleteUserRole
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Delete user role
tags:
- users
get:
deprecated: false
description: This endpoint gets an existing user role
operationId: GetUserRole
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/AuthtypesUserRole'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Get user role
tags:
- users
/api/v2/users:
get:
deprecated: false
@@ -22443,7 +23193,7 @@ paths:
tags:
- users
post:
deprecated: false
deprecated: true
description: This endpoint assigns the role to the user roles by user id
operationId: SetRoleByUserID
parameters:
@@ -22494,7 +23244,7 @@ paths:
- users
/api/v2/users/{id}/roles/{roleId}:
delete:
deprecated: false
deprecated: true
description: This endpoint removes a role from the user by user id and role
id
operationId: RemoveUserRoleByUserIDAndRoleID

View File

@@ -0,0 +1,149 @@
# Authz
SigNoz uses [OpenFGA](https://openfga.dev/), a relationship-based access control (ReBAC) system, to authorize every request. Transactions are never attached to users directly — they are attached to **roles** (as relationship tuples in OpenFGA), and users or service accounts (principals) are made **assignees** of those roles. The central interface is `AuthZ` in [pkg/authz/authz.go](/pkg/authz/authz.go), backed by an embedded OpenFGA server in [pkg/authz/openfgaserver](/pkg/authz/openfgaserver/server.go).
As a feature author, you will rarely touch OpenFGA directly. You interact with two layers:
1. **The registries** in [pkg/types/coretypes](/pkg/types/coretypes) — where every resource, verb, and managed-role permission is declared in code.
2. **The route wiring** in [pkg/apiserver/signozapiserver](/pkg/apiserver/signozapiserver) — where each route declares what resource it touches and which verb it needs, and the middleware turns that into a check.
## What are the building blocks?
### Type
A `Type` is the coarse FGA type of a resource. The set is finite and defined in [pkg/types/coretypes/registry_type.go](/pkg/types/coretypes/registry_type.go): `user`, `serviceaccount`, `anonymous`, `role`, `organization`, `metaresource`, and `telemetryresource`. Each type carries a selector regex (what a valid ID looks like) and the verbs allowed on it.
Almost every feature resource is a `metaresource` (dashboards, rules, pipelines, ...) or a `telemetryresource` (logs, traces, metrics). You should almost never need a new type.
### Verb
A `Verb` is the action being authorized. All verbs are defined in [pkg/types/coretypes/registry_verb.go](/pkg/types/coretypes/registry_verb.go): `create`, `read`, `update`, `delete`, `list`, `assignee`, `attach`, and `detach`.
- `assignee` is special: it is the membership relation between a subject and a role ("user X is an assignee of role Y"), not an action a route checks for.
- `attach`/`detach` authorize linking two resources together (e.g. assigning a role to a service account).
### Kind
A `Kind` is the fine-grained name of your resource — `dashboard`, `rule`, `quick-filter`. Kinds are registered in [pkg/types/coretypes/registry_kind.go](/pkg/types/coretypes/registry_kind.go). A kind rides on top of an existing type, so adding one does **not** require any OpenFGA schema change.
### Resource
A `Resource` ties a type and a kind together and knows how to render itself as an FGA object string. The interface lives in [pkg/types/coretypes/resource.go](/pkg/types/coretypes/resource.go):
```go
type Resource interface {
Type() Type
Kind() Kind
Prefix(orgId valuer.UUID) string // metaresource:organization/<orgID>/dashboard
Object(orgId valuer.UUID, selector string) string
Scope(verb Verb) string // dashboard:read
AllowedVerbs() []Verb
}
```
All resources are registered in [pkg/types/coretypes/registry_resource.go](/pkg/types/coretypes/registry_resource.go) using constructors like `NewResourceMetaResource(KindDashboard)` or `NewResourceTelemetryResource(KindLogs)`.
### Selector
A `Selector` identifies *which* instance(s) of a resource a check is about — a UUID, a role name, or the wildcard `*`. A `SelectorFunc` maps the extracted resource ID to selectors at request time. Two prebuilt ones cover most routes ([pkg/types/coretypes/selector.go](/pkg/types/coretypes/selector.go)):
- `WildcardSelector` — the check is against all instances of the resource (`create`, `list`).
- `IDSelector` — the check is against the specific instance *or* the wildcard (`read`, `update`, `delete` of one object). A subject authorized on `*` is authorized on every instance.
When the ID in the request is not what FGA needs (e.g. routes receive a role UUID but FGA objects use role names), write a custom `SelectorFunc` — see `roleSelector` in [pkg/apiserver/signozapiserver/serviceaccount.go](/pkg/apiserver/signozapiserver/serviceaccount.go).
### Roles, transactions, and tuples
SigNoz ships four managed roles, declared in [pkg/types/coretypes/registry_managed_role.go](/pkg/types/coretypes/registry_managed_role.go): `signoz-admin`, `signoz-editor`, `signoz-viewer`, and `signoz-anonymous`. Their permissions are declared in code as `Transaction`s (a verb on an object) in `ManagedRoleToTransactions` — this map is the single source of truth for what each managed role can do.
At organization bootstrap, `CreateManagedRoles` and `CreateManagedUserRoleTransactions` (see [pkg/authz/authz.go](/pkg/authz/authz.go)) persist the role rows and write one OpenFGA tuple per transaction, linking `role:organization/<orgID>/role/<name>#assignee` to each permitted object. Users and service accounts are then granted roles via `Grant`/`Revoke`, which writes `assignee` tuples. Custom roles (enterprise) are managed through the roles API in [pkg/authz/signozauthzapi/handler.go](/pkg/authz/signozauthzapi/handler.go).
### Schema
The OpenFGA authorization model is a hand-written DSL, embedded at build time: [pkg/authz/openfgaschema/base.fga](/pkg/authz/openfgaschema/base.fga) for community and [ee/authz/openfgaschema/base.fga](/ee/authz/openfgaschema/base.fga) for enterprise. The community model only supports role assignment; the enterprise model defines per-verb relations on every type, enabling genuine per-resource checks. This split is why `CheckWithTupleCreation` behaves differently per edition (see [How does a check work at runtime?](#how-does-a-check-work-at-runtime)). You only touch these files when introducing a brand-new **type** — never for a new kind.
## How do I add authz to my feature?
### 1. Register the kind
Add your kind in [pkg/types/coretypes/registry_kind.go](/pkg/types/coretypes/registry_kind.go) and append it to `Kinds`:
```go
KindThing = MustNewKind("thing")
```
### 2. Register the resource
Add the resource in [pkg/types/coretypes/registry_resource.go](/pkg/types/coretypes/registry_resource.go) and append it to `Resources`:
```go
ResourceMetaResourceThing = NewResourceMetaResource(KindThing)
```
Pass an explicit verb list to `NewResourceMetaResource` only if your resource supports fewer verbs than the type default.
### 3. Grant permissions to managed roles
Decide what each managed role can do with your resource and add the transactions in [pkg/types/coretypes/registry_managed_role.go](/pkg/types/coretypes/registry_managed_role.go):
```go
// thing — editors manage, viewers read
{Verb: VerbCreate, Object: *MustNewObject(ResourceRef{Type: TypeMetaResource, Kind: KindThing}, WildCardSelectorString)},
```
The convention so far: admin gets everything, editor gets CRUD on day-to-day observability resources, viewer gets `read`/`list`, anonymous gets nothing (except public dashboards).
### 4. Wire the route
Register the route in [pkg/apiserver/signozapiserver](/pkg/apiserver/signozapiserver), wrapping your handler with `CheckResources` and declaring what the route touches via a `ResourceDef` ([pkg/http/handler/resourcedef.go](/pkg/http/handler/resourcedef.go)). A complete example from [pkg/apiserver/signozapiserver/serviceaccount.go](/pkg/apiserver/signozapiserver/serviceaccount.go):
```go
router.Handle("/api/v1/service_accounts", handler.New(
provider.authzMiddleware.CheckResources(provider.serviceAccountHandler.Create, authtypes.SigNozAdminRoleName),
handler.OpenAPIDef{
ID: "CreateServiceAccount",
// ...
SecuritySchemes: newScopedSecuritySchemes([]string{coretypes.ResourceServiceAccount.Scope(coretypes.VerbCreate)}),
},
handler.WithResourceDefs(handler.BasicResourceDef{
Resource: coretypes.ResourceServiceAccount,
Verb: coretypes.VerbCreate,
Category: coretypes.ActionCategoryAccessControl,
ID: coretypes.ResponseJSONPath("data.id"),
Selector: coretypes.WildcardSelector,
}),
)).Methods(http.MethodPost)
```
The pieces:
- **`CheckResources(handlerFn, roles...)`** — the resource-aware authorization wrapper from [pkg/http/middleware/authz.go](/pkg/http/middleware/authz.go). The role list is the community-edition fallback: which managed roles may call this route when per-resource checks are unavailable.
- **`ResourceDef`** — declares the resource, verb, audit category, how to extract the instance ID, and how to turn that ID into selectors. ID extractors live in [pkg/types/coretypes/extractor.go](/pkg/types/coretypes/extractor.go): `PathParam("id")`, `BodyJSONPath("data.id")`, `BodyJSONArray("ids")`, and `ResponseJSONPath("data.id")` for IDs only known after the handler runs (e.g. `create`).
- **`SecuritySchemes`** — advertises the required scope (`resource.Scope(verb)`, e.g. `serviceaccount:create`) in the OpenAPI spec.
For routes that link two resources, use `AttachDetachSiblingResourceDef` (both sides are authz-checked, e.g. attaching a role to a service account requires `attach` on **both** the service account and the role). For parent-child routes (e.g. creating an API key under a service account), both sides are checked too, but with different verbs: declare a `BasicResourceDef` checking the child with `create`/`delete`, alongside an `AttachDetachParentChildResourceDef` checking the parent with `attach`/`detach` (within that def the child is only recorded for audit) — see the `/api/v1/service_accounts/{id}/keys` route in [pkg/apiserver/signozapiserver/serviceaccount.go](/pkg/apiserver/signozapiserver/serviceaccount.go).
Prefer `CheckResources` with a `ResourceDef` for anything resource-shaped. The older coarse gates `ViewAccess`/`EditAccess`/`AdminAccess` only check "does the caller hold one of these roles" and give up per-resource granularity; `OpenAccess` performs no authorization (authentication still applies); `CheckWithoutClaims` serves anonymous routes such as public dashboards.
### 5. Backfill existing organizations (only if needed)
Managed-role tuples are written from the registry at organization creation, so **new resources need no migration for new organizations**. If existing organizations must get the new permissions, add a migration in [pkg/sqlmigration](/pkg/sqlmigration) that inserts the tuples — see [083_add_role_crud_tuples.go](/pkg/sqlmigration/083_add_role_crud_tuples.go) for the pattern.
## How does a check work at runtime?
1. The resource middleware ([pkg/http/middleware/resource.go](/pkg/http/middleware/resource.go)) runs on every request. It reads the matched handler's `ResourceDef`s, extracts the resource IDs from path/body, and stores the resolved resources in the request context.
2. `CheckResources` reads them back, runs each `SelectorFunc`, and calls `AuthZ.CheckWithTupleCreation(ctx, claims, orgID, relation, resource, selectors, roleSelectors)`.
3. What happens next depends on the edition:
- **Community** ([pkg/authz/openfgaserver/server.go](/pkg/authz/openfgaserver/server.go)) ignores the resource and selectors and only checks whether the subject is an `assignee` of one of the allowed roles — a plain role gate.
- **Enterprise** ([ee/authz/openfgaserver/server.go](/ee/authz/openfgaserver/server.go)) builds tuples via `authtypes.NewTuples` — subject `user:organization/<orgID>/user/<userID>`, relation `create`, object `serviceaccount:organization/<orgID>/serviceaccount/*` — and batch-checks them against OpenFGA: genuine per-resource authorization, including custom roles.
Because both paths go through the same middleware and the same `ResourceDef` declarations, a route wired once works correctly in both editions.
## What should I remember?
- Declare authz in the registries ([pkg/types/coretypes](/pkg/types/coretypes)), not in migrations — tuples for new organizations are derived from code at bootstrap.
- A new kind never needs an OpenFGA schema change; only a new type does, and then **both** [pkg/authz/openfgaschema/base.fga](/pkg/authz/openfgaschema/base.fga) and [ee/authz/openfgaschema/base.fga](/ee/authz/openfgaschema/base.fga) must be updated together.
- Prefer `CheckResources` + `ResourceDef` over the coarse `ViewAccess`/`EditAccess`/`AdminAccess` gates for new routes.
- Use `WildcardSelector` for `create`/`list`, `IDSelector` for instance operations, and a custom `SelectorFunc` when the request ID is not the FGA selector.
- Linking routes check **both** sides: peers via `AttachDetachSiblingResourceDef` (same verb on both), parent-child via a `BasicResourceDef` on the child (`create`/`delete`) paired with an `AttachDetachParentChildResourceDef` on the parent (`attach`/`detach`).
- Changing `ManagedRoleToTransactions` only affects organizations created afterwards — add a [pkg/sqlmigration](/pkg/sqlmigration) migration to backfill existing ones.

View File

@@ -11,6 +11,7 @@ We adhere to three primary style guides as our foundation:
We **recommend** (almost enforce) reviewing these guides before contributing to the codebase. They provide valuable insights into writing idiomatic Go code and will help you understand our approach to backend development. In addition, we have a few additional rules that make certain areas stricter than the above which can be found in area-specific files in this package:
- [Abstractions](abstractions.md) - When to introduce new types and intermediate representations
- [Authz](authz.md) - Authorization, roles, and access control
- [Errors](errors.md) - Structured error handling
- [Endpoint](endpoint.md) - HTTP endpoint patterns
- [Flagger](flagger.md) - Feature flag patterns

View File

@@ -519,7 +519,7 @@ func (module *module) getOrCreateAPIKey(ctx context.Context, orgID valuer.UUID,
if err != nil {
return "", err
}
err = module.serviceAccount.SetRoleByName(ctx, orgID, serviceAccount.ID, authtypes.SigNozViewerRoleName)
_, err = module.serviceAccount.SetRoleByName(ctx, orgID, serviceAccount.ID, authtypes.SigNozViewerRoleName)
if err != nil {
return "", err
}

View File

@@ -141,7 +141,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationServiceAccount(ctx context.Cont
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration service account: %w", err))
}
err = ah.Signoz.Modules.ServiceAccount.SetRoleByName(ctx, orgId, cloudIntegrationServiceAccount.ID, authtypes.SigNozViewerRoleName)
_, err = ah.Signoz.Modules.ServiceAccount.SetRoleByName(ctx, orgId, cloudIntegrationServiceAccount.ID, authtypes.SigNozViewerRoleName)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration service account: %w", err))
}

View File

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

View File

@@ -25,6 +25,7 @@ You are operating within a constrained context window and strict system prompts.
- Never create barrel files.
- When writing new css, prefer CSS Modules
- Use ./docs/css-modules-guide.md as reference on how to write good CSS Modules.
- When writing code that could need authorization checks, read ./src/lib/authz/README.md
3. FORCED VERIFICATION: Your internal tools mark file writes as successful even if the code does not compile. You are FORBIDDEN from reporting a task as complete until you have:
- Run `pnpm tsgo --noEmit`

View File

@@ -0,0 +1,3 @@
export const IS_DEV = false;
export const IS_PROD = true;
export const MODE = 'test';

View File

@@ -29,6 +29,7 @@ const config: Config.InitialOptions = {
'^constants/env$': '<rootDir>/__mocks__/env.ts',
'^src/constants/env$': '<rootDir>/__mocks__/env.ts',
'^@signozhq/icons$': '<rootDir>/__mocks__/signozhqIconsMock.tsx',
'^lib/env$': '<rootDir>/__mocks__/lib/env.ts',
'^test-mocks/(.*)$': '<rootDir>/__mocks__/$1',
'^react-syntax-highlighter/dist/esm/(.*)$':
'<rootDir>/node_modules/react-syntax-highlighter/dist/cjs/$1',

View File

@@ -79,6 +79,7 @@
"event-source-polyfill": "1.0.31",
"eventemitter3": "5.0.1",
"history": "4.10.1",
"html-to-image": "1.11.13",
"http-status-codes": "2.3.0",
"i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3",

View File

@@ -164,6 +164,9 @@ importers:
history:
specifier: 4.10.1
version: 4.10.1
html-to-image:
specifier: 1.11.13
version: 1.11.13
http-status-codes:
specifier: 2.3.0
version: 2.3.0
@@ -5451,6 +5454,9 @@ packages:
resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==}
engines: {node: '>=20.10'}
html-to-image@1.11.13:
resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==}
html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
@@ -14495,6 +14501,8 @@ snapshots:
html-tags@5.1.0: {}
html-to-image@1.11.13: {}
html-void-elements@3.0.0: {}
http-proxy-agent@5.0.0:

View File

@@ -62,6 +62,6 @@
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
"LLM_OBSERVABILITY_BASE": "SigNoz | LLM Observability",
"LLM_OBSERVABILITY_MODEL_PRICING": "SigNoz | Model Pricing"
}
"LLM_OBSERVABILITY_OVERVIEW": "SigNoz | LLM Observability Overview",
"LLM_OBSERVABILITY_CONFIGURATION": "SigNoz | LLM Observability Configuration"
}

View File

@@ -87,6 +87,6 @@
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
"LLM_OBSERVABILITY_BASE": "SigNoz | LLM Observability",
"LLM_OBSERVABILITY_MODEL_PRICING": "SigNoz | Model Pricing"
}
"LLM_OBSERVABILITY_OVERVIEW": "SigNoz | LLM Observability Overview",
"LLM_OBSERVABILITY_CONFIGURATION": "SigNoz | LLM Observability Configuration"
}

View File

@@ -329,10 +329,3 @@ export const LLMObservabilityPage = Loadable(
/* webpackChunkName: "LLM Observability Page" */ 'pages/LLMObservability'
),
);
export const LLMObservabilityModelPricingPage = Loadable(
() =>
import(
/* webpackChunkName: "LLM Observability Model Pricing Page" */ 'pages/LLMObservabilityModelPricing'
),
);

View File

@@ -24,7 +24,6 @@ import {
LicensePage,
ListAllALertsPage,
LLMObservabilityPage,
LLMObservabilityModelPricingPage,
LiveLogs,
Login,
Logs,
@@ -515,17 +514,24 @@ const routes: AppRoutes[] = [
isPrivate: true,
},
{
path: ROUTES.LLM_OBSERVABILITY_BASE,
path: ROUTES.LLM_OBSERVABILITY_ATTRIBUTE_MAPPING,
exact: true,
component: LLMObservabilityPage,
key: 'LLM_OBSERVABILITY_BASE',
key: 'LLM_OBSERVABILITY_ATTRIBUTE_MAPPING',
isPrivate: true,
},
{
path: ROUTES.LLM_OBSERVABILITY_MODEL_PRICING,
path: ROUTES.LLM_OBSERVABILITY_OVERVIEW,
exact: true,
component: LLMObservabilityModelPricingPage,
key: 'LLM_OBSERVABILITY_MODEL_PRICING',
component: LLMObservabilityPage,
key: 'LLM_OBSERVABILITY_OVERVIEW',
isPrivate: true,
},
{
path: ROUTES.LLM_OBSERVABILITY_CONFIGURATION,
exact: true,
component: LLMObservabilityPage,
key: 'LLM_OBSERVABILITY_CONFIGURATION',
isPrivate: true,
},
];

View File

@@ -269,3 +269,170 @@ export function mockQueryRangeV5WithEventsResponse({
),
);
}
export type MockTracesOptions = {
offset?: number;
pageSize?: number;
hasMore?: boolean;
delay?: number;
customTraces?: Array<{
serviceName?: string;
name?: string;
durationNano?: number;
httpMethod?: string;
responseStatusCode?: string;
}>;
onReceiveRequest?: (
req: RestRequest,
) =>
| undefined
| void
| Omit<MockTracesOptions, 'onReceiveRequest'>
| Promise<Omit<MockTracesOptions, 'onReceiveRequest'>>
| Promise<void>;
};
const createTracesResponse = ({
offset = 0,
pageSize = 10,
hasMore = true,
customTraces,
}: MockTracesOptions): MetricRangePayloadV5 => {
const serviceNames = ['frontend', 'backend', 'database', 'api-gateway'];
const spanNames = [
'GET /api/users',
'POST /api/orders',
'SELECT * FROM users',
];
const httpMethods = ['GET', 'POST', 'PUT', 'DELETE'];
const statusCodes = ['200', '201', '400', '404', '500'];
const rows = customTraces
? customTraces.map((trace, index) => {
const baseTimestamp = new Date('2026-04-21T17:54:33Z').getTime();
const currentTimestamp = new Date(baseTimestamp - index * 60000);
return {
timestamp: currentTimestamp.toISOString(),
data: {
serviceName:
trace.serviceName ?? serviceNames[index % serviceNames.length],
name: trace.name ?? spanNames[index % spanNames.length],
durationNano: trace.durationNano ?? 1000000 + index * 100000,
httpMethod: trace.httpMethod ?? httpMethods[index % httpMethods.length],
responseStatusCode:
trace.responseStatusCode ?? statusCodes[index % statusCodes.length],
traceID: `trace-id-${index}`,
spanID: `span-id-${index}`,
},
};
})
: Array.from(
{ length: hasMore ? pageSize : Math.ceil(pageSize / 2) },
(_, index) => {
const cumulativeIndex = offset + index;
const baseTimestamp = new Date('2026-04-21T17:54:33Z').getTime();
const currentTimestamp = new Date(baseTimestamp - cumulativeIndex * 60000);
return {
timestamp: currentTimestamp.toISOString(),
data: {
serviceName: serviceNames[cumulativeIndex % serviceNames.length],
name: spanNames[cumulativeIndex % spanNames.length],
durationNano: 1000000 + cumulativeIndex * 100000,
httpMethod: httpMethods[cumulativeIndex % httpMethods.length],
responseStatusCode: statusCodes[cumulativeIndex % statusCodes.length],
traceID: `trace-id-${cumulativeIndex}`,
spanID: `span-id-${cumulativeIndex}`,
},
};
},
);
return {
data: {
type: 'raw',
data: {
results: [
{
queryName: 'A',
nextCursor: hasMore ? 'next-cursor-token' : '',
rows,
},
],
},
meta: {
bytesScanned: 9682976,
durationMs: 295,
rowsScanned: 34198,
stepIntervals: { A: 170 },
},
},
};
};
export function mockQueryRangeV5WithTracesResponse({
hasMore = true,
offset = 0,
pageSize = 10,
delay = 0,
customTraces,
onReceiveRequest,
}: MockTracesOptions = {}): void {
server.use(
rest.post(QUERY_RANGE_URL, async (req, res, ctx) =>
res(
...(delay ? [ctx.delay(delay)] : []),
ctx.status(200),
ctx.json(
createTracesResponse(
(await onReceiveRequest?.(req)) ?? {
hasMore,
pageSize,
offset,
customTraces,
},
),
),
),
),
);
}
export function mockQueryRangeV5WithEmptyTraces(): void {
server.use(
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
data: {
type: 'raw',
data: {
results: [{ queryName: 'A', nextCursor: '', rows: [] }],
},
meta: {
bytesScanned: 0,
durationMs: 10,
rowsScanned: 0,
stepIntervals: {},
},
},
}),
),
),
);
}
export function mockQueryRangeV5WithKeyNotFoundError(): void {
server.use(
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
res(
ctx.status(400),
ctx.json({
error: {
code: 'invalid_input',
errors: [{ message: 'key not found' }],
},
}),
),
),
);
}

View File

@@ -21,6 +21,7 @@ import type {
GetChecks200,
GetChecksParams,
InframonitoringtypesPostableClustersDTO,
InframonitoringtypesPostableContainersDTO,
InframonitoringtypesPostableDaemonSetsDTO,
InframonitoringtypesPostableDeploymentsDTO,
InframonitoringtypesPostableHostsDTO,
@@ -31,6 +32,7 @@ import type {
InframonitoringtypesPostableStatefulSetsDTO,
InframonitoringtypesPostableVolumesDTO,
ListClusters200,
ListContainers200,
ListDaemonSets200,
ListDeployments200,
ListHosts200,
@@ -548,6 +550,89 @@ export const useListJobs = <
> => {
return useMutation(getListJobsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes containers with key kubeletstats metrics: CPU usage (cores), CPU request/limit utilization, memory working set, and memory request/limit utilization. Each container also reports health signals from the k8s_cluster receiver: status (kubectl-style display status derived from k8s.container.status.state + k8s.container.status.reason), restarts (absolute count from k8s.container.restarts), and ready (ready/not_ready from k8s.container.ready). The row identity is (k8s.pod.uid, k8s.container.name), stable across container restarts. Each container includes metadata attributes (k8s.container.name, k8s.pod.name, container.image.name, container.image.tag, k8s.namespace.name, k8s.node.name, k8s.cluster.name, and workload owner such as deployment/statefulset/daemonset/job). The response type is 'list' for the default (k8s.pod.uid, k8s.container.name) grouping (each row is one container with its current status and ready state) or 'grouped_list' for custom groupBy keys (each row aggregates containers in the group with per-status counts under containerCountsByStatus, per-readiness counts under containerCountsByReady, and restarts as the group sum). Status requires the optional k8s.container.status.state and k8s.container.status.reason metrics; when either is missing, status is omitted and a warning is returned while restarts and ready are still computed. Supports filtering via a filter expression, custom groupBy, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (cpu, cpuRequestUtilization, cpuLimitUtilization, memory, memoryRequestUtilization, memoryLimitUtilization) and restarts return -1 as a sentinel when no data is available for that field.
* @summary List Kubernetes Containers for Infra Monitoring
*/
export const listContainers = (
inframonitoringtypesPostableContainersDTO?: BodyType<InframonitoringtypesPostableContainersDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListContainers200>({
url: `/api/v2/infra_monitoring/kube_containers`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: inframonitoringtypesPostableContainersDTO,
signal,
});
};
export const getListContainersMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof listContainers>>,
TError,
{ data?: BodyType<InframonitoringtypesPostableContainersDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof listContainers>>,
TError,
{ data?: BodyType<InframonitoringtypesPostableContainersDTO> },
TContext
> => {
const mutationKey = ['listContainers'];
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 listContainers>>,
{ data?: BodyType<InframonitoringtypesPostableContainersDTO> }
> = (props) => {
const { data } = props ?? {};
return listContainers(data);
};
return { mutationFn, ...mutationOptions };
};
export type ListContainersMutationResult = NonNullable<
Awaited<ReturnType<typeof listContainers>>
>;
export type ListContainersMutationBody =
| BodyType<InframonitoringtypesPostableContainersDTO>
| undefined;
export type ListContainersMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List Kubernetes Containers for Infra Monitoring
*/
export const useListContainers = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof listContainers>>,
TError,
{ data?: BodyType<InframonitoringtypesPostableContainersDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof listContainers>>,
TError,
{ data?: BodyType<InframonitoringtypesPostableContainersDTO> },
TContext
> => {
return useMutation(getListContainersMutationOptions(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 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

View File

@@ -22,12 +22,16 @@ import type {
CreateServiceAccountKey201,
CreateServiceAccountKeyPathParameters,
CreateServiceAccountRole201,
CreateServiceAccountRolePathParameters,
CreateServiceAccountRoleDeprecated201,
CreateServiceAccountRoleDeprecatedPathParameters,
DeleteServiceAccountPathParameters,
DeleteServiceAccountRoleDeprecatedPathParameters,
DeleteServiceAccountRolePathParameters,
GetMyServiceAccount200,
GetServiceAccount200,
GetServiceAccountPathParameters,
GetServiceAccountRole200,
GetServiceAccountRolePathParameters,
GetServiceAccountRoles200,
GetServiceAccountRolesPathParameters,
ListServiceAccountKeys200,
@@ -35,6 +39,7 @@ import type {
ListServiceAccounts200,
RenderErrorResponseDTO,
RevokeServiceAccountKeyPathParameters,
ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO,
ServiceaccounttypesPostableFactorAPIKeyDTO,
ServiceaccounttypesPostableServiceAccountDTO,
ServiceaccounttypesPostableServiceAccountRoleDTO,
@@ -46,6 +51,272 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* This endpoint assigns a role to a service account
* @summary Create service account role
*/
export const createServiceAccountRole = (
serviceaccounttypesPostableServiceAccountRoleDTO?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateServiceAccountRole201>({
url: `/api/v1/service_account_roles`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: serviceaccounttypesPostableServiceAccountRoleDTO,
signal,
});
};
export const getCreateServiceAccountRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> },
TContext
> => {
const mutationKey = ['createServiceAccountRole'];
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 createServiceAccountRole>>,
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> }
> = (props) => {
const { data } = props ?? {};
return createServiceAccountRole(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateServiceAccountRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof createServiceAccountRole>>
>;
export type CreateServiceAccountRoleMutationBody =
| BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>
| undefined;
export type CreateServiceAccountRoleMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create service account role
*/
export const useCreateServiceAccountRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> },
TContext
> => {
return useMutation(getCreateServiceAccountRoleMutationOptions(options));
};
/**
* This endpoint revokes a role from a service account
* @summary Delete service account role
*/
export const deleteServiceAccountRole = (
{ id }: DeleteServiceAccountRolePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/service_account_roles/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteServiceAccountRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
> => {
const mutationKey = ['deleteServiceAccountRole'];
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 deleteServiceAccountRole>>,
{ pathParams: DeleteServiceAccountRolePathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteServiceAccountRole(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteServiceAccountRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteServiceAccountRole>>
>;
export type DeleteServiceAccountRoleMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete service account role
*/
export const useDeleteServiceAccountRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
> => {
return useMutation(getDeleteServiceAccountRoleMutationOptions(options));
};
/**
* This endpoint gets an existing service account role
* @summary Get service account role
*/
export const getServiceAccountRole = (
{ id }: GetServiceAccountRolePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetServiceAccountRole200>({
url: `/api/v1/service_account_roles/${id}`,
method: 'GET',
signal,
});
};
export const getGetServiceAccountRoleQueryKey = ({
id,
}: GetServiceAccountRolePathParameters) => {
return [`/api/v1/service_account_roles/${id}`] as const;
};
export const getGetServiceAccountRoleQueryOptions = <
TData = Awaited<ReturnType<typeof getServiceAccountRole>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetServiceAccountRolePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getServiceAccountRole>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetServiceAccountRoleQueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getServiceAccountRole>>
> = ({ signal }) => getServiceAccountRole({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getServiceAccountRole>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetServiceAccountRoleQueryResult = NonNullable<
Awaited<ReturnType<typeof getServiceAccountRole>>
>;
export type GetServiceAccountRoleQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get service account role
*/
export function useGetServiceAccountRole<
TData = Awaited<ReturnType<typeof getServiceAccountRole>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetServiceAccountRolePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getServiceAccountRole>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetServiceAccountRoleQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get service account role
*/
export const invalidateGetServiceAccountRole = async (
queryClient: QueryClient,
{ id }: GetServiceAccountRolePathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetServiceAccountRoleQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint lists the service accounts for an organisation
* @summary List service accounts
@@ -984,45 +1255,46 @@ export const invalidateGetServiceAccountRoles = async (
/**
* This endpoint assigns a role to a service account
* @deprecated
* @summary Create service account role
*/
export const createServiceAccountRole = (
{ id }: CreateServiceAccountRolePathParameters,
serviceaccounttypesPostableServiceAccountRoleDTO?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>,
export const createServiceAccountRoleDeprecated = (
{ id }: CreateServiceAccountRoleDeprecatedPathParameters,
serviceaccounttypesDeprecatedPostableServiceAccountRoleDTO?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateServiceAccountRole201>({
return GeneratedAPIInstance<CreateServiceAccountRoleDeprecated201>({
url: `/api/v1/service_accounts/${id}/roles`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: serviceaccounttypesPostableServiceAccountRoleDTO,
data: serviceaccounttypesDeprecatedPostableServiceAccountRoleDTO,
signal,
});
};
export const getCreateServiceAccountRoleMutationOptions = <
export const getCreateServiceAccountRoleDeprecatedMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountRole>>,
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
TError,
{
pathParams: CreateServiceAccountRolePathParameters;
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountRole>>,
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
TError,
{
pathParams: CreateServiceAccountRolePathParameters;
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
},
TContext
> => {
const mutationKey = ['createServiceAccountRole'];
const mutationKey = ['createServiceAccountRoleDeprecated'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
@@ -1032,62 +1304,66 @@ export const getCreateServiceAccountRoleMutationOptions = <
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createServiceAccountRole>>,
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
{
pathParams: CreateServiceAccountRolePathParameters;
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return createServiceAccountRole(pathParams, data);
return createServiceAccountRoleDeprecated(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateServiceAccountRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof createServiceAccountRole>>
export type CreateServiceAccountRoleDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>
>;
export type CreateServiceAccountRoleMutationBody =
| BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>
export type CreateServiceAccountRoleDeprecatedMutationBody =
| BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>
| undefined;
export type CreateServiceAccountRoleMutationError =
export type CreateServiceAccountRoleDeprecatedMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Create service account role
*/
export const useCreateServiceAccountRole = <
export const useCreateServiceAccountRoleDeprecated = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountRole>>,
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
TError,
{
pathParams: CreateServiceAccountRolePathParameters;
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createServiceAccountRole>>,
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
TError,
{
pathParams: CreateServiceAccountRolePathParameters;
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
},
TContext
> => {
return useMutation(getCreateServiceAccountRoleMutationOptions(options));
return useMutation(
getCreateServiceAccountRoleDeprecatedMutationOptions(options),
);
};
/**
* This endpoint revokes a role from service account
* @deprecated
* @summary Delete service account role
*/
export const deleteServiceAccountRole = (
{ id, rid }: DeleteServiceAccountRolePathParameters,
export const deleteServiceAccountRoleDeprecated = (
{ id, rid }: DeleteServiceAccountRoleDeprecatedPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
@@ -1097,23 +1373,23 @@ export const deleteServiceAccountRole = (
});
};
export const getDeleteServiceAccountRoleMutationOptions = <
export const getDeleteServiceAccountRoleDeprecatedMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
TContext
> => {
const mutationKey = ['deleteServiceAccountRole'];
const mutationKey = ['deleteServiceAccountRoleDeprecated'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
@@ -1123,44 +1399,47 @@ export const getDeleteServiceAccountRoleMutationOptions = <
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
{ pathParams: DeleteServiceAccountRolePathParameters }
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteServiceAccountRole(pathParams);
return deleteServiceAccountRoleDeprecated(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteServiceAccountRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteServiceAccountRole>>
export type DeleteServiceAccountRoleDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>
>;
export type DeleteServiceAccountRoleMutationError =
export type DeleteServiceAccountRoleDeprecatedMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Delete service account role
*/
export const useDeleteServiceAccountRole = <
export const useDeleteServiceAccountRoleDeprecated = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
TContext
> => {
return useMutation(getDeleteServiceAccountRoleMutationOptions(options));
return useMutation(
getDeleteServiceAccountRoleDeprecatedMutationOptions(options),
);
};
/**
* This endpoint gets my service account

View File

@@ -2048,6 +2048,13 @@ export interface AuthtypesAuthNSupportDTO {
password?: AuthtypesPasswordAuthNSupportDTO[] | null;
}
export interface AuthtypesDeprecatedPostableUserRoleDTO {
/**
* @type string
*/
id: string;
}
export interface AuthtypesGettableAuthDomainDTO {
authNProviderInfo?: AuthtypesAuthNProviderInfoDTO;
config?: AuthtypesAuthDomainConfigDTO;
@@ -2287,13 +2294,6 @@ export interface AuthtypesPostableRotateTokenDTO {
refreshToken?: string;
}
export interface AuthtypesPostableUserRoleDTO {
/**
* @type string
*/
id: string;
}
export interface AuthtypesPostableUserDTO {
/**
* @type string
@@ -2310,7 +2310,18 @@ export interface AuthtypesPostableUserDTO {
/**
* @type array
*/
userRoles?: AuthtypesPostableUserRoleDTO[];
userRoles?: AuthtypesDeprecatedPostableUserRoleDTO[];
}
export interface AuthtypesPostableUserRoleDTO {
/**
* @type string
*/
roleId: string;
/**
* @type string
*/
userId: string;
}
export interface AuthtypesRoleDTO {
@@ -5566,6 +5577,7 @@ export enum InframonitoringtypesCheckTypeDTO {
namespaces = 'namespaces',
clusters = 'clusters',
volumes = 'volumes',
kube_containers = 'kube_containers',
}
export interface InframonitoringtypesMissingMetricsComponentEntryDTO {
associatedComponent: InframonitoringtypesAssociatedComponentDTO;
@@ -5693,6 +5705,81 @@ export interface InframonitoringtypesPodCountsByPhaseDTO {
unknown: number;
}
export interface InframonitoringtypesPodCountsByStatusDTO {
/**
* @type integer
*/
completed: number;
/**
* @type integer
*/
containerCannotRun: number;
/**
* @type integer
*/
containerCreating: number;
/**
* @type integer
*/
crashLoopBackOff: number;
/**
* @type integer
*/
createContainerConfigError: number;
/**
* @type integer
*/
errImagePull: number;
/**
* @type integer
*/
error: number;
/**
* @type integer
*/
evicted: number;
/**
* @type integer
*/
failed: number;
/**
* @type integer
*/
imagePullBackOff: number;
/**
* @type integer
*/
nodeAffinity: number;
/**
* @type integer
*/
nodeLost: number;
/**
* @type integer
*/
oomKilled: number;
/**
* @type integer
*/
pending: number;
/**
* @type integer
*/
running: number;
/**
* @type integer
*/
shutdown: number;
/**
* @type integer
*/
unexpectedAdmissionError: number;
/**
* @type integer
*/
unknown: number;
}
export interface InframonitoringtypesClusterRecordDTO {
/**
* @type number
@@ -5724,6 +5811,7 @@ export interface InframonitoringtypesClusterRecordDTO {
meta: InframonitoringtypesClusterRecordDTOMeta;
nodeCountsByReadiness: InframonitoringtypesNodeCountsByReadinessDTO;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
}
export enum InframonitoringtypesResponseTypeDTO {
@@ -5769,6 +5857,174 @@ export interface InframonitoringtypesClustersDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesContainerCountsByReadyDTO {
/**
* @type integer
*/
notReady: number;
/**
* @type integer
*/
ready: number;
}
export interface InframonitoringtypesContainerCountsByStatusDTO {
/**
* @type integer
*/
completed: number;
/**
* @type integer
*/
containerCannotRun: number;
/**
* @type integer
*/
containerCreating: number;
/**
* @type integer
*/
crashLoopBackOff: number;
/**
* @type integer
*/
createContainerConfigError: number;
/**
* @type integer
*/
errImagePull: number;
/**
* @type integer
*/
error: number;
/**
* @type integer
*/
imagePullBackOff: number;
/**
* @type integer
*/
oomKilled: number;
/**
* @type integer
*/
running: number;
/**
* @type integer
*/
terminated: number;
/**
* @type integer
*/
unknown: number;
/**
* @type integer
*/
waiting: number;
}
export enum InframonitoringtypesContainerReadyDTO {
ready = 'ready',
not_ready = 'not_ready',
no_data = 'no_data',
}
export type InframonitoringtypesContainerRecordDTOMetaAnyOf = {
[key: string]: string;
};
/**
* @nullable
*/
export type InframonitoringtypesContainerRecordDTOMeta =
InframonitoringtypesContainerRecordDTOMetaAnyOf | null;
export enum InframonitoringtypesContainerStatusDTO {
running = 'running',
waiting = 'waiting',
terminated = 'terminated',
crashloopbackoff = 'crashloopbackoff',
imagepullbackoff = 'imagepullbackoff',
errimagepull = 'errimagepull',
createcontainerconfigerror = 'createcontainerconfigerror',
containercreating = 'containercreating',
oomkilled = 'oomkilled',
completed = 'completed',
error = 'error',
containercannotrun = 'containercannotrun',
unknown = 'unknown',
no_data = 'no_data',
}
export interface InframonitoringtypesContainerRecordDTO {
containerCountsByReady: InframonitoringtypesContainerCountsByReadyDTO;
containerCountsByStatus: InframonitoringtypesContainerCountsByStatusDTO;
/**
* @type string
*/
containerName: string;
/**
* @type number
* @format double
*/
cpu: number;
/**
* @type number
* @format double
*/
cpuLimitUtilization: number;
/**
* @type number
* @format double
*/
cpuRequestUtilization: number;
/**
* @type number
* @format double
*/
memory: number;
/**
* @type number
* @format double
*/
memoryLimitUtilization: number;
/**
* @type number
* @format double
*/
memoryRequestUtilization: number;
/**
* @type object,null
*/
meta: InframonitoringtypesContainerRecordDTOMeta;
/**
* @type string
*/
podUID: string;
ready: InframonitoringtypesContainerReadyDTO;
/**
* @type integer
* @format int64
*/
restarts: number;
status: InframonitoringtypesContainerStatusDTO;
}
export interface InframonitoringtypesContainersDTO {
/**
* @type boolean
*/
endTimeBeforeRetention: boolean;
/**
* @type array
*/
records: InframonitoringtypesContainerRecordDTO[];
/**
* @type integer
*/
total: number;
type: InframonitoringtypesResponseTypeDTO;
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export type InframonitoringtypesDaemonSetRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -5827,6 +6083,7 @@ export interface InframonitoringtypesDaemonSetRecordDTO {
*/
meta: InframonitoringtypesDaemonSetRecordDTOMeta;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
}
export interface InframonitoringtypesDaemonSetsDTO {
@@ -5904,6 +6161,7 @@ export interface InframonitoringtypesDeploymentRecordDTO {
*/
meta: InframonitoringtypesDeploymentRecordDTOMeta;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
}
export interface InframonitoringtypesDeploymentsDTO {
@@ -6070,6 +6328,7 @@ export interface InframonitoringtypesJobRecordDTO {
*/
meta: InframonitoringtypesJobRecordDTOMeta;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
/**
* @type integer
*/
@@ -6123,6 +6382,7 @@ export interface InframonitoringtypesNamespaceRecordDTO {
*/
namespaceName: string;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
}
export interface InframonitoringtypesNamespacesDTO {
@@ -6189,6 +6449,7 @@ export interface InframonitoringtypesNodeRecordDTO {
*/
nodeName: string;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
}
export interface InframonitoringtypesNodesDTO {
@@ -6226,6 +6487,27 @@ export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
export type InframonitoringtypesPodRecordDTOMeta =
InframonitoringtypesPodRecordDTOMetaAnyOf | null;
export enum InframonitoringtypesPodStatusDTO {
pending = 'pending',
running = 'running',
failed = 'failed',
unknown = 'unknown',
crashloopbackoff = 'crashloopbackoff',
imagepullbackoff = 'imagepullbackoff',
errimagepull = 'errimagepull',
createcontainerconfigerror = 'createcontainerconfigerror',
containercreating = 'containercreating',
oomkilled = 'oomkilled',
completed = 'completed',
error = 'error',
containercannotrun = 'containercannotrun',
evicted = 'evicted',
nodeaffinity = 'nodeaffinity',
nodelost = 'nodelost',
shutdown = 'shutdown',
unexpectedadmissionerror = 'unexpectedadmissionerror',
no_data = 'no_data',
}
export interface InframonitoringtypesPodRecordDTO {
/**
* @type object,null
@@ -6252,6 +6534,7 @@ export interface InframonitoringtypesPodRecordDTO {
*/
podCPURequest: number;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
/**
* @type number
* @format double
@@ -6268,6 +6551,12 @@ export interface InframonitoringtypesPodRecordDTO {
*/
podMemoryRequest: number;
podPhase: InframonitoringtypesPodPhaseDTO;
/**
* @type integer
* @format int64
*/
podRestarts: number;
podStatus: InframonitoringtypesPodStatusDTO;
/**
* @type string
*/
@@ -6318,6 +6607,33 @@ export interface InframonitoringtypesPostableClustersDTO {
start: number;
}
export interface InframonitoringtypesPostableContainersDTO {
/**
* @type integer
* @format int64
*/
end: number;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
groupBy?: Querybuildertypesv5GroupByKeyDTO[] | null;
/**
* @type integer
*/
limit: number;
/**
* @type integer
*/
offset?: number;
orderBy?: Querybuildertypesv5OrderByDTO;
/**
* @type integer
* @format int64
*/
start: number;
}
export interface InframonitoringtypesPostableDaemonSetsDTO {
/**
* @type integer
@@ -6585,6 +6901,7 @@ export interface InframonitoringtypesStatefulSetRecordDTO {
*/
meta: InframonitoringtypesStatefulSetRecordDTOMeta;
podCountsByPhase: InframonitoringtypesPodCountsByPhaseDTO;
podCountsByStatus: InframonitoringtypesPodCountsByStatusDTO;
/**
* @type number
* @format double
@@ -8508,6 +8825,13 @@ export interface RuletypesRuleDTO {
export enum RuletypesThresholdKindDTO {
basic = 'basic',
}
export interface ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO {
/**
* @type string
*/
id: string;
}
export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
/**
* @type string
@@ -8577,7 +8901,11 @@ export interface ServiceaccounttypesPostableServiceAccountRoleDTO {
/**
* @type string
*/
id: string;
roleId: string;
/**
* @type string
*/
serviceAccountId: string;
}
export interface ServiceaccounttypesServiceAccountDTO {
@@ -10343,6 +10671,28 @@ export type UpdateRoutePolicy200 = {
status: string;
};
export type CreateServiceAccountRole201 = {
data: TypesIdentifiableDTO;
/**
* @type string
*/
status: string;
};
export type DeleteServiceAccountRolePathParameters = {
id: string;
};
export type GetServiceAccountRolePathParameters = {
id: string;
};
export type GetServiceAccountRole200 = {
data: ServiceaccounttypesServiceAccountRoleDTO;
/**
* @type string
*/
status: string;
};
export type ListServiceAccounts200 = {
/**
* @type array
@@ -10426,10 +10776,10 @@ export type GetServiceAccountRoles200 = {
status: string;
};
export type CreateServiceAccountRolePathParameters = {
export type CreateServiceAccountRoleDeprecatedPathParameters = {
id: string;
};
export type CreateServiceAccountRole201 = {
export type CreateServiceAccountRoleDeprecated201 = {
data: TypesIdentifiableDTO;
/**
* @type string
@@ -10437,7 +10787,7 @@ export type CreateServiceAccountRole201 = {
status: string;
};
export type DeleteServiceAccountRolePathParameters = {
export type DeleteServiceAccountRoleDeprecatedPathParameters = {
id: string;
rid: string;
};
@@ -10887,6 +11237,14 @@ export type ListJobs200 = {
status: string;
};
export type ListContainers200 = {
data: InframonitoringtypesContainersDTO;
/**
* @type string
*/
status: string;
};
export type ListNamespaces200 = {
data: InframonitoringtypesNamespacesDTO;
/**
@@ -11597,6 +11955,28 @@ export type RotateSession200 = {
status: string;
};
export type CreateUserRole201 = {
data: TypesIdentifiableDTO;
/**
* @type string
*/
status: string;
};
export type DeleteUserRolePathParameters = {
id: string;
};
export type GetUserRolePathParameters = {
id: string;
};
export type GetUserRole200 = {
data: AuthtypesUserRoleDTO;
/**
* @type string
*/
status: string;
};
export type ListUsers200 = {
/**
* @type array

View File

@@ -19,12 +19,15 @@ import type {
import type {
AuthtypesPostableUserDTO,
AuthtypesPostableUserRoleDTO,
CreateInvite201,
CreateResetPasswordToken201,
CreateResetPasswordTokenPathParameters,
CreateUser201,
CreateUserRole201,
DeleteUserDeprecatedPathParameters,
DeleteUserPathParameters,
DeleteUserRolePathParameters,
GetMyUser200,
GetMyUserDeprecated200,
GetResetPasswordToken200,
@@ -37,6 +40,8 @@ import type {
GetUserDeprecated200,
GetUserDeprecatedPathParameters,
GetUserPathParameters,
GetUserRole200,
GetUserRolePathParameters,
GetUsersByRoleID200,
GetUsersByRoleIDPathParameters,
ListUsers200,
@@ -1154,6 +1159,267 @@ export const invalidateGetUsersByRoleID = async (
return queryClient;
};
/**
* This endpoint assigns a role to a user
* @summary Create user role
*/
export const createUserRole = (
authtypesPostableUserRoleDTO?: BodyType<AuthtypesPostableUserRoleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateUserRole201>({
url: `/api/v2/user_roles`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: authtypesPostableUserRoleDTO,
signal,
});
};
export const getCreateUserRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createUserRole>>,
TError,
{ data?: BodyType<AuthtypesPostableUserRoleDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createUserRole>>,
TError,
{ data?: BodyType<AuthtypesPostableUserRoleDTO> },
TContext
> => {
const mutationKey = ['createUserRole'];
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 createUserRole>>,
{ data?: BodyType<AuthtypesPostableUserRoleDTO> }
> = (props) => {
const { data } = props ?? {};
return createUserRole(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateUserRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof createUserRole>>
>;
export type CreateUserRoleMutationBody =
| BodyType<AuthtypesPostableUserRoleDTO>
| undefined;
export type CreateUserRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create user role
*/
export const useCreateUserRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createUserRole>>,
TError,
{ data?: BodyType<AuthtypesPostableUserRoleDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createUserRole>>,
TError,
{ data?: BodyType<AuthtypesPostableUserRoleDTO> },
TContext
> => {
return useMutation(getCreateUserRoleMutationOptions(options));
};
/**
* This endpoint revokes a role from a user
* @summary Delete user role
*/
export const deleteUserRole = (
{ id }: DeleteUserRolePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/user_roles/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteUserRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteUserRole>>,
TError,
{ pathParams: DeleteUserRolePathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteUserRole>>,
TError,
{ pathParams: DeleteUserRolePathParameters },
TContext
> => {
const mutationKey = ['deleteUserRole'];
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 deleteUserRole>>,
{ pathParams: DeleteUserRolePathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteUserRole(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteUserRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteUserRole>>
>;
export type DeleteUserRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete user role
*/
export const useDeleteUserRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteUserRole>>,
TError,
{ pathParams: DeleteUserRolePathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteUserRole>>,
TError,
{ pathParams: DeleteUserRolePathParameters },
TContext
> => {
return useMutation(getDeleteUserRoleMutationOptions(options));
};
/**
* This endpoint gets an existing user role
* @summary Get user role
*/
export const getUserRole = (
{ id }: GetUserRolePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetUserRole200>({
url: `/api/v2/user_roles/${id}`,
method: 'GET',
signal,
});
};
export const getGetUserRoleQueryKey = ({ id }: GetUserRolePathParameters) => {
return [`/api/v2/user_roles/${id}`] as const;
};
export const getGetUserRoleQueryOptions = <
TData = Awaited<ReturnType<typeof getUserRole>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetUserRolePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getUserRole>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetUserRoleQueryKey({ id });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getUserRole>>> = ({
signal,
}) => getUserRole({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getUserRole>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetUserRoleQueryResult = NonNullable<
Awaited<ReturnType<typeof getUserRole>>
>;
export type GetUserRoleQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get user role
*/
export function useGetUserRole<
TData = Awaited<ReturnType<typeof getUserRole>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetUserRolePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getUserRole>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetUserRoleQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get user role
*/
export const invalidateGetUserRole = async (
queryClient: QueryClient,
{ id }: GetUserRolePathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetUserRoleQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint lists all users for the organization
* @summary List users v2
@@ -1865,6 +2131,7 @@ export const invalidateGetRolesByUserID = async (
/**
* This endpoint assigns the role to the user roles by user id
* @deprecated
* @summary Set user roles
*/
export const setRoleByUserID = (
@@ -1936,6 +2203,7 @@ export type SetRoleByUserIDMutationBody =
export type SetRoleByUserIDMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Set user roles
*/
export const useSetRoleByUserID = <
@@ -1964,6 +2232,7 @@ export const useSetRoleByUserID = <
};
/**
* This endpoint removes a role from the user by user id and role id
* @deprecated
* @summary Remove a role from user
*/
export const removeUserRoleByUserIDAndRoleID = (
@@ -2022,6 +2291,7 @@ export type RemoveUserRoleByUserIDAndRoleIDMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Remove a role from user
*/
export const useRemoveUserRoleByUserIDAndRoleID = <

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#1287b1" viewBox="0 0 24 24"><title>Apache Cassandra</title><path d="M10.374 10.53a3 3 0 0 1-.428-.222l.555.143c0 .02-.01.036-.01.055l-.117.025zm-.283 1.506-.315.253.852-1.079-1.078.391c.002.017.009.033.009.05a.57.57 0 0 1-.184.42q.154.327.375.616a3.2 3.2 0 0 1 .34-.651zm.717-2.347-.652-.82a.43.43 0 0 1-.506.162c-.054.073-.083.162-.13.24l1.258.463zm-1.666.444c-.07.314-.087.637-.05.956a.57.57 0 0 1 .451.475l.946-.606c-.067-.022-.126-.06-.191-.088l-1.119-.08.64-.14a3.2 3.2 0 0 1-.668-.554zM20.1 11.648c-.164.202.833 1.022.833 1.022s-1.654-1.022-2.234-.72c-.278.144.574.811 1.175 1.242-.428-.274-.982-.571-1.175-.408-.328.277 1.565 2.549 1.565 2.549s-2.145-2.322-2.36-2.209c-.214.114.593 1.224.593 1.224s-1.06-1.16-1.35-.959c-.29.202 1.514 3.218 1.514 3.218s-1.956-3.091-2.763-2.574c1.268 2.782.795 3.18.795 3.18s-.162-2.839-1.742-2.764c-.795.038.379 2.12.379 2.12s-1.08-1.902-1.8-1.864c1.326 2.51.854 3.53.854 3.53s.219-2.143-1.58-3.336c.682.606-.427 3.336-.427 3.336s.976-4.023-.719-3.256c-.268.121-.019 2.007-.019 2.007s-.34-2.158-.851-2.045c-.298.066-1.893 2.99-1.893 2.99s1.306-3.16.908-3.027c-.29.096-.833 1.4-.833 1.4s.265-1.287 0-1.363c-.264-.075-1.74 1.363-1.74 1.363s1.097-1.287.908-1.552c-.287-.402-.623-.42-1.022-.265-.581.226-1.363 1.287-1.363 1.287s.78-1.074.643-1.476c-.219-.647-2.46 1.249-2.46 1.249s1.325-1.25 1.022-1.514c-.303-.265-1.947-.183-2.46-.185-1.515-.004-2.039-.36-2.498-.724 1.987.997 3.803-.151 6.094.494l.21.06c-1.3-.558-2.144-1.378-2.226-2.354-.036-.416.074-.827.297-1.222.619-.4 1.29-.773 2.06-1.095a4 4 0 0 0-.064.698c0 2.44 2.203 4.417 4.92 4.417s4.92-1.977 4.92-4.417c0-.45-.083-.881-.223-1.29 1.431.404 2.45.968 3.132 1.335.022.092.045.184.053.279.024.274-.018.547-.11.814.095-.147.198-.288.28-.445.367-.997 1.855.227 1.855.227s-1.085-.454-1.06-.24c.026.215 1.628.96 1.628.96s-1.45-.455-1.362-.114c.088.34 1.817 1.703 1.817 1.703s-1.956-1.489-2.12-1.287zm-7.268 2.65.042-.008-.06.01zM9.256 9.753c.12.13.26.234.396.343l.927-.029-1.064-.788c-.093.154-.195.303-.26.474Zm10.62 3.44c.3.215.54.373.54.373s-.24-.181-.54-.374zM7.507 8.617c-.14.229-.214.492-.215.76a3.99 3.99 0 0 0 2.358 3.64q0-.008.003-.014a3.2 3.2 0 0 1-.58-.788c-.648.099-.926-.794-.336-1.08a3.2 3.2 0 0 1 .138-1.388 3.16 3.16 0 0 1-.52-1.36q-.444.105-.848.23m1.488.82q.163-.36.402-.661a.435.435 0 0 1 .568-.557c.077-.059.166-.099.248-.15a16 16 0 0 0-1.727.284c.114.388.272.76.509 1.084m2.285 3.928c1.4 0 2.633-.723 3.344-1.816a3.4 3.4 0 0 0-1.265-.539l-.297-.023.916.9-1.197-.467.704 1.078-1.074-.832-.012.006.347 1.278-.596-1.134-.098 1.33-.401-1.326-.472 1.261.114-1.359-.015-.008-.814 1.154.286-1.067c-.34.322-.605.713-.781 1.146q.143.153.303.29.484.126 1.008.128m10.145-4.434c.971-.567 1.716-1.955 1.716-1.955s-1.893 1.955-3.205 1.665c1.186-.934 1.766-2.549 1.766-2.549s-1.506 2.325-2.448 2.423c1.086-.959 1.54-2.322 1.54-2.322s-1.237 1.817-2.196 1.944c1.287-1.161 1.338-1.893 1.338-1.893s-1.781 2.302-2.499 1.943c.858-.934 1.439-2.12 1.439-2.12s-1.489 2.019-1.893 1.69c-.277-.05.454-.958.454-.958s-.908.807-1.16.606c.454-.278 1.236-1.64 1.236-1.64S16 7.505 15.621 7.304l.731-1.483s-.73 1.483-1.715 1.23c.454-.58.63-1.112.63-1.112s-.756 1.213-1.69.885c-.22-.077.273-.635.273-.635s-.626.61-1.055.534c-.43-.076.025-.858.025-.858s-.757 1.186-.908 1.136.075-.833.075-.833-.555.908-.858.858 0-.934 0-.934-.328.984-.58.909c-.252-.076-.303-.656-.303-.656s-.068.788-.429.858c-2.725.53-5.728 1.69-9.489 5.45C3.887 10.738 5.3 7.91 11.962 7.659c5.044-.191 7.399 2.137 8.177 2.17C22.51 9.93 24 7.633 24 7.633s-1.489 1.716-2.574 1.3zm-7.74.872-.608.464v.001l.054.003a3.4 3.4 0 0 0 .554-.468m1.583-.426c0-.536-.237-.929-.594-1.217a3.2 3.2 0 0 1-.165.825.393.393 0 0 1-.328.681c-.154.233-.34.445-.549.63l.661.034-.995.237c-.025.018-.045.041-.07.058a3.2 3.2 0 0 1 1.536.691c.32-.574.504-1.235.504-1.94zM10.99 7.996a3.5 3.5 0 0 0-.785.46.43.43 0 0 1-.013.357l.885.643.023-.016-.36-1.262.627 1.12c.018-.006.04-.006.058-.011l-.02-1.251.398 1.163.477-1.15.016 1.268.012.007.713-1.005-.363 1.218.009.01 1.04-.69-.759 1.05.002.005.95-.34.041-.045a.395.395 0 0 1 .394-.632 3.4 3.4 0 0 0 .27-.784 14 14 0 0 0-2.798-.168c-.286.011-.55.033-.817.053"/></svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="48 -2.25 262.5 364"><defs><style>.cls-1{fill:#326ce5}.cls-3{fill:none}</style></defs><path d="M59.724 97.778a10.183 10.183 0 0 1 0-17.073l114.07-74.16a10.18 10.18 0 0 1 11.1 0l114.07 74.16a10.183 10.183 0 0 1 0 17.073l-114.07 74.16a10.18 10.18 0 0 1-11.1 0z" class="cls-1"/><path fill="#c1d2f7" d="M197.356 110.866h7.912a6.003 6.003 0 0 0 5.2-9.005l-25.924-44.902a6.004 6.004 0 0 0-10.399 0l-25.924 44.902a6.003 6.003 0 0 0 5.2 9.005h7.912a6.003 6.003 0 0 1 6.004 6.003v51.257l5.31 3.452a12.29 12.29 0 0 0 13.395 0l5.31-3.452v-51.257a6.003 6.003 0 0 1 6.004-6.003"/><path d="M173.793 353.271a10.1 10.1 0 0 0 3.457 1.402 119 119 0 0 0-6.623-3.46zm-6.456-161.195-11.316 7.356a111 111 0 0 0 11.316 6.6zm24.015 23.716c9.739 3.115 19.813 5.648 30.11 8.186 10.92 2.692 21.973 5.427 32.818 9.009l-35.416-23.025a216 216 0 0 1-27.512-9.41zm0 62.191v.586c0 2.57-2.688 4.652-6.003 4.652H173.34c-3.315 0-6.003-2.083-6.003-4.652v-6.605c-24.906-6.402-49.873-14.485-70.86-33.82l-14.529 9.445c22.468 22.05 50.28 28.933 79.715 36.189 27.178 6.698 55.18 13.651 78.685 33.442l14.68-9.544c-18.532-16.504-40.41-23.687-63.676-29.693M93.315 300.95l38.908 25.295c20.064 5.113 40.2 11.248 58.31 23.36l14.901-9.687c-19.752-13.672-42.55-19.313-66.419-25.197-15.287-3.767-30.835-7.616-45.7-13.771m74.022-82.254a124.3 124.3 0 0 1-21.534-12.62l-14.805 9.625a124.5 124.5 0 0 0 36.339 18.708zm51.41 16.294c-9.106-2.245-18.304-4.524-27.395-7.299v13.682q5.051 1.286 10.177 2.538c29.934 7.379 60.888 15.01 85.708 39.829.61.61 1.162 1.24 1.752 1.857l9.974-6.484a10.1 10.1 0 0 0 3.475-3.8 101 101 0 0 0-3.289-3.486c-22.585-22.585-50.669-29.508-80.402-36.837m-19.933 19.933q-3.722-.918-7.462-1.855v13.21c25.817 6.561 51.803 14.688 73.455 35.04l14.521-9.44-.11-.118c-22.586-22.585-50.67-29.509-80.404-36.837m-31.477-8.587c-16.28-5.278-32.118-12.397-46.344-24.131l-14.682 9.545c17.87 15.557 38.784 22.673 61.026 28.5z" class="cls-3"/><path d="M73.24 254.96c-.348-.348-.658-.71-1-1.06l-12.514 8.136a10 10 0 0 0-1.644 1.397c1.053 1.157 2.115 2.31 3.245 3.44 22.585 22.585 50.67 29.509 80.403 36.837 25.343 6.247 51.401 12.717 73.865 29.603l14.806-9.626c-20.865-16.42-45.53-22.509-71.453-28.898-29.934-7.379-60.888-15.01-85.708-39.83" class="cls-3"/><path d="m218.864 209.962-27.512-17.886v8.476a216 216 0 0 0 27.512 9.41m-27.512 5.83v11.9c9.091 2.774 18.289 5.053 27.395 7.298 29.733 7.329 57.817 14.252 80.402 36.837a101 101 0 0 1 3.29 3.486 10.193 10.193 0 0 0-3.476-13.277l-44.683-29.05c-10.845-3.58-21.898-6.316-32.818-9.008-10.297-2.538-20.371-5.071-30.11-8.186m-24.015-9.76a111 111 0 0 1-11.316-6.6l-10.218 6.644a124.3 124.3 0 0 0 21.534 12.62zm34.192 37.88q-5.125-1.264-10.177-2.539v11.695q3.737.938 7.462 1.855c29.733 7.328 57.818 14.252 80.403 36.837.039.039.073.079.111.118l9.66-6.28c-.589-.618-1.142-1.248-1.751-1.858-24.82-24.82-55.774-32.45-85.708-39.829m-34.192-9.503a124.5 124.5 0 0 1-36.34-18.708l-10.004 6.504c14.226 11.734 30.064 18.853 46.344 24.13zm0 25.842c-22.242-5.828-43.156-12.944-61.026-28.5l-9.834 6.392c20.987 19.336 45.954 27.42 70.86 33.821zm24.015 17.732c23.266 6.006 45.144 13.19 63.675 29.693l9.78-6.358c-21.652-20.352-47.638-28.479-73.455-35.04zM81.948 247.59l-9.707 6.311c.341.35.652.712 1 1.06 24.82 24.82 55.773 32.45 85.707 39.829 25.922 6.39 50.588 12.478 71.452 28.898l9.948-6.467c-23.505-19.791-51.507-26.744-78.685-33.442-29.436-7.256-57.247-14.138-79.715-36.189m-20.621 19.284c-1.13-1.13-2.192-2.283-3.245-3.44a10.154 10.154 0 0 0 1.644 15.68l33.589 21.837c14.865 6.155 30.413 10.004 45.7 13.771 23.869 5.884 46.667 11.525 66.419 25.197l10.16-6.605c-22.463-16.886-48.521-23.356-73.864-29.603-29.733-7.328-57.818-14.252-80.403-36.837m109.3 84.34a119 119 0 0 1 6.623 3.46 10.16 10.16 0 0 0 7.645-1.402l5.639-3.666c-18.11-12.112-38.247-18.247-58.311-23.36z" class="cls-1"/></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"><title>PlanetScale</title><path d="M0 12C0 5.373 5.373 0 12 0c4.873 0 9.067 2.904 10.947 7.077l-15.87 15.87a12 12 0 0 1-1.935-1.099L14.99 12H12l-8.485 8.485A11.96 11.96 0 0 1 0 12m12.004 12L24 12.004C23.998 18.628 18.628 23.998 12.004 24"/></svg>

After

Width:  |  Height:  |  Size: 326 B

View File

@@ -3,7 +3,7 @@ import { Button, Popover, Tooltip } from 'antd';
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
import { Typography } from '@signozhq/ui/typography';
import { TelemetryFieldKey } from 'api/v5/v5';
import { useExportRawData } from 'hooks/useExportData/useServerExport';
import { useExportRawData } from 'hooks/useDownloadOptionsMenu/useDownloadOptionsMenu';
import { Download, LoaderCircle } from '@signozhq/icons';
import { DataSource } from 'types/common/queryBuilder';

View File

@@ -12,6 +12,8 @@ type BadgeColor =
interface HttpStatusBadgeProps {
statusCode: string | number;
testId?: string;
className?: string;
}
function getStatusCodeColor(statusCode: number): BadgeColor {
@@ -35,6 +37,8 @@ function getStatusCodeColor(statusCode: number): BadgeColor {
function HttpStatusBadge({
statusCode,
testId,
className,
}: HttpStatusBadgeProps): JSX.Element | null {
const numericStatusCode = Number(statusCode);
@@ -45,7 +49,12 @@ function HttpStatusBadge({
const color = getStatusCodeColor(numericStatusCode);
return (
<Badge color={color} variant="outline">
<Badge
color={color}
variant="outline"
data-testid={testId}
className={className}
>
{statusCode}
</Badge>
);

View File

@@ -20,9 +20,9 @@
padding: 0px 8px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--l2-border);
background: var(--l2-background);
color: var(--l2-foreground);
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
background: var(--input-with-label-background-color, var(--l2-background));
color: var(--input-with-label-color, var(--l2-foreground));
display: flex;
justify-content: flex-start;
@@ -35,21 +35,54 @@
min-width: 150px;
font-family: 'Space Mono', monospace !important;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
--input-border-radius: 0px;
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
background: var(--input-with-label-background-color, var(--l2-background));
color: var(--input-with-label-color, var(--l2-foreground));
border-right: none;
border-left: none;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
font-size: 12px !important;
line-height: 27px;
line-height: 25px;
position: relative;
&:hover,
&:focus {
z-index: 1;
}
.ant-select-selector {
position: relative;
border-radius: inherit;
}
.ant-select:hover .ant-select-selector,
.ant-select-focused .ant-select-selector {
z-index: 1;
}
&.input__has-label-after {
margin-left: -1px;
.ant-select-selector {
margin-left: -1px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
&.input__has-close-button {
.ant-select-selector {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
~ .close-btn {
margin-left: -1px;
}
}
&::placeholder {
color: var(--l2-foreground) !important;
color: var(--input-with-label-color, var(--l3-foreground)) !important;
font-size: 12px !important;
}
&[type='number']::-webkit-inner-spin-button,
@@ -63,25 +96,35 @@
.close-btn {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--l2-border);
background: var(--l2-background);
height: 38px;
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
background: var(--input-with-label-background-color, var(--l2-background));
height: 100%;
width: 38px;
position: relative;
&:hover,
&:focus {
z-index: 2;
}
}
&.labelAfter {
.input {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
border-radius: 2px;
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
background: var(--input-with-label-background-color, var(--l2-background));
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
.ant-select-selector {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
.label {
border-left: none;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
border-radius: 0px 2px 2px 0px;
}
}
}

View File

@@ -45,7 +45,10 @@ function InputWithLabel({
>
{!labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
<Input
className="input"
className={cx('input', {
'input__has-label-after': !labelAfter,
'input__has-close-button': !!onClose,
})}
placeholder={placeholder}
type={type}
value={inputValue}

View File

@@ -80,8 +80,8 @@
width: 1px;
background: repeating-linear-gradient(
to bottom,
var(--l1-border),
var(--l1-border) 4px,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
transparent 4px,
transparent 8px
);
@@ -101,7 +101,8 @@
top: 12px;
width: 6px;
height: 6px;
border-left: 6px dotted var(--l1-border);
border-left: 6px dotted
var(--query-builder-v2-border-color, var(--l2-border));
}
/* Horizontal line pointing from vertical to the item */
@@ -114,8 +115,8 @@
height: 1px;
background: repeating-linear-gradient(
to right,
var(--l1-border),
var(--l1-border) 4px,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
transparent 4px,
transparent 8px
);
@@ -241,7 +242,8 @@
top: 12px;
width: 6px;
height: 6px;
border-left: 6px dotted var(--l1-border);
border-left: 6px dotted
var(--query-builder-v2-border-color, var(--l2-border));
}
/* Horizontal line pointing from vertical to the item */
@@ -254,8 +256,8 @@
height: 1px;
background: repeating-linear-gradient(
to right,
var(--l1-border),
var(--l1-border) 4px,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
transparent 4px,
transparent 8px
);
@@ -273,6 +275,16 @@
line-height: 16px; /* 128.571% */
resize: none;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
&:placeholder {
color: var(--query-builder-v2-placeholder-color, var(--l3-foreground));
}
}
.formula-legend {
@@ -282,15 +294,42 @@
.ant-input-group-addon {
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
background: var(--l2-background);
color: var(--l2-foreground);
background: var(
--query-builder-v2-background-color,
var(--l2-background)
);
color: var(--query-builder-v2-color, var(--l2-foreground));
font-size: 12px;
font-weight: 300;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
position: relative;
}
.ant-input {
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
height: 36px;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
position: relative;
margin-left: -1px;
&:hover,
&:focus {
z-index: 1;
border-color: var(--internal-ant-border-color-hover);
}
&:placeholder {
color: var(--query-builder-v2-placeholder-color, var(--l3-foreground));
}
}
}
}
@@ -323,8 +362,8 @@
width: 1px;
background: repeating-linear-gradient(
to bottom,
var(--l1-border),
var(--l1-border) 4px,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
transparent 4px,
transparent 8px
);
@@ -395,8 +434,8 @@
width: 1px;
background: repeating-linear-gradient(
to bottom,
var(--l1-border),
var(--l1-border) 4px,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
transparent 4px,
transparent 8px
);
@@ -412,7 +451,7 @@
min-width: 120px;
border-radius: 2px;
border: 1px solid var(--l1-border);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--l1-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
@@ -457,13 +496,16 @@
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--l1-border) !important;
background: var(--l1-background) !important;
height: 34px !important;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border)) !important;
background: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
height: 36px !important;
box-sizing: border-box !important;
.ant-select-selection-item {
color: var(--l1-foreground);
color: var(--query-builder-v2-color, var(--l1-foreground));
}
}

View File

@@ -92,6 +92,11 @@
.ant-select {
width: 100%;
.ant-select-selector {
min-height: 36px;
}
.ant-select-selection-search-input {
min-width: max-content !important;
max-width: 100% !important;
@@ -100,9 +105,12 @@
.ant-select-selector {
border-radius: 2px;
border: 1.005px solid var(--l1-border);
background: var(--l1-background);
color: var(--l1-foreground);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border)) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
color: var(--query-builder-v2-color, var(--l2-foreground));
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
@@ -123,6 +131,7 @@
.input {
flex: initial;
width: 100px !important;
min-height: 36px;
}
}
}

View File

@@ -8,9 +8,9 @@
.ant-select-selection-search-input {
font-size: 12px !important;
line-height: 27px;
line-height: 25px;
&::placeholder {
color: var(--l2-foreground) !important;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
font-size: 12px !important;
}
}
@@ -22,9 +22,12 @@
.ant-select-selector {
width: 100%;
border-radius: 2px;
border: 1px solid var(--l1-border) !important;
background: var(--l1-background);
color: var(--l1-foreground);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border)) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
color: var(--query-builder-v2-color, var(--l2-foreground));
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
@@ -33,36 +36,49 @@
min-height: 36px;
.ant-select-selection-placeholder {
color: var(--l2-foreground) !important;
color: var(
--query-builder-v2-placeholder-color,
var(--l3-foreground)
) !important;
font-size: 12px !important;
}
}
}
.ant-select-dropdown {
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
.qb-select-popover.ant-select-dropdown {
border-radius: 4px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--query-builder-v2-background-color, var(--l2-background));
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
.ant-select-item {
color: var(--l1-foreground);
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
.ant-select-item {
color: var(--query-builder-v2-color, var(--l2-foreground));
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
&:hover,
&.ant-select-item-option-active {
background: var(--l3-background) !important;
}
&:not(:last-of-type) {
margin-bottom: 4px;
}
&.ant-select-item-option-selected {
background: var(--l3-background) !important;
border: 1px solid var(--l1-border);
font-weight: 600;
}
&:hover,
&.ant-select-item-option-active {
background: var(
--query-builder-v2-selected-background-color,
var(--l3-background)
) !important;
}
&.ant-select-item-option-selected {
background: var(
--query-builder-v2-selected-background-color,
var(--l3-background)
) !important;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
font-weight: 600;
}
}
}

View File

@@ -142,6 +142,7 @@ export const MetricsSelect = memo(function MetricsSelect({
{signalSourceChangeEnabled && (
<Select
className="source-selector"
popupClassName="qb-select-popover"
placeholder="Source"
options={SOURCE_OPTIONS}
value={source}

View File

@@ -1,8 +1,23 @@
// TODO: Improve the styling of the query aggregation container and its components. - @YounixM , @H4ad
.query-add-ons {
width: 100%;
--toggle-group-secondary-bg: var(
--query-builder-v2-toggle-group-background-color,
var(--l1-background-hover)
);
--toggle-group-secondary-border: var(
--query-builder-v2-toggle-group-border-color,
var(--l2-border)
);
--toggle-group-secondary-active-bg: var(
--query-builder-v2-toggle-group-active-background-color,
var(--l1-background)
);
--toggle-group-secondary-bg-hover: var(
--query-builder-v2-toggle-group-background-color-hover,
var(--l2-background)
);
.add-on-tab-title {
display: flex;
align-items: center;
@@ -29,32 +44,33 @@
font-style: normal;
font-weight: var(--font-weight-normal);
color: var(--l2-foreground);
color: var(--query-builder-v2-color, var(--l2-foreground));
}
> button {
border: 1px solid var(--l1-border);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border-left: none;
min-width: 120px;
height: 36px;
line-height: 36px;
&:first-child {
border-left: 1px solid var(--l1-border);
border-left: 1px solid
var(--query-builder-v2-border-color, var(--l2-border));
}
&::before {
background: var(--l1-border);
background: var(--query-builder-v2-border-color, var(--l2-border));
}
&[data-state='on'] {
color: var(--text-robin-500);
border: 1px solid var(--l1-border);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
display: none;
&::before {
background: var(--l1-border);
background: var(--query-builder-v2-border-color, var(--l2-border));
}
}
}
@@ -65,7 +81,7 @@
height: 30px;
border-radius: 2px;
border: 1px solid var(--l1-border);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--l3-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
@@ -78,10 +94,13 @@
align-items: center;
.having-filter-select-container {
position: relative;
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
align-items: flex-start;
background: var(--query-builder-v2-background-color, var(--l2-background));
padding-right: 38px;
.having-filter-select-editor {
border-radius: 2px;
@@ -106,15 +125,17 @@
}
.cm-content {
border-radius: 2px;
border: 1px solid var(--l1-border);
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border-left-width: 0px;
border-right-width: 0px;
padding: 0px !important;
background-color: var(--l2-background) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
&:focus-within {
border-color: var(--l1-border);
border-color: var(--query-builder-v2-border-color, var(--l2-border));
}
}
@@ -218,17 +239,32 @@
}
.cm-line {
line-height: 36px !important;
min-height: 34px;
line-height: 32px !important;
font-family: 'Space Mono', monospace !important;
background-color: var(--l2-background) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
&,
.ͼ1a {
color: var(--query-builder-v2-color, var(--l2-foreground));
}
::-moz-selection {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
opacity: 0.5 !important;
}
::selection {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
opacity: 0.5 !important;
}
@@ -237,8 +273,11 @@
}
.chip-decorator {
background: var(--l3-background) !important;
color: var(--l1-foreground) !important;
background: var(
--query-builder-v2-chip-decorator-background-color,
var(--l3-background)
) !important;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
border-radius: 4px;
padding: 2px 4px;
margin-right: 4px;
@@ -246,34 +285,38 @@
}
.cm-selectionBackground {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
opacity: 0.5 !important;
}
.cm-activeLine > span {
font-size: 12px !important;
}
.cm-placeholder {
color: var(
--query-builder-v2-placeholder-color,
var(--l3-foreground)
) !important;
}
}
}
.close-btn {
position: absolute;
top: 0;
right: 0;
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--l2-border);
background: var(--l2-background);
height: 38px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--query-builder-v2-background-color, var(--l2-background));
height: 100%;
width: 38px;
border-left: transparent;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
&:focus:not(:focus-visible),
&.ant-btn:focus:not(:focus-visible) {
border-color: var(--l2-border);
border-left-color: transparent;
outline: none;
box-shadow: none;
}
}
}
}
@@ -300,20 +343,8 @@
font-size: 12px !important;
}
$add-on-row-height: 38px;
.periscope-input-with-label {
.input {
.ant-select {
height: $add-on-row-height;
}
}
}
.input-with-label {
.input {
height: $add-on-row-height;
}
input {
min-height: 36px;
}
}
}

View File

@@ -23,7 +23,7 @@
flex: 1;
min-width: 0;
font-size: 12px;
color: var(--l2-foreground) !important;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
&.error {
.cm-editor {
@@ -51,14 +51,15 @@
.cm-content {
border-radius: 2px;
border: 1px solid var(--l1-border);
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
padding: 0px !important;
background-color: var(--l1-background) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
&:focus-within {
border-color: var(--l1-border);
border-color: var(--query-builder-v2-border-color, var(--l2-border));
}
}
@@ -74,7 +75,7 @@
right: 0px !important;
border-radius: 4px;
border: 1px solid var(--l1-border);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: linear-gradient(
139deg,
color-mix(in srgb, var(--card) 80%, transparent) 0%,
@@ -118,7 +119,7 @@
box-sizing: border-box;
overflow: hidden;
color: var(--l2-foreground) !important;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
font-family: 'Space Mono', monospace !important;
.cm-completionIcon {
@@ -127,7 +128,10 @@
&:hover,
&[aria-selected='true'] {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
color: var(--l1-foreground) !important;
font-weight: 600 !important;
}
@@ -142,15 +146,24 @@
.cm-line {
line-height: 36px !important;
font-family: 'Space Mono', monospace !important;
background-color: var(--l2-background) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
::-moz-selection {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
opacity: 0.5 !important;
}
::selection {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
opacity: 0.5 !important;
}
@@ -159,8 +172,11 @@
}
.chip-decorator {
background: var(--l3-background) !important;
color: var(--l1-foreground) !important;
background: var(
--query-builder-v2-chip-decorator-background-color,
var(--l3-background)
) !important;
color: var(--query-builder-v2-color, var(--l1-foreground)) !important;
border-radius: 4px;
padding: 2px 4px;
margin-right: 4px;
@@ -168,7 +184,10 @@
}
.cm-selectionBackground {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
opacity: 0.5 !important;
}
}
@@ -201,12 +220,11 @@
.close-btn {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
height: 38px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--query-builder-v2-background-color, var(--l2-background));
height: 100%;
width: 38px;
border-left: transparent;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
@@ -217,13 +235,13 @@
height: 36px;
line-height: 36px;
border-radius: 2px;
border: 1px solid var(--l1-border);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
font-family: 'Space Mono', monospace !important;
&::placeholder {
color: var(--l1-foreground);
color: var(--query-builder-v2-color, var(--l2-foreground));
opacity: 0.5;
}
}
@@ -238,9 +256,10 @@
.query-aggregation-interval-input-container {
.query-aggregation-interval-input {
input {
min-height: 36px;
max-width: 120px;
&::placeholder {
color: var(--l2-foreground);
color: var(--query-builder-v2-color, var(--l2-foreground));
}
}
}
@@ -251,8 +270,8 @@
.query-aggregation-error-popover {
.ant-popover-inner {
background-color: var(--l1-border);
border: 1px solid var(--l1-border);
background-color: var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border-radius: 4px;
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
}

View File

@@ -1,7 +1,7 @@
.add-trace-operator-button,
.add-new-query-button,
.add-formula-button {
border: 1px solid var(--l1-border);
background: var(--l2-background);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--query-builder-v2-background-color, var(--l2-background));
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}

View File

@@ -40,11 +40,14 @@ $max-recents-shown: 5;
.query-status-container {
width: 32px;
background-color: var(--l1-background) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid var(--l1-border);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border-radius: 2px;
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important;
@@ -83,16 +86,16 @@ $max-recents-shown: 5;
.cm-content {
border-radius: 2px;
border: 1px solid var(--l1-border);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
padding: 0px !important;
&:focus-within {
border-color: var(--l1-border);
border-color: var(--query-builder-v2-border-color, var(--l2-border));
}
}
&.cm-focused {
outline: 1px solid var(--l1-border);
outline: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
}
.cm-tooltip-autocomplete {
@@ -183,11 +186,17 @@ $max-recents-shown: 5;
font-family: 'Space Mono', monospace !important;
background-color: var(--l1-background) !important;
color: var(--l2-foreground) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
&:hover {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
}
.cm-completionIcon {
@@ -205,7 +214,10 @@ $max-recents-shown: 5;
}
&[aria-selected='true'] {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
font-weight: 600 !important;
}
}
@@ -274,25 +286,49 @@ $max-recents-shown: 5;
}
.cm-line {
line-height: 34px !important;
line-height: 36px !important;
font-family: 'Space Mono', monospace !important;
background-color: var(--l2-background) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
&,
.ͼ1a {
color: var(--query-builder-v2-color, var(--l2-foreground));
}
::-moz-selection {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
opacity: 0.5 !important;
}
::selection {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
opacity: 0.5 !important;
}
}
.cm-selectionBackground {
background: var(--l3-background) !important;
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
opacity: 0.5 !important;
}
.cm-placeholder {
color: var(
--query-builder-v2-placeholder-color,
var(--l3-foreground)
) !important;
}
}
.cursor-position {

View File

@@ -65,6 +65,14 @@
display: flex;
flex-direction: column;
gap: 8px;
// Meant to fix the query builder colors
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);
--input-focus-background: var(--l2-background);
--input-border-color: var(--l2-border);
--input-hover-border-color: var(--internal-ant-border-color-hover);
--input-focus-border-color: var(--internal-ant-border-color-hover);
}
&-aggregation-container {

View File

@@ -2,6 +2,7 @@ import { Button } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { TooltipSimple } from '@signozhq/ui/tooltip';
interface CheckboxValueRowProps {
value: string;
@@ -46,9 +47,11 @@ function CheckboxValueRow({
{customRendererForValue ? (
customRendererForValue(value)
) : (
<Typography.Text className="value-string" truncate={1}>
{String(value)}
</Typography.Text>
<TooltipSimple title={String(value)} side="top" align="start">
<Typography.Text className="value-string" truncate={1}>
{String(value)}
</Typography.Text>
</TooltipSimple>
)}
<Button type="text" className="only-btn">
{onlyButtonLabel}

View File

@@ -1,10 +1,11 @@
import { ComponentType } from 'react';
import { TabsProps } from 'antd';
import { History } from 'history';
export type TabRoutes = {
name: React.ReactNode;
route: string;
Component: () => JSX.Element;
Component: ComponentType;
key: string;
};

View File

@@ -0,0 +1,34 @@
import { type MouseEvent, type ReactNode } from 'react';
import { Badge } from '@signozhq/ui/badge';
interface TagBadgeProps {
children: ReactNode;
// Show a remove button (editable contexts: create modal, settings drawer).
closable?: boolean;
onClose?: (event: MouseEvent<HTMLButtonElement>) => void;
className?: string;
}
// The single sienna tag chip used everywhere dashboards render tags — list rows,
// the details header, and the tag editors. Kept as one component so the tag
// styling stays identical across all of them.
function TagBadge({
children,
closable,
onClose,
className,
}: TagBadgeProps): JSX.Element {
return (
<Badge
color="sienna"
variant="outline"
className={className}
closable={closable}
onClose={onClose}
>
{children}
</Badge>
);
}
export default TagBadge;

View File

@@ -15,59 +15,28 @@
border: 1px solid var(--l2-border);
}
// Sienna chip — matches the dashboard list-row tag badge.
// Sienna chip via the @signozhq Badge; this only constrains its width.
.tag {
display: inline-flex;
align-items: center;
gap: 4px;
max-width: 240px;
height: 24px;
padding: 2px 4px 2px 8px;
border-radius: 50px;
border: 1px solid color-mix(in srgb, var(--bg-sienna-500) 20%, transparent);
background: color-mix(in srgb, var(--bg-sienna-500) 10%, transparent);
color: var(--bg-sienna-400);
font-size: 13px;
font-weight: var(--font-weight-normal);
line-height: 20px;
cursor: text;
}
// The tag label is a bare, chrome-less button inside the Badge (double-click to
// edit); strip its Button styling and let it ellipsize.
.tagLabel {
--button-height: auto;
--button-padding: 0;
--button-gap: 0;
--button-variant-ghost-background-color: transparent;
--button-variant-ghost-hover-background-color: transparent;
--button-variant-ghost-color: inherit;
--button-variant-ghost-hover-color: inherit;
--button-variant-ghost-color: currentColor;
--button-variant-ghost-hover-color: currentColor;
overflow: hidden;
max-width: 200px;
font-size: 13px;
font-weight: var(--font-weight-normal);
text-overflow: ellipsis;
white-space: nowrap;
cursor: text;
}
.remove {
// Size overrides to fit the chip, plus a sienna-tinted hover — the Button's
// default ghost hover is a grey that clashes with the chip. Resting color is
// left at the Button default.
--button-height: 16px;
--button-padding: 0;
--button-border-radius: 50%;
--button-variant-ghost-hover-background-color: color-mix(
in srgb,
var(--bg-sienna-500) 22%,
transparent
);
--button-variant-ghost-hover-color: var(--bg-sienna-400);
width: 16px;
min-width: 16px;
flex: none;
}
.input {
flex: 1;
min-width: 120px;

View File

@@ -2,9 +2,9 @@ import { type ChangeEvent, type KeyboardEvent, useState } from 'react';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { Typography } from '@signozhq/ui/typography';
import { X } from '@signozhq/icons';
import cx from 'classnames';
import TagBadge from '../TagBadge/TagBadge';
import { parseKeyValueTag } from './utils';
import styles from './TagKeyValueInput.module.scss';
@@ -65,7 +65,9 @@ function TagKeyValueInput({
};
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
// Plain Enter adds the tag; let Cmd/Ctrl+Enter pass through so a host form
// (e.g. a modal) can submit on it.
if (e.key === 'Enter' && !e.metaKey && !e.ctrlKey) {
e.preventDefault();
commit();
}
@@ -93,11 +95,17 @@ function TagKeyValueInput({
};
const handleEditKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
// Plain Enter commits the edit; let Cmd/Ctrl+Enter pass through so a host
// form (e.g. a modal) can submit on it.
if (e.key === 'Enter' && !e.metaKey && !e.ctrlKey) {
e.preventDefault();
e.stopPropagation();
commitEdit();
} else if (e.key === 'Escape') {
// Contain Escape so it cancels the inline edit instead of bubbling up and
// closing the host drawer/modal.
e.preventDefault();
e.stopPropagation();
cancelEdit();
}
};
@@ -120,27 +128,23 @@ function TagKeyValueInput({
onBlur={commitEdit}
/>
) : (
<div key={tag} className={styles.tag} data-testid={`${testId}-chip`}>
<TagBadge
key={tag}
className={styles.tag}
closable
onClose={(): void => removeTag(tag)}
>
<Button
variant="ghost"
color="secondary"
className={styles.tagLabel}
title="Double-click to edit"
testId={`${testId}-chip`}
onDoubleClick={(): void => startEdit(index)}
>
{tag}
</Button>
<Button
variant="ghost"
color="secondary"
size="icon"
className={styles.remove}
aria-label={`Remove ${tag}`}
onClick={(): void => removeTag(tag)}
>
<X size={12} />
</Button>
</div>
</TagBadge>
),
)}
<Input

View File

@@ -57,10 +57,13 @@ function TimelineV3(props: ITimelineV3Props): JSX.Element {
}
const timeAtCursor = offsetTimestamp + cursorXPercent * spread;
const unit = getIntervalUnit(spread, offsetTimestamp);
// Use the same width-derived interval spread the ticks use, so the badge
// unit always matches the tick unit (narrow rulers pick fewer intervals).
const intervalSpread = spread / getMinimumIntervalsBasedOnWidth(width);
const unit = getIntervalUnit(intervalSpread, offsetTimestamp);
const formatted = toFixed(resolveTimeFromInterval(timeAtCursor, unit), 2);
return `${formatted}${unit.name}`;
}, [cursorXPercent, spread, offsetTimestamp]);
}, [cursorXPercent, spread, offsetTimestamp, width]);
if (endTimestamp < startTimestamp) {
console.error(
@@ -94,12 +97,17 @@ function TimelineV3(props: ITimelineV3Props): JSX.Element {
>
<text
x={index === intervals.length - 1 ? -10 : 0}
y={timelineHeight * 2}
y={timelineHeight * 2 - 3}
fill={strokeColor}
>
{interval.label}
</text>
<line y1={0} y2={timelineHeight} stroke={strokeColor} strokeWidth="1" />
<line
y1={0}
y2={timelineHeight - 3}
stroke={strokeColor}
strokeWidth="1"
/>
</g>
))}
</svg>

View File

@@ -0,0 +1,63 @@
import {
getIntervals,
getIntervalUnit,
getMinimumIntervalsBasedOnWidth,
} from '../utils';
// A tick label looks like "200.00ms" / "1.10s" — grab the trailing unit name.
function unitOfLabel(label: string): string {
const match = label.match(/[a-z]+$/i);
return match ? match[0] : '';
}
describe('getMinimumIntervalsBasedOnWidth', () => {
it('returns fewer intervals for narrower rulers', () => {
expect(getMinimumIntervalsBasedOnWidth(500)).toBe(3);
expect(getMinimumIntervalsBasedOnWidth(700)).toBe(4);
expect(getMinimumIntervalsBasedOnWidth(900)).toBe(5);
expect(getMinimumIntervalsBasedOnWidth(1200)).toBe(6);
});
});
describe('getIntervalUnit', () => {
it('selects the unit from the interval spread', () => {
expect(getIntervalUnit(130, 0).name).toBe('ms');
expect(getIntervalUnit(1100, 0).name).toBe('s');
expect(getIntervalUnit(70_000, 0).name).toBe('m');
});
it('accounts for a large offset (deep-zoom labels stay readable)', () => {
// Cursor spread is tiny but the window starts 5,000,000ms into the trace.
expect(getIntervalUnit(100, 5_000_000).name).toBe('hr');
});
// Regression: the interval COUNT changes the chosen unit, so the crosshair
// badge must use the same width-derived count as the ticks. On a 5.5s trace a
// narrow ruler (5 intervals → 1100ms → "s") and a wide one (6 intervals →
// 916ms → "ms") pick different units.
it('can resolve to different units for the same spread at different counts', () => {
const spread = 5500;
expect(getIntervalUnit(spread / 5, 0).name).toBe('s');
expect(getIntervalUnit(spread / 6, 0).name).toBe('ms');
});
});
describe('badge/tick unit consistency', () => {
// The invariant the fix guarantees: when the badge and the ticks are fed the
// same width-derived intervalSpread, every tick label uses the badge's unit.
it.each([
{ spread: 1287, width: 900 },
{ spread: 5500, width: 900 }, // narrow → 5 intervals, the mismatch case
{ spread: 5500, width: 1200 }, // wide → 6 intervals
{ spread: 120_000, width: 700 },
])('spread=$spread width=$width', ({ spread, width }) => {
const minIntervals = getMinimumIntervalsBasedOnWidth(width);
const intervalSpread = spread / minIntervals;
const badgeUnit = getIntervalUnit(intervalSpread, 0).name;
const intervals = getIntervals(intervalSpread, spread, 0);
intervals.forEach((interval) => {
expect(unitOfLabel(interval.label)).toBe(badgeUnit);
});
});
});

View File

@@ -10,14 +10,18 @@ export type { Interval };
/**
* Select the interval unit matching the timeline's logic.
* Exported so crosshair labels use the same unit as timeline ticks.
* Exported so crosshair labels use the same unit as the timeline ticks.
*
* Takes the already-computed `intervalSpread` (spread / minIntervals) rather
* than deriving it from a hardcoded interval count — the tick count is
* width-dependent (`getMinimumIntervalsBasedOnWidth`), so callers must pass the
* same `intervalSpread` the ticks use or the badge unit can diverge from the
* ticks (e.g. ms vs s) on narrower rulers like the waterfall.
*/
export function getIntervalUnit(
spread: number,
intervalSpread: number,
offsetTimestamp: number,
): IIntervalUnit {
const minIntervals = 6;
const intervalSpread = spread / minIntervals;
const valueForUnitSelection = Math.max(offsetTimestamp, intervalSpread);
let unit: IIntervalUnit = INTERVAL_UNITS[0];
for (let idx = INTERVAL_UNITS.length - 1; idx >= 0; idx -= 1) {

View File

@@ -1,12 +1,12 @@
/* Overlay stays below content */
[data-slot='dialog-overlay'] {
z-index: 50;
z-index: 1000 !important;
}
/* Dialog content always above overlay */
[data-slot='dialog-content'] {
position: fixed;
z-index: 60;
z-index: 1001 !important;
background: var(--l1-background);
color: var(--l1-foreground);

View File

@@ -22,6 +22,7 @@ import {
} from 'container/AIAssistant/store/useAIAssistantStore';
import { useThemeMode } from 'hooks/useDarkMode';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { IS_DEV } from 'lib/env';
import history from 'lib/history';
import { ROLES as UserRole } from 'types/roles';
@@ -30,6 +31,33 @@ import { useCmdK } from '../../providers/cmdKProvider';
import './cmdKPalette.scss';
const AuthZDevModal = IS_DEV
? React.lazy(() =>
import('lib/authz/devtools/AuthZDevModal/AuthZDevModal').then((m) => ({
default: m.AuthZDevModal,
})),
)
: null;
const AuthZDevFloatingIndicator = IS_DEV
? React.lazy(() =>
import('lib/authz/devtools/AuthZDevFloatingIndicator/AuthZDevFloatingIndicator').then(
(m) => ({
default: m.AuthZDevFloatingIndicator,
}),
),
)
: null;
const openAuthZDevModal = IS_DEV
? (): void => {
void import('lib/authz/devtools/useAuthZDevStore').then((m) => {
m.openAuthZDevModal();
return m;
});
}
: undefined;
type CmdAction = {
id: string;
name: string;
@@ -110,6 +138,7 @@ export function CmdKPalette({
aiAssistant: isAIAssistantEnabled
? { open: handleOpenAIAssistant }
: undefined,
authzDevTools: openAuthZDevModal ? { open: openAuthZDevModal } : undefined,
});
// RBAC filter: show action if no roles set OR current user role is included
@@ -146,37 +175,57 @@ export function CmdKPalette({
};
return (
<CommandDialog open={open} onOpenChange={setOpen} position="top" offset={110}>
<CommandInput placeholder="Search…" className="cmdk-input-wrapper" />
<CommandList className="cmdk-list-scroll">
<CommandEmpty>No results</CommandEmpty>
{grouped.map(([section, items]) => (
<CommandGroup
key={section}
heading={section}
className="cmdk-section-heading"
>
{items.map((it) => (
<CommandItem
key={it.id}
onSelect={(): void => handleInvoke(it)}
value={it.name}
className={theme === 'light' ? 'cmdk-item-light' : 'cmdk-item'}
>
<span
className={cx('cmd-item-icon', it.id === 'ai-assistant' && 'noz-icon')}
<>
<CommandDialog
open={open}
onOpenChange={setOpen}
position="top"
offset={110}
>
<CommandInput placeholder="Search…" className="cmdk-input-wrapper" />
<CommandList className="cmdk-list-scroll">
<CommandEmpty>No results</CommandEmpty>
{grouped.map(([section, items]) => (
<CommandGroup
key={section}
heading={section}
className="cmdk-section-heading"
>
{items.map((it) => (
<CommandItem
key={it.id}
onSelect={(): void => handleInvoke(it)}
value={it.name}
className={theme === 'light' ? 'cmdk-item-light' : 'cmdk-item'}
>
{it.icon}
</span>
{it.name}
{it.shortcut && it.shortcut.length > 0 && (
<CommandShortcut>{it.shortcut.join(' • ')}</CommandShortcut>
)}
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</CommandDialog>
<span
className={cx(
'cmd-item-icon',
it.id === 'ai-assistant' && 'noz-icon',
)}
>
{it.icon}
</span>
{it.name}
{it.shortcut && it.shortcut.length > 0 && (
<CommandShortcut>{it.shortcut.join(' • ')}</CommandShortcut>
)}
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</CommandDialog>
{IS_DEV && AuthZDevModal && (
<React.Suspense fallback={null}>
<AuthZDevModal />
</React.Suspense>
)}
{IS_DEV && AuthZDevFloatingIndicator && (
<React.Suspense fallback={null}>
<AuthZDevFloatingIndicator />
</React.Suspense>
)}
</>
);
}

View File

@@ -11,6 +11,7 @@ export enum FeatureKeys {
USE_JSON_BODY = 'use_json_body',
USE_FINE_GRAINED_AUTHZ = 'use_fine_grained_authz',
USE_DASHBOARD_V2 = 'use_dashboard_v2',
USE_INFRA_MONITORING_V2 = 'use_infra_monitoring_v2',
ENABLE_AI_OBSERVABILITY = 'enable_ai_observability',
ENABLE_METRICS_REDUCTION = 'enable_metrics_reduction',
}

View File

@@ -31,6 +31,12 @@ export enum LOCALSTORAGE {
METRICS_LIST_OPTIONS = 'METRICS_LIST_OPTIONS',
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
QUICK_FILTERS_WIDTH_LOGS = 'QUICK_FILTERS_WIDTH_LOGS',
QUICK_FILTERS_WIDTH_TRACES = 'QUICK_FILTERS_WIDTH_TRACES',
QUICK_FILTERS_WIDTH_METER = 'QUICK_FILTERS_WIDTH_METER',
QUICK_FILTERS_WIDTH_API_MONITORING = 'QUICK_FILTERS_WIDTH_API_MONITORING',
QUICK_FILTERS_WIDTH_EXCEPTIONS = 'QUICK_FILTERS_WIDTH_EXCEPTIONS',
QUICK_FILTERS_WIDTH_INFRA = 'QUICK_FILTERS_WIDTH_INFRA',
FUNNEL_STEPS = 'FUNNEL_STEPS',
SPAN_DETAILS_PINNED_ATTRIBUTES = 'SPAN_DETAILS_PINNED_ATTRIBUTES',
LAST_USED_CUSTOM_TIME_RANGES = 'LAST_USED_CUSTOM_TIME_RANGES',

View File

@@ -89,8 +89,10 @@ const ROUTES = {
AI_ASSISTANT_BASE: '/ai-assistant',
AI_ASSISTANT_ICON_PREVIEW: '/ai-assistant-icon-preview',
MCP_SERVER: '/settings/mcp-server',
LLM_OBSERVABILITY_ATTRIBUTE_MAPPING: '/llm-observability/attribute-mapping',
LLM_OBSERVABILITY_BASE: '/llm-observability',
LLM_OBSERVABILITY_MODEL_PRICING: '/llm-observability/settings/model-pricing',
LLM_OBSERVABILITY_OVERVIEW: '/llm-observability/overview',
LLM_OBSERVABILITY_CONFIGURATION: '/llm-observability/configuration',
} as const;
export default ROUTES;

View File

@@ -43,10 +43,17 @@ type ActionDeps = {
aiAssistant?: {
open: () => void;
};
/**
* Provided only in development mode. Opens the AuthZ DevTools modal
* for testing permission overrides.
*/
authzDevTools?: {
open: () => void;
};
};
export function createShortcutActions(deps: ActionDeps): CmdAction[] {
const { navigate, handleThemeChange, aiAssistant } = deps;
const { navigate, handleThemeChange, aiAssistant, authzDevTools } = deps;
const actions: CmdAction[] = [
{
@@ -302,5 +309,17 @@ export function createShortcutActions(deps: ActionDeps): CmdAction[] {
});
}
if (authzDevTools) {
actions.push({
id: 'authz-devtools',
name: 'AuthZ DevTools',
keywords: 'authz permissions rbac debug devtools override testing',
section: 'Dev',
icon: <Settings size={14} />,
roles: ['ADMIN', 'EDITOR', 'VIEWER'],
perform: authzDevTools.open,
});
}
return actions;
}

View File

@@ -177,7 +177,13 @@ export default function ConversationView({
conversationId={conversationId}
messages={messages}
isStreaming={isStreamingHere}
onSendSuggestedPrompt={(text): void => handleSend(text)}
onSendSuggestedPrompt={(text): void => {
handleSend(
text,
undefined,
autoContexts.length > 0 ? autoContexts : undefined,
);
}}
/>
{showDisclaimer && (
<div className={disclaimerClass} role="note" aria-live="polite">

View File

@@ -0,0 +1,142 @@
import { MemoryRouter } from 'react-router-dom';
// eslint-disable-next-line no-restricted-imports
import { fireEvent, render } from '@testing-library/react';
import { MessageContext } from 'api/ai-assistant/chat';
import { useAIAssistantStore } from 'container/AIAssistant/store/useAIAssistantStore';
import { VariantContext } from 'container/AIAssistant/VariantContext';
const CHIP_ID = 'recent-errors';
const CHIP_TEXT = 'Show me recent errors';
// Auto-derived page context that a normal (typed) send would attach. The chip
// send must forward the exact same array.
const mockAutoContexts: MessageContext[] = [
{
source: 'auto',
type: 'dashboard',
resourceId: 'dashboard-123',
resourceName: 'Checkout dashboard',
},
];
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('container/AIAssistant/getAutoContexts', () => ({
getAutoContexts: jest.fn(() => mockAutoContexts),
}));
jest.mock('container/AIAssistant/hooks/useAIAssistantAnalyticsContext', () => ({
normalizePage: (page: string): string => page,
useAIAssistantAnalyticsContext: (): unknown => ({ threadId: 'thread-1' }),
}));
// ChatInput is heavy and irrelevant here — the chip path lives entirely in the
// empty state. Provide a lightweight stub plus the `autoContextKey` named export
// ConversationView imports for its dismissed-context filter.
jest.mock('container/AIAssistant/components/ChatInput', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="chat-input" />,
autoContextKey: (): string => '',
}));
jest.mock('container/AIAssistant/components/ConversationSkeleton', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="skeleton" />,
}));
// VirtualizedMessages renders the real empty-state chips; stub only its
// never-rendered-in-empty-state children and the virtual list.
jest.mock('components/Noz/Noz', () => ({
__esModule: true,
default: (): JSX.Element => <div data-testid="noz" />,
}));
jest.mock('container/AIAssistant/components/MessageBubble', () => ({
__esModule: true,
default: (): null => null,
}));
jest.mock('container/AIAssistant/components/StreamingMessage', () => ({
__esModule: true,
default: (): null => null,
}));
jest.mock('react-virtuoso', () => ({
__esModule: true,
Virtuoso: (): null => null,
}));
jest.mock(
'container/AIAssistant/components/VirtualizedMessages/useEmptyStateChips',
() => ({
useEmptyStateChips: (): { chips: { id: string; text: string }[] } => ({
chips: [{ id: CHIP_ID, text: CHIP_TEXT }],
}),
}),
);
// eslint-disable-next-line import/first
import ConversationView from '../ConversationView';
const CONVERSATION_ID = 'conv-1';
function renderView(variant: 'panel' | 'page' | 'modal'): {
getByTestId: (id: string) => HTMLElement;
} {
return render(
<MemoryRouter initialEntries={['/dashboard/dashboard-123']}>
<VariantContext.Provider value={variant}>
<ConversationView conversationId={CONVERSATION_ID} />
</VariantContext.Provider>
</MemoryRouter>,
);
}
describe('ConversationView — empty-state chip context', () => {
let sendMessage: jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
sendMessage = jest.fn();
useAIAssistantStore.setState({
conversations: {
[CONVERSATION_ID]: {
id: CONVERSATION_ID,
messages: [],
createdAt: 1,
updatedAt: 1,
},
},
streams: {},
activeConversationId: CONVERSATION_ID,
isLoadingThread: false,
sendMessage,
} as unknown as Partial<ReturnType<typeof useAIAssistantStore.getState>>);
});
it('forwards the page auto-contexts when a chip is clicked (embedded variant)', () => {
const { getByTestId } = renderView('panel');
fireEvent.click(getByTestId(`empty-state-chip-${CHIP_ID}`));
// The chip send must carry the same auto-contexts a typed message would.
expect(sendMessage).toHaveBeenCalledTimes(1);
expect(sendMessage).toHaveBeenCalledWith(
CHIP_TEXT,
undefined,
mockAutoContexts,
);
});
it('sends undefined contexts on the standalone page (no page context to attach)', () => {
const { getByTestId } = renderView('page');
fireEvent.click(getByTestId(`empty-state-chip-${CHIP_ID}`));
expect(sendMessage).toHaveBeenCalledTimes(1);
expect(sendMessage).toHaveBeenCalledWith(CHIP_TEXT, undefined, undefined);
});
});

View File

@@ -163,12 +163,23 @@
}
&.filter-visible {
// Width is owned by ResizableBox (inline style); this section is the
// ResizableBox root, so it stays position: relative for the drag handle.
.api-quick-filter-left-section {
width: 260px;
.resizable-box__content {
display: flex;
flex-direction: column;
}
.quick-filters-container {
flex: 1;
min-height: 0;
}
}
.api-module-right-section {
width: calc(100% - 260px);
flex: 1;
min-width: 0;
}
}
}

View File

@@ -4,21 +4,49 @@ import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
import { LOCALSTORAGE } from 'constants/localStorage';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { ResizableBox } from 'periscope/components/ResizableBox';
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
import DomainList from './Domains/DomainList';
import './Explorer.styles.scss';
const QUICK_FILTERS_DEFAULT_WIDTH = 260;
const QUICK_FILTERS_MIN_WIDTH = 240;
const QUICK_FILTERS_MAX_WIDTH = 500;
function Explorer(): JSX.Element {
useEffect(() => {
logEvent('API Monitoring: Landing page visited', {});
}, []);
const {
initialWidth: quickFiltersInitialWidth,
persistWidth: persistQuickFiltersWidth,
} = usePanelWidth({
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_API_MONITORING,
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
minWidth: QUICK_FILTERS_MIN_WIDTH,
maxWidth: QUICK_FILTERS_MAX_WIDTH,
});
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className={cx('api-monitoring-page', 'filter-visible')}>
<section className="api-quick-filter-left-section">
<ResizableBox
handle="right"
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
initialWidth={quickFiltersInitialWidth}
minWidth={QUICK_FILTERS_MIN_WIDTH}
maxWidth={QUICK_FILTERS_MAX_WIDTH}
onResize={persistQuickFiltersWidth}
resetToDefaultOnDoubleClick
withHandle
className="api-quick-filter-left-section"
handleTestId="quick-filters-resize-handle"
>
<QuickFilters
className="qf-api-monitoring"
source={QuickFiltersSource.API_MONITORING}
@@ -27,7 +55,7 @@ function Explorer(): JSX.Element {
showQueryName={false}
handleFilterVisibilityChange={(): void => {}}
/>
</section>
</ResizableBox>
<DomainList />
</div>
</Sentry.ErrorBoundary>

View File

@@ -149,6 +149,18 @@
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
padding: 20px 16px 0px 16px;
/* Preserve author-entered line breaks in the description. */
white-space: pre-wrap;
overflow-wrap: anywhere;
a {
color: var(--accent-primary);
text-decoration: underline;
&:hover {
text-decoration: none;
}
}
}
.dashboard-variables {

View File

@@ -43,6 +43,7 @@ import { sortLayout } from 'providers/Dashboard/util';
import { DashboardData } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update';
import { ROLES, USER_ROLES } from 'types/roles';
import { linkifyText } from 'utils/linkifyText';
import { ComponentTypes } from 'utils/permission';
import { v4 as uuid } from 'uuid';
@@ -515,7 +516,9 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
</div>
)}
{!isEmpty(description) && (
<section className="dashboard-description-section">{description}</section>
<section className="dashboard-description-section">
{linkifyText(description ?? '')}
</section>
)}
{!isEmpty(dashboardVariables) && (

View File

@@ -1,3 +1,4 @@
import type { MouseEvent as ReactMouseEvent } from 'react';
import type { PrecisionOption } from 'components/Graph/types';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
@@ -27,7 +28,7 @@ interface PieArcProps {
fill: string;
onEnter: (slice: PieSlice, centroidX: number, centroidY: number) => void;
onLeave: () => void;
onClick?: (slice: PieSlice) => void;
onClick?: (slice: PieSlice, event: ReactMouseEvent) => void;
}
/**
@@ -72,7 +73,7 @@ export default function PieArc({
<g
onMouseEnter={(): void => onEnter(slice, centroidX, centroidY)}
onMouseLeave={onLeave}
onClick={(): void => onClick?.(slice)}
onClick={(event): void => onClick?.(slice, event)}
>
<path d={arcPath} fill={fill} />
{shouldShowLabel && (

View File

@@ -80,6 +80,7 @@ describe('PieArc', () => {
expect(onLeave).toHaveBeenCalledTimes(1);
fireEvent.click(g);
expect(onClick).toHaveBeenCalledWith(SLICE);
// onClick now also receives the DOM event (for drill-down popover positioning).
expect(onClick).toHaveBeenCalledWith(SLICE, expect.anything());
});
});

View File

@@ -1,3 +1,4 @@
import type { MouseEvent as ReactMouseEvent } from 'react';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PrecisionOption } from 'components/Graph/types';
import {
@@ -79,6 +80,10 @@ export interface PieSlice {
label: string;
value: number;
color: string;
/** Source query of the slice's value column — the drill-down target (present for V2 panels). */
queryName?: string;
/** Group-by key→value of the slice's source row, used to build drill-down filters. */
labels?: Record<string, string>;
}
/**
@@ -99,7 +104,7 @@ export interface PieChartProps {
* (shared GRAPH_VISIBILITY_STATES, keyed by label). Omit to disable persistence.
*/
id?: string;
/** Fired when a slice (or its legend entry) is clicked. */
onSliceClick?: (slice: PieSlice) => void;
/** Fired when a slice's arc is clicked; carries the DOM event for popover positioning. */
onSliceClick?: (slice: PieSlice, event: ReactMouseEvent) => void;
'data-testid'?: string;
}

View File

@@ -2,7 +2,7 @@
position: relative;
display: flex;
width: 100%;
height: 100%;
height: auto;
flex-direction: column;
// Stacked children (the FullView / standalone graph-manager) sit below the chart

View File

@@ -41,6 +41,28 @@
.steps-container {
width: 80%;
.alert-query-section-container {
--query-builder-v2-color: var(--l2-foreground);
--query-builder-v2-background-color: var(--l3-background);
--query-builder-v2-border-color: var(--l3-border);
--query-builder-v2-selection-background-color: var(--l2-background);
--query-builder-v2-chip-decorator-background-color: var(--l2-background);
--query-builder-v2-placeholder-color: var(--l3-foreground);
--query-builder-v2-selected-background-color: var(--l3-foreground);
--query-search-background-color: var(--l3-background);
--query-search-background-color-selection: var(--l3-background);
--input-with-label-border-color: var(--l3-border);
--input-with-label-background-color: var(--l3-background);
--input-with-label-color: var(--l2-foreground);
--query-builder-v2-toggle-group-background-color: var(--l3-background);
--query-builder-v2-toggle-group-border-color: var(--l3-border);
--query-builder-v2-toggle-group-active-background-color: var(--l2-background);
--query-builder-v2-toggle-group-background-color-hover: var(--l3-background);
--input-height: 36px;
--input-hover-background: var(--l3-background);
--input-hover-border-color: var(--internal-ant-border-color-hover);
}
}
.qb-chart-preview-container {

View File

@@ -0,0 +1,4 @@
.wrapper :global(button[data-color]) {
--checkbox-checked-background: var(--series-color);
--checkbox-border-color: var(--series-color);
}

View File

@@ -3,6 +3,7 @@ import { Checkbox } from '@signozhq/ui/checkbox';
import { CSSProperties } from 'react';
import { CheckBoxProps } from '../types';
import styles from './CustomCheckBox.module.scss';
function CustomCheckBox({
data,
@@ -15,12 +16,11 @@ function CustomCheckBox({
const isChecked = graphVisibilityState[index] || false;
const colorStyle = {
'--checkbox-checked-background': color,
'--checkbox-border-color': color,
'--series-color': color,
} as CSSProperties;
return (
<span style={colorStyle}>
<span className={styles.wrapper} style={colorStyle}>
<Checkbox
onChange={(checked): void => checkBoxOnChangeHandler(checked, index)}
value={isChecked}

View File

@@ -3,6 +3,30 @@
overflow-x: auto;
overflow-y: hidden;
--query-builder-v2-color: var(--l2-foreground);
--query-builder-v2-background-color: var(--l3-background);
--query-builder-v2-border-color: var(--l3-border);
--query-builder-v2-selection-background-color: var(--l2-background);
--query-builder-v2-chip-decorator-background-color: var(--l2-background);
--query-builder-v2-placeholder-color: var(--l3-foreground);
--query-builder-v2-selected-background-color: var(--l3-foreground);
--query-search-background-color: var(--l3-background);
--query-search-background-color-selection: var(--l3-background);
--input-with-label-border-color: var(--l3-border);
--input-with-label-background-color: var(--l3-background);
--input-with-label-color: var(--l2-foreground);
--query-builder-v2-toggle-group-background-color: var(--l3-background);
--query-builder-v2-toggle-group-border-color: var(--l3-border);
--query-builder-v2-toggle-group-active-background-color: var(--l2-background);
--query-builder-v2-toggle-group-background-color-hover: var(--l3-background);
--input-height: 36px;
--input-hover-background: var(--l3-background);
--input-hover-border-color: var(--internal-ant-border-color-hover);
--input-focus-border-color: var(--internal-ant-border-color-hover);
--input-background: var(--l3-background);
--input-focus-background: var(--l3-background);
--input-border-color: var(--l3-border);
.full-view-header-container {
display: flex;
flex-direction: column;

View File

@@ -66,6 +66,7 @@ function WidgetGraphComponent({
customOnRowClick,
customTimeRangeWindowForCoRelation,
enableDrillDown,
hidePagination,
}: WidgetGraphComponentProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const [deleteModal, setDeleteModal] = useState(false);
@@ -430,6 +431,7 @@ function WidgetGraphComponent({
customSeries={customSeries}
customOnRowClick={customOnRowClick}
enableDrillDown={enableDrillDown}
hidePagination={hidePagination}
onColumnWidthsChange={onColumnWidthsChange}
/>
</div>

View File

@@ -42,6 +42,8 @@ export interface WidgetGraphComponentProps {
customOnRowClick?: (record: RowData) => void;
customTimeRangeWindowForCoRelation?: string | undefined;
enableDrillDown?: boolean;
/** Hide list-panel pagination controls (e.g. public dashboards, where paging isn't supported). */
hidePagination?: boolean;
}
export interface GridCardGraphProps {

View File

@@ -1,15 +1,20 @@
import { useEffect, useState } from 'react';
import { useEffect, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { Button, Skeleton } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import logEvent from 'api/common/logEvent';
import { useListDashboardsForUserV2 } from 'api/generated/services/dashboard';
import {
DashboardtypesListOrderDTO,
DashboardtypesListSortDTO,
} from 'api/generated/services/sigNoz.schemas';
import ROUTES from 'constants/routes';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import { useIsDashboardV2 } from 'hooks/useIsDashboardV2';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { ArrowRight, ArrowUpRight, Plus } from '@signozhq/icons';
import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { Dashboard } from 'types/api/dashboard/getAll';
import { USER_ROLES } from 'types/roles';
import { openInNewTab } from 'utils/navigation';
@@ -17,6 +22,13 @@ import dialsUrl from '@/assets/Icons/dials.svg';
import { getItemIcon } from '../constants';
// The five most-recent dashboards, normalised across the v1 and v2 list APIs.
interface RecentDashboard {
id: string;
title: string;
tags: string[];
}
export default function Dashboards({
onUpdateChecklistDoneItem,
loadingUserPreferences,
@@ -26,33 +38,58 @@ export default function Dashboards({
}): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const { user } = useAppContext();
const isDashboardV2 = useIsDashboardV2();
const [sortedDashboards, setSortedDashboards] = useState<Dashboard[]>([]);
// Fetch Dashboards
// Fetch the recent dashboards from whichever API the `use_dashboard_v2` flag
// selects; the inactive one stays disabled so it never fires.
const {
data: dashboardsList,
isLoading: isDashboardListLoading,
isError: isDashboardListError,
} = useGetAllDashboard();
data: v1List,
isLoading: v1Loading,
isError: v1Error,
} = useGetAllDashboard({ enabled: !isDashboardV2 });
const {
data: v2List,
isLoading: v2Loading,
isError: v2Error,
} = useListDashboardsForUserV2(
{
sort: DashboardtypesListSortDTO.updated_at,
order: DashboardtypesListOrderDTO.desc,
limit: 5,
offset: 0,
},
{ query: { enabled: isDashboardV2 } },
);
const isDashboardListLoading = isDashboardV2 ? v2Loading : v1Loading;
const isDashboardListError = isDashboardV2 ? v2Error : v1Error;
const sortedDashboards = useMemo<RecentDashboard[]>(() => {
if (isDashboardV2) {
return (v2List?.data?.dashboards ?? []).map((d) => ({
id: d.id,
title: d.spec?.display?.name ?? d.name,
tags: (d.tags ?? []).map((t) => (t.value ? `${t.key}:${t.value}` : t.key)),
}));
}
return [...(v1List?.data ?? [])]
.sort(
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
)
.slice(0, 5)
.map((d) => ({
id: d.id,
title: d.data.title,
tags: d.data.tags ?? [],
}));
}, [isDashboardV2, v1List, v2List]);
useEffect(() => {
if (!dashboardsList) {
return;
}
const sortedDashboards = dashboardsList.data.sort((a, b) => {
const aUpdateAt = new Date(a.updatedAt).getTime();
const bUpdateAt = new Date(b.updatedAt).getTime();
return bUpdateAt - aUpdateAt;
});
if (sortedDashboards.length > 0 && !loadingUserPreferences) {
onUpdateChecklistDoneItem('SETUP_DASHBOARDS');
}
setSortedDashboards(sortedDashboards.slice(0, 5));
}, [dashboardsList, onUpdateChecklistDoneItem, loadingUserPreferences]);
}, [sortedDashboards, onUpdateChecklistDoneItem, loadingUserPreferences]);
const emptyStateCard = (): JSX.Element => (
<div className="empty-state-container">
@@ -113,7 +150,7 @@ export default function Dashboards({
event.stopPropagation();
logEvent('Homepage: Dashboard clicked', {
dashboardId: dashboard.id,
dashboardName: dashboard.data.title,
dashboardName: dashboard.title,
});
if (event.metaKey || event.ctrlKey) {
openInNewTab(getLink());
@@ -143,12 +180,12 @@ export default function Dashboards({
/>
<div className="alert-rule-item-name home-data-item-name">
{dashboard.data.title}
{dashboard.title}
</div>
</div>
<div className="alert-rule-item-description home-data-item-tag">
{dashboard.data.tags?.map((tag) => (
{dashboard.tags.map((tag) => (
<Badge color="sienna" variant="outline" key={tag}>
{tag}
</Badge>

View File

@@ -6,6 +6,7 @@ import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import K8sBaseDetails from 'container/InfraMonitoringK8s/Base/K8sBaseDetails';
import { K8sBaseList } from 'container/InfraMonitoringK8s/Base/K8sBaseList';
import { K8sBaseFilters } from 'container/InfraMonitoringK8s/Base/types';
@@ -16,6 +17,8 @@ import {
} from 'container/InfraMonitoringK8s/hooks';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { ResizableBox } from 'periscope/components/ResizableBox';
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
import { useAppContext } from 'providers/App/App';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -40,8 +43,21 @@ import { getHostsQuickFiltersConfig } from './utils';
import styles from './InfraMonitoringHosts.module.scss';
import { ArrowUpToLine, Filter } from '@signozhq/icons';
const QUICK_FILTERS_DEFAULT_WIDTH = 280;
const QUICK_FILTERS_MIN_WIDTH = 240;
const QUICK_FILTERS_MAX_WIDTH = 500;
function Hosts(): JSX.Element {
const [showFilters, setShowFilters] = useState(true);
const {
initialWidth: quickFiltersInitialWidth,
persistWidth: persistQuickFiltersWidth,
} = usePanelWidth({
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_INFRA,
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
minWidth: QUICK_FILTERS_MIN_WIDTH,
maxWidth: QUICK_FILTERS_MAX_WIDTH,
});
const [, setCurrentPage] = useInfraMonitoringPageListing();
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
@@ -139,7 +155,18 @@ function Hosts(): JSX.Element {
<div className={styles.infraMonitoringContainer}>
<div className={styles.infraContentRow}>
{showFilters && (
<div className={styles.quickFiltersContainer}>
<ResizableBox
handle="right"
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
initialWidth={quickFiltersInitialWidth}
minWidth={QUICK_FILTERS_MIN_WIDTH}
maxWidth={QUICK_FILTERS_MAX_WIDTH}
onResize={persistQuickFiltersWidth}
resetToDefaultOnDoubleClick
withHandle
className={styles.quickFiltersContainer}
handleTestId="quick-filters-resize-handle"
>
<div className={styles.quickFiltersContainerHeader}>
<Typography.Text>Filters</Typography.Text>
<Tooltip title="Collapse Filters">
@@ -156,7 +183,7 @@ function Hosts(): JSX.Element {
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleQuickFiltersChange}
/>
</div>
</ResizableBox>
)}
<div
className={`${styles.listContainer}${
@@ -189,8 +216,6 @@ function Hosts(): JSX.Element {
queryKeyPrefix="hosts"
tabsConfig={{
showEvents: false,
showContainers: true,
showProcesses: true,
}}
/>
</>

View File

@@ -44,26 +44,20 @@
}
.quickFiltersContainer {
width: 280px;
min-width: 280px;
// Width is owned by ResizableBox (inline style). No overflow here: the panel
// must not scroll (QuickFilters scrolls its own list internally) — an
// overflow on the root would also clip the edge-mounted resize grip.
flex-shrink: 0;
border-right: 1px solid var(--l1-border);
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
:global(.resizable-box__content) {
display: flex;
flex-direction: column;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--accent-primary);
}
&::-webkit-scrollbar-thumb:hover {
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
:global(.quick-filters-container) {
flex: 1;
min-height: 0;
}
:global(.ant-collapse-header) {
@@ -142,7 +136,9 @@
}
.listContainerFiltersVisible {
max-width: calc(100% - 280px);
// Width is driven by flex now that the filters panel is resizable; the list
// fills whatever space the (variable-width) panel leaves.
max-width: 100%;
}
.quickFiltersToggleContainer {

View File

@@ -0,0 +1,225 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { InfraMonitoringEvents } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import K8sBaseDetails from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
import { K8sBaseList } from 'container/InfraMonitoringK8sV2/Base/K8sBaseList';
import { K8sBaseFilters } from 'container/InfraMonitoringK8sV2/Base/types';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import {
useInfraMonitoringFiltersK8s,
useInfraMonitoringPageListing,
} from 'container/InfraMonitoringK8sV2/hooks';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { ResizableBox } from 'periscope/components/ResizableBox';
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
import { useAppContext } from 'providers/App/App';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import {
fetchHostEntityData,
fetchHostListData,
getHostMetricsQueryPayload,
hostDetailsMetadataConfig,
hostGetEntityName,
hostGetSelectedItemFilters,
hostInitialEventsFilter,
hostInitialLogTracesFilter,
hostWidgetInfo,
} from './constants';
import {
getHostItemKey,
getHostRowKey,
hostColumnsConfig,
} from './table.config';
import { getHostsQuickFiltersConfig } from './utils';
import styles from './InfraMonitoringHosts.module.scss';
import { ArrowUpToLine, Filter } from '@signozhq/icons';
const QUICK_FILTERS_DEFAULT_WIDTH = 280;
const QUICK_FILTERS_MIN_WIDTH = 240;
const QUICK_FILTERS_MAX_WIDTH = 500;
function Hosts(): JSX.Element {
const [showFilters, setShowFilters] = useState(true);
const {
initialWidth: quickFiltersInitialWidth,
persistWidth: persistQuickFiltersWidth,
} = usePanelWidth({
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_INFRA,
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
minWidth: QUICK_FILTERS_MIN_WIDTH,
maxWidth: QUICK_FILTERS_MAX_WIDTH,
});
const [, setCurrentPage] = useInfraMonitoringPageListing();
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const { currentQuery } = useQueryBuilder();
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
entityVersion: '',
});
// Track previous urlFilters to only sync when the value actually changes
// (not when handleChangeQueryData changes due to query updates)
const prevUrlFiltersRef = useRef<string | null>(null);
useEffect(() => {
const currentFiltersJson = urlFilters ? JSON.stringify(urlFilters) : null;
// Only sync if urlFilters value has actually changed
if (prevUrlFiltersRef.current !== currentFiltersJson) {
prevUrlFiltersRef.current = currentFiltersJson;
// Sync filters to query builder, using empty filter when urlFilters is null
handleChangeQueryData('filters', urlFilters || { items: [], op: 'and' });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [urlFilters]); // handleChangeQueryData intentionally omitted - we call the current version but don't re-run when it changes
const handleFilterVisibilityChange = (): void => {
setShowFilters(!showFilters);
};
const handleQuickFiltersChange = (query: Query): void => {
const filters = query.builder.queryData[0].filters;
// Nuqs batches these calls into a single URL update
// The useEffect will sync filters to query builder
setUrlFilters(filters || null);
setCurrentPage(1);
logEvent(InfraMonitoringEvents.FilterApplied, {
entity: InfraMonitoringEvents.HostEntity,
page: InfraMonitoringEvents.ListPage,
});
};
const fetchListData = useCallback(
async (filters: K8sBaseFilters, signal?: AbortSignal) => {
filters.orderBy ||= {
columnName: 'cpu',
order: 'desc',
};
return fetchHostListData(filters, signal);
},
[],
);
const fetchEntityData = useCallback(
async (
filters: Parameters<typeof fetchHostEntityData>[0],
signal?: AbortSignal,
) => fetchHostEntityData(filters, signal),
[],
);
const getSelectedItemFilters = useCallback(
(selectedItem: string) =>
hostGetSelectedItemFilters(selectedItem, dotMetricsEnabled),
[dotMetricsEnabled],
);
const getInitialLogTracesFilters = useCallback(
(host: import('api/infraMonitoring/getHostLists').HostData) =>
hostInitialLogTracesFilter(host, dotMetricsEnabled),
[dotMetricsEnabled],
);
const controlListPrefix = !showFilters ? (
<div className={styles.quickFiltersToggleContainer}>
<Button
className="periscope-btn ghost"
type="text"
size="small"
onClick={handleFilterVisibilityChange}
>
<Filter size={14} />
</Button>
</div>
) : undefined;
return (
<>
<div className={styles.infraMonitoringContainer}>
<div className={styles.infraContentRow}>
{showFilters && (
<ResizableBox
handle="right"
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
initialWidth={quickFiltersInitialWidth}
minWidth={QUICK_FILTERS_MIN_WIDTH}
maxWidth={QUICK_FILTERS_MAX_WIDTH}
onResize={persistQuickFiltersWidth}
resetToDefaultOnDoubleClick
withHandle
className={styles.quickFiltersContainer}
handleTestId="quick-filters-resize-handle"
>
<div className={styles.quickFiltersContainerHeader}>
<Typography.Text>Filters</Typography.Text>
<Tooltip title="Collapse Filters">
<ArrowUpToLine
style={{ rotate: '270deg', cursor: 'pointer' }}
onClick={handleFilterVisibilityChange}
size="md"
/>
</Tooltip>
</div>
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={getHostsQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleQuickFiltersChange}
/>
</ResizableBox>
)}
<div
className={`${styles.listContainer}${
showFilters ? ` ${styles.listContainerFiltersVisible}` : ''
}`}
>
<K8sBaseList
controlListPrefix={controlListPrefix}
entity={InfraMonitoringEntity.HOSTS}
tableColumns={hostColumnsConfig}
fetchListData={fetchListData}
getRowKey={getHostRowKey}
getItemKey={getHostItemKey}
eventCategory={InfraMonitoringEvents.HostEntity}
/>
</div>
</div>
</div>
<K8sBaseDetails
category={InfraMonitoringEntity.HOSTS}
eventCategory={InfraMonitoringEvents.HostEntity}
getSelectedItemFilters={getSelectedItemFilters}
fetchEntityData={fetchEntityData}
getEntityName={hostGetEntityName}
getInitialLogTracesFilters={getInitialLogTracesFilters}
getInitialEventsFilters={hostInitialEventsFilter}
metadataConfig={hostDetailsMetadataConfig}
entityWidgetInfo={hostWidgetInfo}
getEntityQueryPayload={getHostMetricsQueryPayload}
queryKeyPrefix="hosts"
tabsConfig={{
showEvents: false,
}}
/>
</>
);
}
export default Hosts;

View File

@@ -0,0 +1,171 @@
.infraMonitoringContainer {
display: flex;
height: 100%;
flex-direction: column;
}
.infraContentRow {
display: flex;
flex-direction: row;
height: calc(100vh - 45px);
width: 100%;
position: relative;
overflow-y: auto;
:global(.periscope-btn) {
&.ghost:not(:disabled) {
border: none;
background: transparent;
&:hover {
background: transparent;
color: var(--bg-robin-500) !important;
font-weight: 500;
}
}
}
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--accent-primary);
}
&::-webkit-scrollbar-thumb:hover {
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
}
.quickFiltersContainer {
// Width is owned by ResizableBox (inline style). No overflow here: the panel
// must not scroll (QuickFilters scrolls its own list internally) — an
// overflow on the root would also clip the edge-mounted resize grip.
flex-shrink: 0;
border-right: 1px solid var(--l1-border);
:global(.resizable-box__content) {
display: flex;
flex-direction: column;
}
:global(.quick-filters-container) {
flex: 1;
min-height: 0;
}
:global(.ant-collapse-header) {
border-bottom: 1px solid var(--l1-border);
padding: 12px 8px;
}
:global(.ant-collapse-content-box) {
padding: 0 !important;
padding-block: 0 !important;
:global(.quick-filters .checkbox-filter) {
padding-left: 18px;
}
}
:global(.quick-filters) {
overflow-y: auto;
overflow-x: hidden;
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--accent-primary);
}
&::-webkit-scrollbar-thumb:hover {
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
}
}
.quickFiltersContainerHeader {
padding: 8px;
border-bottom: 1px solid var(--l1-border);
display: flex;
align-items: center;
justify-content: space-between;
}
.listContainer {
flex: 1;
min-width: 0;
max-width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
> * {
min-width: 0;
max-width: 100%;
box-sizing: border-box;
}
> :global(.ant-table-wrapper) {
width: 100%;
max-width: 100%;
:global(.ant-spin-container) {
display: flex !important;
flex-direction: column;
}
:global(.ant-table),
:global(.ant-table-container) {
max-width: 100%;
}
}
}
.listContainerFiltersVisible {
// Width is driven by flex now that the filters panel is resizable; the list
// fills whatever space the (variable-width) panel leaves.
max-width: 100%;
}
.quickFiltersToggleContainer {
padding: 0 8px;
}
.infraMonitoringTags {
width: fit-content;
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px;
letter-spacing: 0.44px;
text-transform: uppercase;
border-radius: 50px;
padding: 2px 8px;
}
.tagsActive {
color: var(--bg-forest-500, #25e192);
border: 1px solid rgba(37, 225, 146, 0.2);
background: rgba(37, 225, 146, 0.1);
}
.tagsInactive {
color: var(--bg-slate-50, #62687c);
border: 1px solid rgba(98, 104, 124, 0.2);
background: rgba(98, 104, 124, 0.1);
}

View File

@@ -0,0 +1,191 @@
import React from 'react';
import { Color } from '@signozhq/design-tokens';
import { Badge } from '@signozhq/ui/badge';
import { Progress } from '@signozhq/ui/progress';
import { Typography } from '@signozhq/ui/typography';
import {
getHostLists,
HostData,
HostListPayload,
} from 'api/infraMonitoring/getHostLists';
import {
createFilterItem,
K8sDetailsFilters,
K8sDetailsMetadataConfig,
} from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
import { K8sBaseFilters } from 'container/InfraMonitoringK8sV2/Base/types';
import {
getHostQueryPayload,
hostWidgetInfo,
} from 'container/LogDetailedView/InfraMetrics/constants';
import {
TagFilter,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import { getHostListsQuery } from './utils';
import infraHostsStyles from './InfraMonitoringHosts.module.scss';
export function getProgressColor(percent: number): string {
if (percent >= 90) {
return Color.BG_SAKURA_500;
}
if (percent >= 60) {
return Color.BG_AMBER_500;
}
return Color.BG_FOREST_500;
}
export function getMemoryProgressColor(percent: number): string {
if (percent >= 90) {
return Color.BG_CHERRY_500;
}
if (percent >= 60) {
return Color.BG_AMBER_500;
}
return Color.BG_FOREST_500;
}
export const hostDetailsMetadataConfig: K8sDetailsMetadataConfig<HostData>[] = [
{
label: 'STATUS',
getValue: (h): string => (h.active ? 'ACTIVE' : 'INACTIVE'),
render: (value, h): React.ReactNode => (
<Badge
variant="outline"
className={`${infraHostsStyles.infraMonitoringTags} ${
h.active ? infraHostsStyles.tagsActive : infraHostsStyles.tagsInactive
}`}
>
{value}
</Badge>
),
},
{
label: 'OPERATING SYSTEM',
getValue: (h): string => h.os || '-',
render: (value): React.ReactNode =>
value !== '-' ? (
<Badge variant="outline" className={infraHostsStyles.infraMonitoringTags}>
{value}
</Badge>
) : (
<Typography.Text>-</Typography.Text>
),
},
{
label: 'CPU USAGE',
getValue: (h): number => h.cpu * 100,
render: (value): React.ReactNode => (
<Progress
percent={Number(Number(value).toFixed(1))}
strokeColor={getProgressColor(Number(value))}
showInfo
/>
),
},
{
label: 'MEMORY USAGE',
getValue: (h): number => h.memory * 100,
render: (value): React.ReactNode => (
<Progress
percent={Number(Number(value).toFixed(1))}
strokeColor={getMemoryProgressColor(Number(value))}
showInfo
/>
),
},
];
export function getHostMetricsQueryPayload(
host: HostData,
start: number,
end: number,
dotMetricsEnabled: boolean,
): ReturnType<typeof getHostQueryPayload> {
return getHostQueryPayload(host.hostName, start, end, dotMetricsEnabled);
}
export { hostWidgetInfo };
export function hostGetSelectedItemFilters(
selectedItem: string,
dotMetricsEnabled: boolean,
): TagFilter {
const hostKey = dotMetricsEnabled ? 'host.name' : 'host_name';
return {
op: 'AND',
items: [createFilterItem(hostKey, selectedItem)],
};
}
export function hostInitialLogTracesFilter(
host: HostData,
dotMetricsEnabled: boolean,
): TagFilterItem[] {
const hostKey = dotMetricsEnabled ? 'host.name' : 'host_name';
return [createFilterItem(hostKey, host.hostName || '')];
}
export function hostInitialEventsFilter(_host: HostData): TagFilterItem[] {
return [];
}
export const hostGetEntityName = (host: HostData): string => host.hostName;
export async function fetchHostListData(
filters: K8sBaseFilters,
signal?: AbortSignal,
): Promise<{
data: HostData[];
total: number;
error?: string | null;
rawData?: unknown;
}> {
const baseQuery = getHostListsQuery();
const payload: HostListPayload = {
...baseQuery,
limit: filters.limit,
offset: filters.offset,
filters: filters.filters ?? { items: [], op: 'and' },
orderBy: filters.orderBy,
start: filters.start,
end: filters.end,
groupBy: filters.groupBy ?? [],
};
const response = await getHostLists(payload, signal);
return {
data: response.payload?.data?.records || [],
total: response.payload?.data?.total || 0,
error: response.error,
rawData: response.payload?.data,
};
}
export async function fetchHostEntityData(
filters: K8sDetailsFilters,
signal?: AbortSignal,
): Promise<{ data: HostData | null; error?: string | null }> {
const response = await getHostLists(
{
...getHostListsQuery(),
filters: filters.filters,
start: filters.start,
end: filters.end,
limit: 1,
offset: 0,
groupBy: [],
},
signal,
);
const records = response.payload?.data?.records || [];
return {
data: records.length > 0 ? records[0] : null,
error: response.error,
};
}

View File

@@ -0,0 +1,69 @@
import { useEffect } from 'react';
import * as Sentry from '@sentry/react';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { DataSource } from 'types/common/queryBuilder';
import Hosts from './Hosts';
function InfraMonitoringHosts(): JSX.Element {
const {
updateAllQueriesOperators,
handleSetConfig,
setSupersetQuery,
setLastUsedQuery,
currentQuery,
resetQuery,
} = useQueryBuilder();
useEffect(() => {
const newQuery = updateAllQueriesOperators(
initialQueriesMap.metrics,
PANEL_TYPES.TIME_SERIES,
DataSource.METRICS,
);
setSupersetQuery(newQuery);
setLastUsedQuery(0);
handleSetConfig(PANEL_TYPES.TIME_SERIES, DataSource.METRICS);
return (): void => {
setLastUsedQuery(0);
};
}, [
updateAllQueriesOperators,
setSupersetQuery,
setLastUsedQuery,
handleSetConfig,
]);
useEffect(() => {
const updatedCurrentQuery = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
filters: {
items: [],
op: 'AND',
},
},
],
},
};
resetQuery(updatedCurrentQuery);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<Hosts />
</Sentry.ErrorBoundary>
);
}
export default InfraMonitoringHosts;

View File

@@ -0,0 +1,201 @@
import React from 'react';
import { Tooltip } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import { HostData } from 'api/infraMonitoring/getHostLists';
import TanStackTable, { TableColumnDef } from 'components/TanStackTableView';
import { getGroupByEl } from 'container/InfraMonitoringK8sV2/Base/utils';
import {
EntityProgressBar,
ExpandButtonWrapper,
ValidateColumnValueWrapper,
} from 'container/InfraMonitoringK8sV2/components';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
import { useInfraMonitoringGroupBy } from 'container/InfraMonitoringK8sV2/hooks';
import EntityGroupHeader from 'container/InfraMonitoringK8sV2/Base/EntityGroupHeader';
import { HostnameCell } from './utils';
import styles from './table.module.scss';
import { Container, Info } from '@signozhq/icons';
function hostRowSource(host: HostData): { meta: Record<string, string> } {
return {
meta: {
...(host.meta ?? {}),
host_name: host.hostName ?? '',
'host.name': host.hostName ?? '',
os_type: host.os ?? '',
'os.type': host.os ?? '',
},
};
}
export function getHostRowKey(host: HostData): string {
return host.hostName || 'unknown';
}
export function getHostItemKey(host: HostData): string {
return host.hostName ?? '';
}
function HostGroupCell({ row }: { row: HostData }): JSX.Element {
const [groupBy] = useInfraMonitoringGroupBy();
const synthetic = hostRowSource(row);
return getGroupByEl(synthetic, groupBy) as JSX.Element;
}
export const hostColumnsConfig: TableColumnDef<HostData>[] = [
{
id: 'hostGroup',
header: (): React.ReactNode => <EntityGroupHeader title="HOST GROUP" />,
accessorFn: (row): string => row.hostName ?? '',
width: { min: 300 },
enableSort: false,
enableRemove: false,
enableMove: false,
pin: 'left',
visibilityBehavior: 'hidden-on-collapse',
cell: ({ row, isExpanded, toggleExpanded }): React.ReactNode => (
<ExpandButtonWrapper isExpanded={isExpanded} toggleExpanded={toggleExpanded}>
<HostGroupCell row={row} />
</ExpandButtonWrapper>
),
},
{
id: 'hostName',
header: (): React.ReactNode => (
<EntityGroupHeader title="Hostname" icon={<Container size={14} />} />
),
accessorFn: (row): string => row.hostName ?? '',
width: { min: 290 },
enableSort: false,
enableRemove: false,
enableMove: false,
pin: 'left',
visibilityBehavior: 'hidden-on-expand',
cell: ({ value }): React.ReactNode => (
<HostnameCell hostName={value as string} />
),
},
{
id: 'active',
header: (): React.ReactNode => (
<div className={styles.statusHeader}>
Status
<Tooltip title="Sent system metrics in last 10 mins">
<Info size="md" />
</Tooltip>
</div>
),
accessorFn: (row): boolean => row.active,
width: { min: 150, default: 150 },
enableSort: false,
cell: ({ value }): React.ReactNode => {
const active = value as boolean;
return (
<Badge
className={`${styles.statusTag} ${
active ? styles.statusTagActive : styles.statusTagInactive
}`}
>
{active ? 'ACTIVE' : 'INACTIVE'}
</Badge>
);
},
},
{
id: 'cpu',
header: (): React.ReactNode => (
<div className={styles.columnHeaderRight}>CPU Usage</div>
),
accessorFn: (row): number => row.cpu,
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const cpu = value as number;
return (
<div className={styles.progressContainer}>
<ValidateColumnValueWrapper
value={cpu}
entity={InfraMonitoringEntity.HOSTS}
attribute="CPU metric"
>
<EntityProgressBar value={cpu} type="cpu" />
</ValidateColumnValueWrapper>
</div>
);
},
},
{
id: 'memory',
header: (): React.ReactNode => (
<div className={`${styles.columnHeaderRight} ${styles.memoryUsageHeader}`}>
Memory Usage
<Tooltip title="Excluding cache memory">
<Info size="md" />
</Tooltip>
</div>
),
accessorFn: (row): number => row.memory,
width: { min: 220 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const memory = value as number;
return (
<div className={styles.progressContainer}>
<ValidateColumnValueWrapper
value={memory}
entity={InfraMonitoringEntity.HOSTS}
attribute="memory metric"
>
<EntityProgressBar value={memory} type="memory" />
</ValidateColumnValueWrapper>
</div>
);
},
},
{
id: 'wait',
header: (): React.ReactNode => (
<div className={styles.columnHeaderRight}>IOWait</div>
),
accessorFn: (row): number => row.wait,
width: { min: 100, default: 100 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const wait = value as number;
return (
<ValidateColumnValueWrapper
value={wait}
entity={InfraMonitoringEntity.HOSTS}
attribute="IOWait metric"
>
<TanStackTable.Text>{`${Number((wait * 100).toFixed(1))}%`}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
{
id: 'load15',
header: (): React.ReactNode => (
<div className={styles.columnHeaderRight}>Load Avg</div>
),
accessorFn: (row): number => row.load15,
width: { min: 100, default: 100 },
enableSort: true,
cell: ({ value }): React.ReactNode => {
const load15 = value as number;
return (
<ValidateColumnValueWrapper
value={load15}
entity={InfraMonitoringEntity.HOSTS}
attribute="load average metric"
>
<TanStackTable.Text>{load15}</TanStackTable.Text>
</ValidateColumnValueWrapper>
);
},
},
];

View File

@@ -0,0 +1,68 @@
.entityGroupHeader {
padding-left: var(--spacing-5);
gap: var(--spacing-5);
display: flex;
align-items: center;
}
.hostnameColumnHeader {
display: block;
}
.statusHeader {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
width: 100%;
}
.columnHeaderRight {
text-align: right;
}
.memoryUsageHeader {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: var(--spacing-2);
width: 100%;
}
.statusTag {
width: fit-content;
font-family: Inter, sans-serif;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px;
letter-spacing: 0.44px;
text-transform: uppercase;
border-radius: 50px;
padding: 2px 8px;
}
.statusTagActive {
color: var(--Forest-500, #25e192);
border: 1px solid rgba(37, 225, 146, 0.2);
background: rgba(37, 225, 146, 0.1);
}
.statusTagInactive {
color: var(--Slate-50, #62687c);
border: 1px solid rgba(98, 104, 124, 0.2);
background: rgba(98, 104, 124, 0.1);
}
.progressContainer {
display: flex;
align-items: center;
& > div {
width: 100%;
}
}
.progressBar {
flex: 1;
margin-right: 8px;
}

View File

@@ -0,0 +1,156 @@
import React from 'react';
import { Color } from '@signozhq/design-tokens';
import { Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
import {
FiltersType,
IQuickFiltersConfig,
} from 'components/QuickFilters/types';
import { TriangleAlert } from '@signozhq/icons';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
const HOSTNAME_DOCS_URL =
'https://signoz.io/docs/infrastructure-monitoring/hostmetrics/#host-name-is-blankempty';
export function HostnameCell({
hostName,
}: {
hostName?: string | null;
}): React.ReactElement {
const isEmpty = !hostName || !hostName.trim();
if (!isEmpty) {
return <div className="hostname-column-value">{hostName}</div>;
}
return (
<div className="hostname-cell-missing">
<Typography.Text color="muted" className="hostname-cell-placeholder">
-
</Typography.Text>
<Tooltip
title={
<div>
Missing host.name metadata.
<br />
<a
href={HOSTNAME_DOCS_URL}
target="_blank"
rel="noopener noreferrer"
onClick={(e): void => e.stopPropagation()}
>
Learn how to configure
</a>
</div>
}
trigger={['hover', 'focus']}
>
<span
className="hostname-cell-warning-icon"
tabIndex={0}
role="img"
aria-label="Missing host.name metadata"
onClick={(e): void => e.stopPropagation()}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
e.stopPropagation();
}
}}
>
<TriangleAlert size={14} color={Color.BG_CHERRY_500} />
</span>
</Tooltip>
</div>
);
}
export const getHostListsQuery = (): HostListPayload => ({
filters: {
items: [],
op: 'and',
},
groupBy: [],
orderBy: { columnName: 'cpu', order: 'desc' },
});
export const HostsQuickFiltersConfig: IQuickFiltersConfig[] = [
{
type: FiltersType.CHECKBOX,
title: 'Host Name',
attributeKey: {
key: 'host_name',
dataType: DataTypes.String,
type: 'resource',
},
aggregateOperator: 'noop',
aggregateAttribute: 'system_cpu_load_average_15m',
dataSource: DataSource.METRICS,
defaultOpen: true,
},
{
type: FiltersType.CHECKBOX,
title: 'OS Type',
attributeKey: {
key: 'os_type',
dataType: DataTypes.String,
type: 'resource',
},
aggregateOperator: 'noop',
aggregateAttribute: 'system_cpu_load_average_15m',
dataSource: DataSource.METRICS,
defaultOpen: true,
},
];
export function getHostsQuickFiltersConfig(
dotMetricsEnabled: boolean,
): IQuickFiltersConfig[] {
const hostNameKey = dotMetricsEnabled ? 'host.name' : 'host_name';
const osTypeKey = dotMetricsEnabled ? 'os.type' : 'os_type';
const metricName = dotMetricsEnabled
? 'system.cpu.load_average.15m'
: 'system_cpu_load_average_15m';
const environmentKey = dotMetricsEnabled
? 'deployment.environment'
: 'deployment_environment';
return [
{
type: FiltersType.CHECKBOX,
title: 'Host Name',
attributeKey: {
key: hostNameKey,
dataType: DataTypes.String,
type: 'resource',
},
aggregateOperator: 'noop',
aggregateAttribute: metricName,
dataSource: DataSource.METRICS,
defaultOpen: true,
},
{
type: FiltersType.CHECKBOX,
title: 'OS Type',
attributeKey: {
key: osTypeKey,
dataType: DataTypes.String,
type: 'resource',
},
aggregateOperator: 'noop',
aggregateAttribute: metricName,
dataSource: DataSource.METRICS,
defaultOpen: true,
},
{
type: FiltersType.CHECKBOX,
title: 'Environment',
attributeKey: {
key: environmentKey,
dataType: DataTypes.String,
type: 'resource',
},
defaultOpen: true,
},
];
}

View File

@@ -35,7 +35,6 @@ import {
ChevronsLeftRight,
Compass,
DraftingCompass,
Package2,
ScrollText,
X,
} from '@signozhq/icons';
@@ -54,12 +53,10 @@ import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import { InfraMonitoringEntity, VIEW_TYPES } from '../constants';
import EntityContainers from '../EntityDetailsUtils/EntityContainers';
import EntityEvents from '../EntityDetailsUtils/EntityEvents';
import EntityLogs from '../EntityDetailsUtils/EntityLogs';
import { K8S_ENTITY_LOGS_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityLogs/hooks';
import EntityMetrics from '../EntityDetailsUtils/EntityMetrics';
import EntityProcesses from '../EntityDetailsUtils/EntityProcesses';
import EntityTraces from '../EntityDetailsUtils/EntityTraces';
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityTraces/hooks';
import {
@@ -120,8 +117,6 @@ export interface K8sBaseDetailsProps<T> {
showLogs?: boolean;
showTraces?: boolean;
showEvents?: boolean;
showContainers?: boolean;
showProcesses?: boolean;
};
customTabs?: Array<{
key: string;
@@ -259,8 +254,6 @@ export default function K8sBaseDetails<T>({
showLogs: true,
showTraces: true,
showEvents: true,
showContainers: false,
showProcesses: false,
...tabsConfig,
}),
[tabsConfig],
@@ -575,32 +568,6 @@ export default function K8sBaseDetails<T>({
},
]
: []),
...(tabVisibility.showContainers
? [
{
value: VIEW_TYPES.CONTAINERS,
label: (
<div className="view-title">
<Package2 size={14} />
Containers
</div>
),
},
]
: []),
...(tabVisibility.showProcesses
? [
{
value: VIEW_TYPES.PROCESSES,
label: (
<div className="view-title">
<ChevronsLeftRight size={14} />
Processes
</div>
),
},
]
: []),
...(customTabs?.map((tab) => ({
value: tab.key,
label: (
@@ -680,11 +647,6 @@ export default function K8sBaseDetails<T>({
initialExpression={eventsInitialExpression}
/>
)}
{effectiveView === VIEW_TYPES.CONTAINERS &&
tabVisibility.showContainers && <EntityContainers />}
{effectiveView === VIEW_TYPES.PROCESSES && tabVisibility.showProcesses && (
<EntityProcesses />
)}
{customTabs?.map((tab) =>
selectedView === tab.key ? (
<React.Fragment key={tab.key}>

View File

@@ -1,47 +0,0 @@
import { useTranslation } from 'react-i18next';
import { Space } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import WaitlistFragment from 'components/HostMetricsDetail/WaitlistFragment/WaitlistFragment';
import broomUrl from '@/assets/Icons/broom.svg';
import infraContainersUrl from '@/assets/Icons/infraContainers.svg';
import 'components/HostMetricsDetail/Containers/Containers.styles.scss';
const { Text } = Typography;
function EntityContainers(): JSX.Element {
const { t } = useTranslation(['infraMonitoring']);
return (
<Space direction="vertical" className="host-containers" size={24}>
<div className="infra-container-card-container">
<div className="dev-status-container">
<div className="infra-container-card">
<img
src={infraContainersUrl}
alt="infra-container"
width={32}
height={32}
/>
<Text className="infra-container-card-text">
{t('containers_visualization_message')}
</Text>
</div>
<div className="infra-container-working-msg">
<Space>
<img src={broomUrl} alt="broom" width={24} height={24} />
<Text className="infra-container-card-text">{t('working_message')}</Text>
</Space>
</div>
</div>
<WaitlistFragment entityType="containers" />
</div>
</Space>
);
}
export default EntityContainers;

View File

@@ -1,46 +0,0 @@
import { useTranslation } from 'react-i18next';
import { Space } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import WaitlistFragment from 'components/HostMetricsDetail/WaitlistFragment/WaitlistFragment';
import broomUrl from '@/assets/Icons/broom.svg';
import infraContainersUrl from '@/assets/Icons/infraContainers.svg';
import 'components/HostMetricsDetail/Processes/Processes.styles.scss';
const { Text } = Typography;
function EntityProcesses(): JSX.Element {
const { t } = useTranslation(['infraMonitoring']);
return (
<Space direction="vertical" className="host-processes" size={24}>
<div className="infra-container-card-container">
<div className="dev-status-container">
<div className="infra-container-card">
<img
src={infraContainersUrl}
alt="infra-container"
width={32}
height={32}
/>
<Text className="infra-container-card-text">
{t('processes_visualization_message')}
</Text>
</div>
<div className="infra-container-working-msg">
<Space>
<img src={broomUrl} alt="broom" width={24} height={24} />
<Text className="infra-container-card-text">{t('working_message')}</Text>
</Space>
</div>
</div>
<WaitlistFragment entityType="processes" />
</div>
</Space>
);
}
export default EntityProcesses;

View File

@@ -0,0 +1,98 @@
import { act, waitFor } from 'tests/test-utils';
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
import { renderEntityTraces, verifyQueryPayload } from './testUtils';
import { mockQueryRangeV5WithTracesResponse } from '__tests__/query_range_v5.util';
describe('EntityTraces - Default Behavior', () => {
let capturedPayloads: QueryRangePayloadV5[] = [];
beforeEach(() => {
capturedPayloads = [];
mockQueryRangeV5WithTracesResponse({
onReceiveRequest: async (req) => {
const body = (await req.json()) as QueryRangePayloadV5;
capturedPayloads.push(body);
return {};
},
});
});
it('should fetch traces using V5 API on initial render', async () => {
act(() => {
renderEntityTraces();
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
verifyQueryPayload({
payload: capturedPayloads[0],
expectedOffset: 0,
});
});
it('should pass time range to API (converted to milliseconds)', async () => {
const timeRange = { startTime: 1000, endTime: 2000 };
act(() => {
renderEntityTraces({ timeRange });
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
verifyQueryPayload({
payload: capturedPayloads[0],
expectedTimeRange: {
start: timeRange.startTime * 1000,
end: timeRange.endTime * 1000,
},
});
});
it('should order results by timestamp desc', async () => {
act(() => {
renderEntityTraces();
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
const spec = capturedPayloads[0].compositeQuery.queries[0]?.spec as {
order?: Array<{ key: { name: string }; direction: string }>;
};
const timestampOrder = spec.order?.find((o) => o.key.name === 'timestamp');
expect(timestampOrder?.direction).toBe('desc');
});
it('should use TRACES signal type', async () => {
act(() => {
renderEntityTraces();
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
const query = capturedPayloads[0].compositeQuery.queries[0];
const spec = query?.spec as { signal?: string };
expect(spec?.signal).toBe('traces');
});
it('should use raw request type for traces list view', async () => {
act(() => {
renderEntityTraces();
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
expect(capturedPayloads[0].requestType).toBe('raw');
});
});

View File

@@ -0,0 +1,38 @@
import { act, screen } from 'tests/test-utils';
import { renderEntityTraces } from './testUtils';
import { mockQueryRangeV5WithEmptyTraces } from '__tests__/query_range_v5.util';
describe('EntityTraces - Empty State', () => {
it('should show empty state when no traces returned', async () => {
mockQueryRangeV5WithEmptyTraces();
act(() => {
renderEntityTraces();
});
await expect(screen.findByText(/No data yet/i)).resolves.toBeInTheDocument();
});
it('should not show table when traces are empty', async () => {
mockQueryRangeV5WithEmptyTraces();
act(() => {
renderEntityTraces();
});
await expect(screen.findByText(/No data yet/i)).resolves.toBeInTheDocument();
expect(screen.queryByRole('table')).not.toBeInTheDocument();
});
it('should not show pagination controls when traces are empty', async () => {
mockQueryRangeV5WithEmptyTraces();
act(() => {
renderEntityTraces();
});
await expect(screen.findByText(/No data yet/i)).resolves.toBeInTheDocument();
expect(screen.queryByText(/1 - 10/)).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,44 @@
import { act, screen } from 'tests/test-utils';
import { renderEntityTraces } from './testUtils';
import {
mockQueryRangeV5WithError,
mockQueryRangeV5WithKeyNotFoundError,
} from '__tests__/query_range_v5.util';
describe('EntityTraces - Error State', () => {
it('should show error state on API error', async () => {
mockQueryRangeV5WithError('Internal server error', 500);
act(() => {
renderEntityTraces();
});
await expect(
screen.findByText(/Something went wrong/i),
).resolves.toBeInTheDocument();
});
it('should show empty state for key not found error', async () => {
mockQueryRangeV5WithKeyNotFoundError();
act(() => {
renderEntityTraces();
});
await expect(screen.findByText(/No data yet/i)).resolves.toBeInTheDocument();
});
it('should not show table on API error', async () => {
mockQueryRangeV5WithError('Internal server error', 500);
act(() => {
renderEntityTraces();
});
await expect(
screen.findByText(/Something went wrong/i),
).resolves.toBeInTheDocument();
expect(screen.queryByRole('table')).not.toBeInTheDocument();
});
});

View File

@@ -0,0 +1,55 @@
import { act, waitFor } from 'tests/test-utils';
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
import { renderEntityTraces } from './testUtils';
import { mockQueryRangeV5WithTracesResponse } from '__tests__/query_range_v5.util';
describe('EntityTraces - Expression', () => {
let capturedPayloads: QueryRangePayloadV5[] = [];
beforeEach(() => {
capturedPayloads = [];
mockQueryRangeV5WithTracesResponse({
onReceiveRequest: async (req) => {
const body = (await req.json()) as QueryRangePayloadV5;
capturedPayloads.push(body);
return {};
},
});
});
it('should include expression in query filter', async () => {
const expression = 'k8s.pod.name = "my-pod"';
act(() => {
renderEntityTraces({ expression });
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
const spec = capturedPayloads[0].compositeQuery.queries[0]?.spec as {
filter?: { expression: string };
};
expect(spec.filter?.expression).toContain('k8s.pod.name');
});
it('should include complex expression with multiple conditions', async () => {
const expression = 'k8s.pod.name = "my-pod" AND service.name = "api"';
act(() => {
renderEntityTraces({ expression });
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
const spec = capturedPayloads[0].compositeQuery.queries[0]?.spec as {
filter?: { expression: string };
};
expect(spec.filter?.expression).toContain('k8s.pod.name');
expect(spec.filter?.expression).toContain('service.name');
});
});

View File

@@ -0,0 +1,39 @@
import { act, screen, waitFor } from 'tests/test-utils';
import { renderEntityTraces } from './testUtils';
import { mockQueryRangeV5WithTracesResponse } from '__tests__/query_range_v5.util';
describe('EntityTraces - Loading State', () => {
it('should show loading state while fetching', async () => {
mockQueryRangeV5WithTracesResponse({ delay: 1000 });
act(() => {
renderEntityTraces();
});
await expect(
screen.findByText(/pending_data_placeholder/i),
).resolves.toBeInTheDocument();
});
it('should hide loading state after data loads', async () => {
mockQueryRangeV5WithTracesResponse({ delay: 100 });
act(() => {
renderEntityTraces();
});
await expect(
screen.findByText(/pending_data_placeholder/i),
).resolves.toBeInTheDocument();
await waitFor(
() => {
expect(
screen.queryByText(/pending_data_placeholder/i),
).not.toBeInTheDocument();
},
{ timeout: 3000 },
);
});
});

View File

@@ -0,0 +1,96 @@
import { act, waitFor } from 'tests/test-utils';
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
import { renderEntityTraces, verifyQueryPayload } from './testUtils';
import { mockQueryRangeV5WithTracesResponse } from '__tests__/query_range_v5.util';
describe('EntityTraces - Pagination', () => {
let capturedPayloads: QueryRangePayloadV5[] = [];
beforeEach(() => {
capturedPayloads = [];
mockQueryRangeV5WithTracesResponse({
onReceiveRequest: async (req) => {
const body = (await req.json()) as QueryRangePayloadV5;
capturedPayloads.push(body);
return {};
},
});
});
it('should use default limit when no pagination provided', async () => {
act(() => {
renderEntityTraces();
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
const spec = capturedPayloads[0].compositeQuery.queries[0]?.spec as {
limit?: number;
};
expect(spec.limit).toBe(10);
});
it('should use custom offset from pagination param', async () => {
act(() => {
renderEntityTraces({ pagination: { offset: 20, limit: 10 } });
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
verifyQueryPayload({
payload: capturedPayloads[0],
expectedOffset: 20,
});
});
it('should use custom limit from pagination param', async () => {
act(() => {
renderEntityTraces({ pagination: { offset: 0, limit: 50 } });
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
verifyQueryPayload({
payload: capturedPayloads[0],
expectedLimit: 50,
});
});
it('should default offset to 0', async () => {
act(() => {
renderEntityTraces();
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
verifyQueryPayload({
payload: capturedPayloads[0],
expectedOffset: 0,
});
});
it('should combine offset and limit correctly', async () => {
act(() => {
renderEntityTraces({ pagination: { offset: 30, limit: 20 } });
});
await waitFor(() => {
expect(capturedPayloads).toHaveLength(1);
});
verifyQueryPayload({
payload: capturedPayloads[0],
expectedOffset: 30,
expectedLimit: 20,
});
});
});

View File

@@ -0,0 +1,185 @@
import { act, screen, within } from 'tests/test-utils';
import { renderEntityTraces } from './testUtils';
import { mockQueryRangeV5WithTracesResponse } from '__tests__/query_range_v5.util';
// Trace list columns are hidden below the antd `md` breakpoint. The global
// matchMedia mock reports `matches: false`, which drops every column, so make
// all breakpoints match for these rendering tests.
jest.spyOn(window, 'matchMedia').mockImplementation(
(query: string) =>
({
matches: true,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
}) as unknown as MediaQueryList,
);
describe('EntityTraces - Table Rendering', () => {
it('should render table with all column headers', async () => {
mockQueryRangeV5WithTracesResponse({ pageSize: 3 });
act(() => {
renderEntityTraces();
});
await expect(screen.findByText('Service Name')).resolves.toBeInTheDocument();
const headerTexts = within(screen.getByRole('table'))
.getAllByRole('columnheader')
.map((header) => header.textContent);
expect(headerTexts).toStrictEqual(
expect.arrayContaining([
'Timestamp',
'Service Name',
'Name',
'Duration',
'HTTP Method',
'Status Code',
]),
);
});
it('should render trace values in table cells', async () => {
mockQueryRangeV5WithTracesResponse({
customTraces: [
{
serviceName: 'checkout-service',
name: 'GET /api/checkout',
durationNano: 5000000,
httpMethod: 'GET',
responseStatusCode: '200',
},
],
});
act(() => {
renderEntityTraces();
});
await expect(screen.findByTestId('serviceName')).resolves.toHaveTextContent(
'checkout-service',
);
expect(screen.getByTestId('name')).toHaveTextContent('GET /api/checkout');
expect(screen.getByTestId('durationNano')).toHaveTextContent('5.00ms');
expect(screen.getByTestId('httpMethod')).toHaveTextContent('GET');
expect(screen.getByTestId('responseStatusCode')).toHaveTextContent('200');
});
it('should render http method as robin colored outline badge', async () => {
mockQueryRangeV5WithTracesResponse({
customTraces: [{ httpMethod: 'POST', responseStatusCode: '200' }],
});
act(() => {
renderEntityTraces();
});
const badge = await screen.findByTestId('httpMethod');
expect(badge).toHaveTextContent('POST');
expect(badge).toHaveAttribute('data-color', 'robin');
expect(badge).toHaveAttribute('data-variant', 'outline');
});
it('should render N/A when http method is empty', async () => {
mockQueryRangeV5WithTracesResponse({
customTraces: [{ httpMethod: '', responseStatusCode: '200' }],
});
act(() => {
renderEntityTraces();
});
await expect(screen.findByText('N/A')).resolves.toBeInTheDocument();
expect(screen.queryByTestId('httpMethod')).not.toBeInTheDocument();
});
it('should render status code badge color per status range', async () => {
mockQueryRangeV5WithTracesResponse({
customTraces: [
{ responseStatusCode: '200' },
{ responseStatusCode: '302' },
{ responseStatusCode: '404' },
{ responseStatusCode: '500' },
{ responseStatusCode: '100' },
],
});
act(() => {
renderEntityTraces();
});
const badges = await screen.findAllByTestId('responseStatusCode');
const badgeColors = badges.map((badge) => badge.getAttribute('data-color'));
expect(badgeColors).toStrictEqual([
'forest', // 2xx -> success
'robin', // 3xx -> redirect
'amber', // 4xx -> client error
'cherry', // 5xx -> server error
'vanilla', // 1xx -> informational
]);
});
it('should render non-numeric status code as plain text without badge', async () => {
mockQueryRangeV5WithTracesResponse({
customTraces: [{ responseStatusCode: 'unknown' }],
});
act(() => {
renderEntityTraces();
});
await expect(screen.findByText('unknown')).resolves.toBeInTheDocument();
expect(screen.queryByTestId('responseStatusCode')).not.toBeInTheDocument();
});
it('should link cells to trace details page', async () => {
mockQueryRangeV5WithTracesResponse({
customTraces: [{ serviceName: 'frontend' }],
});
act(() => {
renderEntityTraces();
});
await expect(screen.findByTestId('serviceName')).resolves.toBeInTheDocument();
const links = within(screen.getByRole('table')).getAllByRole('link');
expect(links.length).toBeGreaterThan(0);
links.forEach((link) => {
expect(link).toHaveAttribute(
'href',
expect.stringContaining('/trace/trace-id-0'),
);
});
});
it('should render one row per trace', async () => {
mockQueryRangeV5WithTracesResponse({
customTraces: [
{ serviceName: 'frontend' },
{ serviceName: 'backend' },
{ serviceName: 'database' },
],
});
act(() => {
renderEntityTraces();
});
const serviceCells = await screen.findAllByTestId('serviceName');
expect(serviceCells.map((cell) => cell.textContent)).toStrictEqual([
'frontend',
'backend',
'database',
]);
});
});

View File

@@ -0,0 +1,121 @@
import { ENVIRONMENT } from 'constants/env';
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8s/constants';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, RenderResult } from 'tests/test-utils';
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
import EntityTraces from '../EntityTraces';
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../hooks';
// QuerySearch fires autocomplete requests on mount; without handlers MSW
// passes them through to the real network and the resulting AxiosError fails
// whichever test happens to be running.
beforeEach(() => {
server.use(
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/keys`, (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({ status: 'success', data: { keys: {}, complete: true } }),
),
),
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/values`, (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({ status: 'success', data: { values: {}, complete: true } }),
),
),
);
});
export interface RenderEntityTracesOptions {
expression?: string;
timeRange?: { startTime: number; endTime: number };
category?: InfraMonitoringEntity;
selectedInterval?: '5m' | '15m' | '30m' | '1h';
pagination?: { offset: number; limit: number };
}
export function renderEntityTraces({
expression = 'k8s.pod.name = "test-pod"',
timeRange = { startTime: 1, endTime: 2 },
category = InfraMonitoringEntity.PODS,
selectedInterval = '5m',
pagination,
}: RenderEntityTracesOptions = {}): RenderResult {
const encodedExpression = encodeURIComponent(expression);
let searchParams = `${K8S_ENTITY_TRACES_EXPRESSION_KEY}=${encodedExpression}`;
if (pagination) {
const paginationStr = encodeURIComponent(JSON.stringify(pagination));
searchParams += `&pagination=${paginationStr}`;
}
return render(
<NuqsTestingAdapter searchParams={searchParams}>
<EntityTraces
timeRange={timeRange}
isModalTimeSelection={false}
handleTimeChange={jest.fn()}
selectedInterval={selectedInterval}
queryKey="test"
category={category}
initialExpression={expression}
/>
</NuqsTestingAdapter>,
);
}
export function verifyQueryPayload({
payload,
expectedOffset,
expectedLimit,
expectedTimeRange,
}: {
payload: QueryRangePayloadV5;
expectedOffset?: number;
expectedLimit?: number;
expectedTimeRange?: { start: number; end: number };
}): void {
const spec = payload.compositeQuery.queries[0]?.spec as {
offset?: number;
limit?: number;
order?: Array<{ key: { name: string }; direction: string }>;
};
if (expectedOffset !== undefined) {
expect(spec.offset).toBe(expectedOffset);
}
if (expectedLimit !== undefined) {
expect(spec.limit).toBe(expectedLimit);
}
if (expectedTimeRange) {
expect(payload.start).toBe(expectedTimeRange.start);
expect(payload.end).toBe(expectedTimeRange.end);
}
const orderKeys = spec.order?.map((o) => o.key.name) ?? [];
expect(orderKeys).toContain('timestamp');
}
jest.mock('container/TopNav/DateTimeSelectionV2/index.tsx', () => ({
__esModule: true,
default: ({
onTimeChange,
}: {
onTimeChange?: (interval: string, dateTimeRange?: [number, number]) => void;
}): JSX.Element => (
<button
type="button"
data-testid="mock-datetime-selection"
onClick={(): void => {
onTimeChange?.('5m');
}}
>
Select Time
</button>
),
}));

View File

@@ -0,0 +1,7 @@
.pointer {
cursor: pointer;
}
.cellText {
color: var(--l2-foreground);
}

View File

@@ -1,6 +1,7 @@
import { TableColumnsType as ColumnsType } from 'antd';
import { Badge } from '@signozhq/ui/badge';
import { Typography } from '@signozhq/ui/typography';
import HttpStatusBadge from 'components/HttpStatusBadge/HttpStatusBadge';
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
import {
BlockLink,
@@ -9,6 +10,7 @@ import {
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { FormatTimezoneAdjustedTimestamp } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
import styles from './traceListColumns.module.scss';
const keyToLabelMap: Record<string, string> = {
timestamp: 'Timestamp',
@@ -48,8 +50,9 @@ const getValueForKey = (data: Record<string, any>, key: string): any => {
const aliases = keyAliases[primaryKey];
if (aliases) {
for (const alias of aliases) {
if (data[alias] !== undefined) {
return data[alias];
const aliasedValue = data[alias] || data.resource?.[alias];
if (aliasedValue !== undefined) {
return aliasedValue;
}
}
}
@@ -78,7 +81,7 @@ export const getTraceListColumns = (
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography.Text>{date}</Typography.Text>
<Typography.Text className={styles.cellText}>{date}</Typography.Text>
</BlockLink>
);
}
@@ -86,34 +89,81 @@ export const getTraceListColumns = (
if (value === '') {
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography data-testid={key}>N/A</Typography>
<Typography data-testid={key} className={styles.cellText}>
N/A
</Typography>
</BlockLink>
);
}
if (primaryKey === 'httpMethod' || primaryKey === 'responseStatusCode') {
if (primaryKey === 'httpMethod') {
const httpMethod = getValueForKey(itemData, key);
if (!httpMethod) {
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography className={styles.cellText}>N/A</Typography>
</BlockLink>
);
}
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Badge data-testid={key} color="sakura">
{getValueForKey(itemData, key)}
<Badge
data-testid={key}
color="robin"
variant="outline"
className={styles.pointer}
>
{httpMethod}
</Badge>
</BlockLink>
);
}
if (primaryKey === 'responseStatusCode') {
const statusCode = getValueForKey(itemData, key);
const numericCode = Number(statusCode);
const isValidCode = !Number.isNaN(numericCode) && numericCode > 0;
if (!isValidCode) {
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography className={styles.cellText}>
{numericCode === 0 || !statusCode ? 'N/A' : statusCode}
</Typography>
</BlockLink>
);
}
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<HttpStatusBadge
statusCode={statusCode}
testId={key}
className={styles.pointer}
/>
</BlockLink>
);
}
if (primaryKey === 'durationNano') {
const durationNano = getValueForKey(itemData, key);
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography data-testid={key}>{getMs(durationNano)}ms</Typography>
<Typography data-testid={key} className={styles.cellText}>
{getMs(durationNano)}ms
</Typography>
</BlockLink>
);
}
return (
<BlockLink to={getTraceLink(itemData)} openInNewTab>
<Typography data-testid={key}>{getValueForKey(itemData, key)}</Typography>
<Typography data-testid={key} className={styles.cellText}>
{getValueForKey(itemData, key)}
</Typography>
</BlockLink>
);
},

View File

@@ -44,40 +44,15 @@
}
.quickFiltersContainer {
width: 280px;
min-width: 280px;
// Width is owned by ResizableBox (inline style). No overflow here: the panel
// must not scroll (QuickFilters scrolls its own list internally) — an
// overflow on the root would also clip the edge-mounted resize grip.
flex-shrink: 0;
border-right: 1px solid var(--l1-border);
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.1rem;
height: 0.1rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--accent-primary);
}
&::-webkit-scrollbar-thumb:hover {
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
:global(.ant-collapse-header) {
border-bottom: 1px solid var(--l1-border);
padding: 12px 8px;
}
:global(.ant-collapse-content-box) {
padding: 0 !important;
padding-block: 0 !important;
:global(.quick-filters .checkbox-filter) {
padding-left: 18px;
}
:global(.resizable-box__content) {
display: flex;
flex-direction: column;
}
:global(.quick-filters) {
@@ -111,22 +86,6 @@
justify-content: space-between;
}
.quickFiltersCategoryLabel {
display: flex;
align-items: center;
gap: 4px;
}
.quickFiltersCategoryLabelIcon {
margin-right: 8px;
}
.quickFiltersCategoryLabelContainer {
display: flex;
align-items: center;
gap: 4px;
}
.listContainer {
flex: 1;
min-width: 0;
@@ -158,5 +117,118 @@
}
.listContainerFiltersVisible {
max-width: calc(100% - 280px);
// Width is driven by flex now that the filters panel is resizable; the list
// fills whatever space the (variable-width) panel leaves.
max-width: 100%;
}
.categorySelectorSection {
padding: var(--spacing-4);
}
.sectionHeader {
display: flex;
align-items: center;
gap: var(--spacing-4);
margin-bottom: var(--spacing-4);
&[data-type='filter'] {
padding: 0 var(--spacing-4);
}
}
.sectionLabel {
font-size: var(--periscope-font-size-small);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--l3-foreground);
white-space: nowrap;
}
.sectionLine {
flex: 1;
height: 1px;
background: var(--l1-border);
}
.categoryCard {
background: var(--l2-background);
border: 1px solid var(--l2-border);
border-radius: var(--spacing-3);
padding: var(--spacing-2);
}
.categoryList {
display: flex;
flex-direction: column;
gap: var(--spacing-1);
}
.categoryItem {
display: flex;
align-items: center;
gap: var(--spacing-5);
padding: var(--spacing-3) var(--spacing-4);
border: none;
border-radius: var(--spacing-2);
background: transparent;
cursor: pointer;
width: 100%;
text-align: left;
transition:
background-color 0.2s ease,
color 0.2s ease;
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
--typography-color: var(--l1-foreground);
&:hover:not(.categoryItemSelected) {
background: var(--l2-background-hover);
}
svg {
color: var(--l3-foreground);
flex-shrink: 0;
transition: color 0.2s ease;
}
&.categoryItemSelected {
background: var(--accent-primary);
color: var(--l1-background);
font-weight: 600;
&:hover {
background: var(--accent-primary-hover, var(--accent-primary));
}
svg {
color: var(--l1-foreground);
}
}
}
.quickFiltersSection {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow-y: auto;
:global(.quick-filters-container) {
flex: 1;
min-height: 0;
}
&::-webkit-scrollbar {
width: 0.1rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--accent-primary);
}
}

View File

@@ -1,12 +1,12 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import * as Sentry from '@sentry/react';
import { Button, CollapseProps } from 'antd';
import { Collapse, Tooltip } from 'antd';
import { Button, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import QuickFilters from 'components/QuickFilters/QuickFilters';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { InfraMonitoringEvents } from 'constants/events';
import { LOCALSTORAGE } from 'constants/localStorage';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import {
@@ -23,6 +23,8 @@ import {
Workflow,
} from '@signozhq/icons';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { ResizableBox } from 'periscope/components/ResizableBox';
import usePanelWidth from 'periscope/components/ResizableBox/usePanelWidth';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { FeatureKeys } from '../../constants/features';
@@ -57,9 +59,23 @@ import K8sVolumesList from './Volumes/K8sVolumesList';
import styles from './InfraMonitoringK8s.module.scss';
const QUICK_FILTERS_DEFAULT_WIDTH = 280;
const QUICK_FILTERS_MIN_WIDTH = 240;
const QUICK_FILTERS_MAX_WIDTH = 500;
export default function InfraMonitoringK8s(): JSX.Element {
const [showFilters, setShowFilters] = useState(true);
const {
initialWidth: quickFiltersInitialWidth,
persistWidth: persistQuickFiltersWidth,
} = usePanelWidth({
storageKey: LOCALSTORAGE.QUICK_FILTERS_WIDTH_INFRA,
defaultWidth: QUICK_FILTERS_DEFAULT_WIDTH,
minWidth: QUICK_FILTERS_MIN_WIDTH,
maxWidth: QUICK_FILTERS_MAX_WIDTH,
});
const [selectedCategory, setSelectedCategory] = useInfraMonitoringCategory();
const [urlFilters, setUrlFilters] = useInfraMonitoringFiltersK8s();
const [, setGroupBy] = useInfraMonitoringGroupBy();
@@ -113,192 +129,74 @@ export default function InfraMonitoringK8s(): JSX.Element {
});
};
const renderCategoryLabel = (
icon: JSX.Element,
label: string,
): JSX.Element => (
<div className={styles.quickFiltersCategoryLabel}>
<div className={styles.quickFiltersCategoryLabelContainer}>
{icon}
<Typography.Text>{label}</Typography.Text>
</div>
</div>
const categories = useMemo(
() => [
{
key: K8sCategories.PODS,
label: 'Pods',
icon: <Container size={14} />,
config: GetPodsQuickFiltersConfig(dotMetricsEnabled),
},
{
key: K8sCategories.NODES,
label: 'Nodes',
icon: <Workflow size={14} />,
config: GetNodesQuickFiltersConfig(dotMetricsEnabled),
},
{
key: K8sCategories.NAMESPACES,
label: 'Namespaces',
icon: <FilePenLine size={14} />,
config: GetNamespaceQuickFiltersConfig(dotMetricsEnabled),
},
{
key: K8sCategories.CLUSTERS,
label: 'Clusters',
icon: <Boxes size={14} />,
config: GetClustersQuickFiltersConfig(dotMetricsEnabled),
},
{
key: K8sCategories.DEPLOYMENTS,
label: 'Deployments',
icon: <Computer size={14} />,
config: GetDeploymentsQuickFiltersConfig(dotMetricsEnabled),
},
{
key: K8sCategories.JOBS,
label: 'Jobs',
icon: <Bolt size={14} />,
config: GetJobsQuickFiltersConfig(dotMetricsEnabled),
},
{
key: K8sCategories.DAEMONSETS,
label: 'DaemonSets',
icon: <Group size={14} />,
config: GetDaemonsetsQuickFiltersConfig(dotMetricsEnabled),
},
{
key: K8sCategories.STATEFULSETS,
label: 'StatefulSets',
icon: <ArrowUpDown size={14} />,
config: GetStatefulsetsQuickFiltersConfig(dotMetricsEnabled),
},
{
key: K8sCategories.VOLUMES,
label: 'Volumes',
icon: <HardDrive size={14} />,
config: GetVolumesQuickFiltersConfig(dotMetricsEnabled),
},
],
[dotMetricsEnabled],
);
const items: CollapseProps['items'] = [
{
label: renderCategoryLabel(
<Container size={14} className={styles.quickFiltersCategoryLabelIcon} />,
'Pods',
),
key: K8sCategories.PODS,
showArrow: false,
children: (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={GetPodsQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
{
label: renderCategoryLabel(
<Workflow size={14} className={styles.quickFiltersCategoryLabelIcon} />,
'Nodes',
),
key: K8sCategories.NODES,
showArrow: false,
children: (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={GetNodesQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
{
label: renderCategoryLabel(
<FilePenLine size={14} className={styles.quickFiltersCategoryLabelIcon} />,
'Namespaces',
),
key: K8sCategories.NAMESPACES,
showArrow: false,
children: (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={GetNamespaceQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
{
label: renderCategoryLabel(
<Boxes size={14} className={styles.quickFiltersCategoryLabelIcon} />,
'Clusters',
),
key: K8sCategories.CLUSTERS,
showArrow: false,
children: (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={GetClustersQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
{
label: renderCategoryLabel(
<Computer size={14} className={styles.quickFiltersCategoryLabelIcon} />,
'Deployments',
),
key: K8sCategories.DEPLOYMENTS,
showArrow: false,
children: (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={GetDeploymentsQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
{
label: renderCategoryLabel(
<Bolt size={14} className={styles.quickFiltersCategoryLabelIcon} />,
'Jobs',
),
key: K8sCategories.JOBS,
showArrow: false,
children: (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={GetJobsQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
{
label: renderCategoryLabel(
<Group size={14} className={styles.quickFiltersCategoryLabelIcon} />,
'DaemonSets',
),
key: K8sCategories.DAEMONSETS,
showArrow: false,
children: (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={GetDaemonsetsQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
{
label: renderCategoryLabel(
<ArrowUpDown size={14} className={styles.quickFiltersCategoryLabelIcon} />,
'StatefulSets',
),
key: K8sCategories.STATEFULSETS,
showArrow: false,
children: (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={GetStatefulsetsQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
{
label: renderCategoryLabel(
<HardDrive size={14} className={styles.quickFiltersCategoryLabelIcon} />,
'Volumes',
),
key: K8sCategories.VOLUMES,
showArrow: false,
children: (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={GetVolumesQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
// TODO: Enable once we have implemented containers.
// {
// label: (
// <div className="k8s-quick-filters-category-label">
// <div className="k8s-quick-filters-category-label-container">
// <PackageOpen
// size={14}
// className="k8s-quick-filters-category-label-icon"
// />
// <Typography.Text>Containers</Typography.Text>
// </div>
// </div>
// ),
// key: K8sCategories.CONTAINERS,
// showArrow: false,
// children: (
// <QuickFilters
// source={QuickFiltersSource.INFRA_MONITORING}
// config={ContainersQuickFiltersConfig}
// handleFilterVisibilityChange={handleFilterVisibilityChange}
// onFilterChange={handleFilterChange}
// />
// ),
// },
];
const selectedCategoryConfig = useMemo(
() => categories.find((cat) => cat.key === selectedCategory)?.config,
[categories, selectedCategory],
);
const handleCategoryChange = (key: string | string[]): void => {
if (Array.isArray(key) && key.length > 0) {
setSelectedCategory(key[0] as string);
const handleCategorySelect = (key: string): void => {
if (key !== selectedCategory) {
setSelectedCategory(key);
// Reset filters
setUrlFilters(null);
setOrderBy(null);
@@ -331,28 +229,71 @@ export default function InfraMonitoringK8s(): JSX.Element {
<div className={styles.infraMonitoringContainer}>
<div className={styles.infraContentRow}>
{showFilters && (
<div className={styles.quickFiltersContainer}>
<div className={styles.quickFiltersContainerHeader}>
<Typography.Text>Filters</Typography.Text>
<Tooltip title="Collapse Filters">
<ArrowUpToLine
style={{ transform: 'rotate(270deg)' }}
onClick={handleFilterVisibilityChange}
size="md"
/>
</Tooltip>
<ResizableBox
handle="right"
defaultWidth={QUICK_FILTERS_DEFAULT_WIDTH}
initialWidth={quickFiltersInitialWidth}
minWidth={QUICK_FILTERS_MIN_WIDTH}
maxWidth={QUICK_FILTERS_MAX_WIDTH}
onResize={persistQuickFiltersWidth}
resetToDefaultOnDoubleClick
withHandle
className={styles.quickFiltersContainer}
handleTestId="quick-filters-resize-handle"
>
<div className={styles.categorySelectorSection}>
<div className={styles.sectionHeader} data-type="resource">
<Typography.Text className={styles.sectionLabel}>
Viewing · Resource
</Typography.Text>
<div className={styles.sectionLine} />
<Tooltip title="Collapse Filters">
<ArrowUpToLine
style={{ transform: 'rotate(270deg)' }}
onClick={handleFilterVisibilityChange}
size="md"
/>
</Tooltip>
</div>
<div className={styles.categoryCard}>
<div className={styles.categoryList}>
{categories.map((category) => (
<button
key={category.key}
type="button"
className={`${styles.categoryItem} ${
selectedCategory === category.key
? styles.categoryItemSelected
: ''
}`}
onClick={(): void => handleCategorySelect(category.key)}
data-testid={`category-${category.key}`}
>
{category.icon}
<Typography.Text>{category.label}</Typography.Text>
</button>
))}
</div>
</div>
</div>
<Collapse
onChange={handleCategoryChange}
items={items}
defaultActiveKey={[selectedCategory]}
activeKey={[selectedCategory]}
accordion
bordered={false}
ghost
/>
</div>
<div className={styles.quickFiltersSection}>
<div className={styles.sectionHeader} data-type="filter">
<Typography.Text className={styles.sectionLabel}>
Filter by
</Typography.Text>
<div className={styles.sectionLine} />
</div>
{selectedCategoryConfig && (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={selectedCategoryConfig}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
)}
</div>
</ResizableBox>
)}
<div

View File

@@ -34,8 +34,6 @@ export const VIEW_TYPES = {
METRICS: VIEWS.METRICS,
LOGS: VIEWS.LOGS,
TRACES: VIEWS.TRACES,
CONTAINERS: VIEWS.CONTAINERS,
PROCESSES: VIEWS.PROCESSES,
EVENTS: VIEWS.EVENTS,
};
@@ -394,41 +392,6 @@ export function GetClustersQuickFiltersConfig(
];
}
export function GetContainersQuickFiltersConfig(
dotMetricsEnabled: boolean,
): IQuickFiltersConfig[] {
const containerKey = dotMetricsEnabled
? 'k8s.container.name'
: 'k8s_container_name';
const environmentKey = dotMetricsEnabled
? 'deployment.environment'
: 'deployment_environment';
return [
{
type: FiltersType.CHECKBOX,
title: 'Container',
attributeKey: {
key: containerKey,
dataType: DataTypes.String,
type: 'resource',
id: `${containerKey}--string--resource`,
},
defaultOpen: true,
},
{
type: FiltersType.CHECKBOX,
title: 'Environment',
attributeKey: {
key: environmentKey,
dataType: DataTypes.String,
type: 'resource',
},
defaultOpen: true,
},
];
}
export function GetVolumesQuickFiltersConfig(
dotMetricsEnabled: boolean,
): IQuickFiltersConfig[] {

View File

@@ -0,0 +1,5 @@
.entityGroupHeader {
display: flex;
align-items: center;
gap: var(--spacing-5);
}

View File

@@ -0,0 +1,21 @@
import { Group } from '@signozhq/icons';
import styles from './EntityGroupHeader.module.scss';
interface EntityGroupHeaderProps {
title: string;
icon?: React.ReactNode;
}
function EntityGroupHeader({
title,
icon,
}: EntityGroupHeaderProps): JSX.Element {
return (
<div className={styles.entityGroupHeader}>
{icon || <Group size={14} data-hide-expanded="true" />} {title}
</div>
);
}
export default EntityGroupHeader;

View File

@@ -0,0 +1,666 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useQuery } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button, Drawer, Tooltip } from 'antd';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
import { InfraMonitoringEvents } from 'constants/events';
import { QueryParams } from 'constants/query';
import {
initialQueryBuilderFormValuesMap,
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import GetMinMax from 'lib/getMinMax';
import {
BarChart,
ChevronsLeftRight,
Compass,
DraftingCompass,
ScrollText,
X,
} from '@signozhq/icons';
import { isCustomTimeRange, useGlobalTimeStore } from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
TagFilter,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import {
LogsAggregatorOperator,
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import { InfraMonitoringEntity, VIEW_TYPES } from '../constants';
import EntityEvents from '../EntityDetailsUtils/EntityEvents';
import EntityLogs from '../EntityDetailsUtils/EntityLogs';
import { K8S_ENTITY_LOGS_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityLogs/hooks';
import EntityMetrics from '../EntityDetailsUtils/EntityMetrics';
import EntityTraces from '../EntityDetailsUtils/EntityTraces';
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityTraces/hooks';
import {
useInfraMonitoringEventsFilters,
useInfraMonitoringLogFilters,
useInfraMonitoringSelectedItem,
useInfraMonitoringTracesFilters,
useInfraMonitoringView,
} from '../hooks';
import LoadingContainer from '../LoadingContainer';
import '../EntityDetailsUtils/entityDetails.styles.scss';
import { parseAsString, useQueryState } from 'nuqs';
const TimeRangeOffset = 1000000000;
export interface K8sDetailsMetadataConfig<T> {
label: string;
getValue: (entity: T) => string | number;
render?: (value: string | number, entity: T) => React.ReactNode;
}
export interface K8sDetailsFilters {
filters: TagFilter;
start: number;
end: number;
}
export interface K8sBaseDetailsProps<T> {
category: InfraMonitoringEntity;
eventCategory: string;
// Data fetching configuration
getSelectedItemFilters: (selectedItem: string) => TagFilter;
fetchEntityData: (
filters: K8sDetailsFilters,
signal?: AbortSignal,
) => Promise<{ data: T | null; error?: string | null }>;
// Entity configuration
getEntityName: (entity: T) => string;
getInitialLogTracesFilters: (entity: T) => TagFilterItem[];
getInitialEventsFilters: (entity: T) => TagFilterItem[];
metadataConfig: K8sDetailsMetadataConfig<T>[];
entityWidgetInfo: {
title: string;
yAxisUnit: string;
}[];
getEntityQueryPayload: (
entity: T,
start: number,
end: number,
dotMetricsEnabled: boolean,
) => GetQueryResultsProps[];
queryKeyPrefix: string;
/** When true, only metrics are shown and the Metrics/Logs/Traces/Events tab bar is hidden. */
hideDetailViewTabs?: boolean;
tabsConfig?: {
showMetrics?: boolean;
showLogs?: boolean;
showTraces?: boolean;
showEvents?: boolean;
};
customTabs?: Array<{
key: string;
label: string;
icon: React.ReactNode;
render: (props: {
entity: T;
timeRange: { startTime: number; endTime: number };
selectedInterval: Time;
handleTimeChange: (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
) => void;
}) => React.ReactNode;
}>;
}
export function createFilterItem(
key: string,
value: string,
dataType: DataTypes = DataTypes.String,
): TagFilterItem {
return {
id: uuidv4(),
key: {
key,
dataType,
type: 'resource',
id: `${key}--string--resource--false`,
},
op: '=',
value,
};
}
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function K8sBaseDetails<T>({
category,
eventCategory,
getSelectedItemFilters,
fetchEntityData,
getEntityName,
getInitialLogTracesFilters,
getInitialEventsFilters,
metadataConfig,
entityWidgetInfo,
getEntityQueryPayload,
queryKeyPrefix,
hideDetailViewTabs = false,
tabsConfig,
customTabs,
}: K8sBaseDetailsProps<T>): JSX.Element {
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
const lastComputedMinMax = useGlobalTimeStore((s) => s.lastComputedMinMax);
const getAutoRefreshQueryKey = useGlobalTimeStore(
(s) => s.getAutoRefreshQueryKey,
);
const isDarkMode = useIsDarkMode();
const [selectedItem, setSelectedItem] = useInfraMonitoringSelectedItem();
const entityQueryKey = useMemo(
() =>
getAutoRefreshQueryKey(
selectedTime,
`${queryKeyPrefix}EntityDetails`,
selectedItem,
),
[queryKeyPrefix, selectedItem, selectedTime, getAutoRefreshQueryKey],
);
const {
data: entityResponse,
isLoading: isEntityLoading,
isError: isEntityError,
error: entityError,
} = useQuery({
queryKey: entityQueryKey,
queryFn: ({ signal }) => {
if (!selectedItem) {
return { data: null };
}
const filters = getSelectedItemFilters(selectedItem);
const { minTime, maxTime } = getMinMaxTime();
return fetchEntityData(
{
filters,
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
},
signal,
);
},
enabled: !!selectedItem,
});
const entity = entityResponse?.data ?? null;
const hasResponseError = !!entityResponse?.error;
const logsAndTracesInitialExpression = useMemo(() => {
if (!entity) {
return '';
}
const primaryFiltersOnly = {
op: 'AND' as const,
items: getInitialLogTracesFilters(entity),
};
return convertFiltersToExpression(primaryFiltersOnly).expression;
}, [entity, getInitialLogTracesFilters]);
const eventsInitialExpression = useMemo(() => {
if (!entity) {
return '';
}
const primaryFiltersOnly = {
op: 'AND' as const,
items: getInitialEventsFilters(entity),
};
return convertFiltersToExpression(primaryFiltersOnly).expression;
}, [entity, getInitialEventsFilters]);
const handleClose = useCallback((): void => {
setSelectedItem(null);
}, [setSelectedItem]);
const entityName = entity ? getEntityName(entity) : '';
// Content state (previously in K8sBaseDetailsContent)
const tabVisibility = useMemo(
() => ({
showMetrics: true,
showLogs: true,
showTraces: true,
showEvents: true,
...tabsConfig,
}),
[tabsConfig],
);
const { startMs, endMs } = useMemo(
() => ({
startMs: Math.floor(lastComputedMinMax.minTime / NANO_SECOND_MULTIPLIER),
endMs: Math.floor(lastComputedMinMax.maxTime / NANO_SECOND_MULTIPLIER),
}),
[lastComputedMinMax],
);
const [modalTimeRange, setModalTimeRange] = useState(() => ({
startTime: startMs,
endTime: endMs,
}));
// TODO(h4ad): Remove this and use context/zustand
const lastSelectedInterval = useRef<Time | null>(null);
const [selectedInterval, setSelectedInterval] = useState<Time>(
lastSelectedInterval.current
? lastSelectedInterval.current
: isCustomTimeRange(selectedTime)
? DEFAULT_TIME_RANGE
: selectedTime,
);
const [selectedView, setSelectedView] = useInfraMonitoringView();
const effectiveView = hideDetailViewTabs ? VIEW_TYPES.METRICS : selectedView;
const [, setLogFiltersParam] = useInfraMonitoringLogFilters();
const [, setTracesFiltersParam] = useInfraMonitoringTracesFilters();
const [, setEventsFiltersParam] = useInfraMonitoringEventsFilters();
const [userLogsExpression] = useQueryState(
K8S_ENTITY_LOGS_EXPRESSION_KEY,
parseAsString,
);
const [userTracesExpression] = useQueryState(
K8S_ENTITY_TRACES_EXPRESSION_KEY,
parseAsString,
);
useEffect(() => {
if (entity) {
logEvent(InfraMonitoringEvents.PageVisited, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: eventCategory,
});
}
}, [entity, eventCategory]);
useEffect(() => {
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
if (!isCustomTimeRange(currentSelectedInterval)) {
setSelectedInterval(currentSelectedInterval);
const { minTime, maxTime } = getMinMaxTime();
setModalTimeRange({
startTime: Math.floor(minTime / TimeRangeOffset),
endTime: Math.floor(maxTime / TimeRangeOffset),
});
}
}, [getMinMaxTime, selectedTime]);
const handleTabChange = (value: string): void => {
setSelectedView(value);
setLogFiltersParam(null);
setTracesFiltersParam(null);
setEventsFiltersParam(null);
logEvent(InfraMonitoringEvents.TabChanged, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: eventCategory,
view: value,
});
};
const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
lastSelectedInterval.current = interval as Time;
setSelectedInterval(interval as Time);
if (interval === 'custom' && dateTimeRange) {
setModalTimeRange({
startTime: Math.floor(dateTimeRange[0] / 1000),
endTime: Math.floor(dateTimeRange[1] / 1000),
});
} else {
const { maxTime, minTime } = GetMinMax(interval);
setModalTimeRange({
startTime: Math.floor(minTime / TimeRangeOffset),
endTime: Math.floor(maxTime / TimeRangeOffset),
});
}
logEvent(InfraMonitoringEvents.TimeUpdated, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: eventCategory,
interval,
view: effectiveView,
});
},
[eventCategory, effectiveView],
);
const handleExplorePagesRedirect = (): void => {
const urlQuery = new URLSearchParams();
if (selectedInterval !== 'custom') {
urlQuery.set(QueryParams.relativeTime, selectedInterval);
} else {
urlQuery.delete(QueryParams.relativeTime);
urlQuery.set(QueryParams.startTime, modalTimeRange.startTime.toString());
urlQuery.set(QueryParams.endTime, modalTimeRange.endTime.toString());
}
logEvent(InfraMonitoringEvents.ExploreClicked, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.DetailedPage,
category: eventCategory,
view: selectedView,
});
if (selectedView === VIEW_TYPES.LOGS) {
const fullExpression = combineInitialAndUserExpression(
logsAndTracesInitialExpression,
userLogsExpression || '',
);
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.logs,
aggregateOperator: LogsAggregatorOperator.NOOP,
expression: fullExpression,
filter: { expression: fullExpression },
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const fullExpression = combineInitialAndUserExpression(
logsAndTracesInitialExpression,
userTracesExpression || '',
);
const compositeQuery = {
...initialQueryState,
queryType: 'builder',
builder: {
...initialQueryState.builder,
queryData: [
{
...initialQueryBuilderFormValuesMap.traces,
aggregateOperator: TracesAggregatorOperator.NOOP,
expression: fullExpression,
filter: { expression: fullExpression },
},
],
},
};
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};
return (
<Drawer
width="70%"
title={
<>
<Divider type="vertical" />
<Typography.Text className="title">
{entityName ||
((isEntityError || hasResponseError) &&
'Failed to load entity details') ||
(isEntityLoading && 'Loading...') ||
'-'}
</Typography.Text>
</>
}
placement="right"
onClose={handleClose}
open={!!selectedItem}
style={{
overscrollBehavior: 'contain',
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
}}
className="entity-detail-drawer"
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
>
{isEntityLoading && <LoadingContainer />}
{(isEntityError || hasResponseError) && (
<div className="entity-error-container">
<Typography.Text color="danger">
{entityResponse?.error ||
(entityError instanceof Error
? entityError.message
: 'Failed to load entity details')}
</Typography.Text>
</div>
)}
{entity && !isEntityLoading && !hasResponseError && (
<>
<div className="entity-detail-drawer__entity">
<div className="entity-details-grid">
<div className="labels-row">
{metadataConfig.map((config) => (
<Typography.Text
key={config.label}
color="muted"
className="entity-details-metadata-label"
>
{config.label}
</Typography.Text>
))}
</div>
<div className="values-row">
{metadataConfig.map((config) => {
const value = config.getValue(entity);
const displayValue = String(value);
return (
<Typography.Text
key={config.label}
className="entity-details-metadata-value"
>
{config.render ? (
config.render(value, entity)
) : (
<Tooltip title={displayValue}>{displayValue}</Tooltip>
)}
</Typography.Text>
);
})}
</div>
</div>
</div>
{!hideDetailViewTabs && (
<div className="views-tabs-container">
<ToggleGroupSimple
type="single"
className="views-tabs"
onChange={handleTabChange}
value={selectedView}
items={[
...(tabVisibility.showMetrics
? [
{
value: VIEW_TYPES.METRICS,
label: (
<div className="view-title">
<BarChart size={14} />
Metrics
</div>
),
},
]
: []),
...(tabVisibility.showLogs
? [
{
value: VIEW_TYPES.LOGS,
label: (
<div className="view-title">
<ScrollText size={14} />
Logs
</div>
),
},
]
: []),
...(tabVisibility.showTraces
? [
{
value: VIEW_TYPES.TRACES,
label: (
<div className="view-title">
<DraftingCompass size={14} />
Traces
</div>
),
},
]
: []),
...(tabVisibility.showEvents
? [
{
value: VIEW_TYPES.EVENTS,
label: (
<div className="view-title">
<ChevronsLeftRight size={14} />
Events
</div>
),
},
]
: []),
...(customTabs?.map((tab) => ({
value: tab.key,
label: (
<div className="view-title">
{tab.icon}
{tab.label}
</div>
),
})) ?? []),
]}
/>
{selectedView === VIEW_TYPES.LOGS && (
<Tooltip title="Go to Logs Explorer" placement="left">
<Button
icon={<Compass size={18} />}
className="compass-button"
onClick={handleExplorePagesRedirect}
/>
</Tooltip>
)}
{selectedView === VIEW_TYPES.TRACES && (
<Tooltip title="Go to Traces Explorer" placement="left">
<Button
icon={<Compass size={18} />}
className="compass-button"
onClick={handleExplorePagesRedirect}
/>
</Tooltip>
)}
</div>
)}
{effectiveView === VIEW_TYPES.METRICS && (
<EntityMetrics<T>
entity={entity}
selectedInterval={selectedInterval}
timeRange={modalTimeRange}
handleTimeChange={handleTimeChange}
isModalTimeSelection
entityWidgetInfo={entityWidgetInfo}
getEntityQueryPayload={getEntityQueryPayload}
category={category}
queryKey={`${queryKeyPrefix}Metrics`}
/>
)}
{effectiveView === VIEW_TYPES.LOGS && (
<EntityLogs
timeRange={modalTimeRange}
isModalTimeSelection
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
queryKey={`${queryKeyPrefix}Logs`}
category={category}
initialExpression={logsAndTracesInitialExpression}
/>
)}
{effectiveView === VIEW_TYPES.TRACES && (
<EntityTraces
timeRange={modalTimeRange}
isModalTimeSelection
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
queryKey={`${queryKeyPrefix}Traces`}
category={category}
initialExpression={logsAndTracesInitialExpression}
/>
)}
{effectiveView === VIEW_TYPES.EVENTS && tabVisibility.showEvents && (
<EntityEvents
timeRange={modalTimeRange}
isModalTimeSelection
handleTimeChange={handleTimeChange}
selectedInterval={selectedInterval}
category={category}
queryKey={`${queryKeyPrefix}Events`}
initialExpression={eventsInitialExpression}
/>
)}
{customTabs?.map((tab) =>
selectedView === tab.key ? (
<React.Fragment key={tab.key}>
{tab.render({
entity,
timeRange: modalTimeRange,
selectedInterval,
handleTimeChange,
})}
</React.Fragment>
) : null,
)}
</>
)}
</Drawer>
);
}

View File

@@ -0,0 +1,40 @@
.emptyStateContainer {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
min-height: 400px;
}
.k8SListTable {
padding-left: var(--spacing-2);
--tanstack-table-header-cell-bg: var(--l2-background);
--tanstack-table-header-cell-color: var(--l2-foreground);
--tanstack-table-cell-bg: var(--l2-background);
--tanstack-table-cell-color: var(--l2-foreground);
--tanstack-table-row-hover-bg: var(--l2-background-hover);
--tanstack-table-row-active-bg: var(--l2-background-active);
--tanstack-table-resize-handle-bg: var(--l2-background);
--tanstack-table-resize-handle-hover-bg: var(--l2-border);
--tanstack-table-row-height: 42px;
--tanstack-cell-padding-top-override: 5px;
--tanstack-cell-padding-bottom-override: 5px;
--tanstack-cell-padding-left-override: 5px;
--tanstack-cell-padding-right-override: 5px;
--tanstack-expansion-first-col-padding-left: 30px;
&[data-has-group-by='false'] {
--tanstack-cell-padding-left-first-column: 28px;
}
}
.paginationContainer {
padding-bottom: var(--spacing-8);
padding-right: var(--spacing-8);
ul {
margin: 0;
}
}

View File

@@ -0,0 +1,297 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useQuery } from 'react-query';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import TanStackTable, {
TableColumnDef,
useHiddenColumnIds,
} from 'components/TanStackTableView';
import { InfraMonitoringEvents } from 'constants/events';
import { parseAsString, useQueryState } from 'nuqs';
import { useGlobalTimeStore } from 'store/globalTime';
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
import { openInNewTab } from 'utils/navigation';
import {
INFRA_MONITORING_K8S_PARAMS_KEYS,
InfraMonitoringEntity,
} from '../constants';
import {
useInfraMonitoringFiltersK8s,
useInfraMonitoringGroupBy,
useInfraMonitoringOrderBy,
useInfraMonitoringPageListing,
useInfraMonitoringPageSizeListing,
} from '../hooks';
import { K8sEmptyState } from './K8sEmptyState';
import { K8sExpandedRow } from './K8sExpandedRow';
import K8sHeader from './K8sHeader';
import { K8sBaseFilters } from './types';
import { getGroupedByMeta } from './utils';
import styles from './K8sBaseList.module.scss';
import cx from 'classnames';
export type K8sBaseListEmptyStateContext = {
isError: boolean;
error?: string | null;
totalCount: number;
hasFilters: boolean;
isLoading: boolean;
rawData?: unknown;
};
/** Base type constraint for K8s entity data */
export type K8sEntityData = { meta?: Record<string, string> };
export type K8sBaseListProps<T extends K8sEntityData> = {
controlListPrefix?: React.ReactNode;
entity: InfraMonitoringEntity;
tableColumns: TableColumnDef<T>[];
fetchListData: (
filters: K8sBaseFilters,
signal?: AbortSignal,
) => Promise<{
data: T[];
total: number;
error?: string | null;
rawData?: unknown;
}>;
/** Function to get the unique key for a row. */
getRowKey?: (record: T) => string;
/** Function to get the item key used for selection. Defaults to getRowKey if not provided. */
getItemKey?: (record: T) => string;
eventCategory: InfraMonitoringEvents;
renderEmptyState?: (
context: K8sBaseListEmptyStateContext,
) => React.ReactNode | null;
};
export function K8sBaseList<T extends K8sEntityData>({
controlListPrefix,
entity,
tableColumns,
fetchListData,
getRowKey,
getItemKey,
eventCategory,
renderEmptyState,
}: K8sBaseListProps<T>): JSX.Element {
const [queryFilters] = useInfraMonitoringFiltersK8s();
const [currentPage] = useInfraMonitoringPageListing();
const [currentPageSize] = useInfraMonitoringPageSizeListing();
const [groupBy] = useInfraMonitoringGroupBy();
const [orderBy] = useInfraMonitoringOrderBy();
const [selectedItem, setSelectedItem] = useQueryState(
'selectedItem',
parseAsString,
);
const columnStorageKey = `k8s-${entity}-columns`;
const hiddenColumnIds = useHiddenColumnIds(columnStorageKey);
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
const isRefreshEnabled = useGlobalTimeStore((s) => s.isRefreshEnabled);
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
const getAutoRefreshQueryKey = useGlobalTimeStore(
(s) => s.getAutoRefreshQueryKey,
);
const queryKey = useMemo(() => {
return getAutoRefreshQueryKey(
selectedTime,
'k8sBaseList',
entity,
String(currentPageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
);
}, [
getAutoRefreshQueryKey,
selectedTime,
entity,
currentPageSize,
currentPage,
queryFilters,
orderBy,
groupBy,
]);
const { data, isLoading, isError } = useQuery({
queryKey,
queryFn: ({ signal }) => {
const { minTime, maxTime } = getMinMaxTime();
return fetchListData(
{
limit: currentPageSize,
offset: (currentPage - 1) * currentPageSize,
filters: queryFilters || { items: [], op: 'AND' },
start: Math.floor(minTime / NANO_SECOND_MULTIPLIER),
end: Math.floor(maxTime / NANO_SECOND_MULTIPLIER),
orderBy: orderBy || undefined,
groupBy: groupBy?.length > 0 ? groupBy : undefined,
},
signal,
);
},
refetchInterval: isRefreshEnabled ? refreshInterval : false,
});
const pageData = data?.data ?? [];
const totalCount = data?.total || 0;
const hasFilters = (queryFilters?.items?.length ?? 0) > 0;
const getGroupKeyFn = useCallback(
(item: T) => getGroupedByMeta(item, groupBy),
[groupBy],
);
useEffect(() => {
logEvent(InfraMonitoringEvents.PageVisited, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: eventCategory,
total: totalCount,
});
}, [eventCategory, totalCount]);
const handleRowClick = useCallback(
(_record: T, itemKey: string): void => {
if (groupBy.length === 0) {
setSelectedItem(itemKey);
}
logEvent(InfraMonitoringEvents.ItemClicked, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: eventCategory,
});
},
[eventCategory, groupBy.length, setSelectedItem],
);
const handleRowClickNewTab = useCallback(
(_record: T, itemKey: string): void => {
if (groupBy.length > 0) {
return;
}
// Build URL with selectedItem param
const url = new URL(window.location.href);
url.searchParams.set('selectedItem', itemKey);
openInNewTab(url.pathname + url.search);
logEvent(InfraMonitoringEvents.ItemClicked, {
entity: InfraMonitoringEvents.K8sEntity,
page: InfraMonitoringEvents.ListPage,
category: eventCategory,
});
},
[eventCategory, groupBy.length],
);
const isGroupedByAttribute = groupBy.length > 0;
// Filter columns for expanded row based on parent's hidden columns
const expandedRowColumns = useMemo(
() => tableColumns.filter((col) => !hiddenColumnIds.includes(col.id)),
[tableColumns, hiddenColumnIds],
);
const renderExpandedRow = useCallback(
(
_record: T,
rowKey: string,
groupMeta?: Record<string, string>,
): JSX.Element => (
<K8sExpandedRow<T>
rowKey={rowKey}
groupMeta={groupMeta}
entity={entity}
tableColumns={expandedRowColumns}
fetchListData={fetchListData}
getRowKey={getRowKey}
getItemKey={getItemKey}
/>
),
[entity, fetchListData, getRowKey, getItemKey, expandedRowColumns],
);
const getRowCanExpand = useCallback(
(): boolean => isGroupedByAttribute,
[isGroupedByAttribute],
);
const showTableLoadingState = isLoading;
const emptyTableMessage: React.ReactNode = renderEmptyState?.({
isError,
error: data?.error,
totalCount,
hasFilters,
isLoading: showTableLoadingState,
rawData: data?.rawData,
}) || (
<K8sEmptyState
isError={isError}
error={data?.error}
isLoading={showTableLoadingState}
rawData={data?.rawData}
/>
);
const showEmptyState = !showTableLoadingState && pageData.length === 0;
return (
<>
<K8sHeader
controlListPrefix={controlListPrefix}
entity={entity}
showAutoRefresh={!selectedItem}
columns={tableColumns}
columnStorageKey={columnStorageKey}
/>
{isError && (
<Typography>{data?.error?.toString() || 'Something went wrong'}</Typography>
)}
{showEmptyState ? (
<div className={styles.emptyStateContainer}>{emptyTableMessage}</div>
) : (
<TanStackTable<T>
data={pageData}
columns={tableColumns}
columnStorageKey={columnStorageKey}
isLoading={showTableLoadingState}
getRowKey={getRowKey}
getItemKey={getItemKey}
groupBy={groupBy}
getGroupKey={getGroupKeyFn}
onRowClick={handleRowClick}
onRowClickNewTab={handleRowClickNewTab}
renderExpandedRow={isGroupedByAttribute ? renderExpandedRow : undefined}
getRowCanExpand={isGroupedByAttribute ? getRowCanExpand : undefined}
className={cx(styles.k8SListTable, expandedRowColumns)}
enableQueryParams={{
page: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE,
limit: INFRA_MONITORING_K8S_PARAMS_KEYS.PAGE_SIZE,
orderBy: INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY,
expanded: INFRA_MONITORING_K8S_PARAMS_KEYS.EXPANDED,
}}
pagination={{
total: totalCount,
defaultLimit: 10,
defaultPage: 1,
showTotalCount: true,
totalCountLabel: entity.charAt(0).toUpperCase() + entity.slice(1),
}}
paginationClassname={styles.paginationContainer}
/>
)}
</>
);
}

View File

@@ -0,0 +1,56 @@
.container {
height: 30vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--spacing-3);
width: fit-content;
max-width: 500px;
padding: 24px;
text-align: center;
}
.title {
font-weight: 500;
margin: 0;
font-size: var(--periscope-font-size-medium);
}
.message {
max-width: 400px;
}
.noDataMessage {
display: flex;
flex-direction: column;
gap: var(--spacing-1);
}
.emptyStateSvg {
width: 32px;
max-width: 100%;
}
.eyesEmoji {
height: 32px;
width: 32px;
}
.errorIcon {
color: var(--danger-background);
}
.actions {
display: flex;
align-items: center;
gap: 8px;
margin-top: 16px;
}

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