Compare commits

...

240 Commits

Author SHA1 Message Date
Naman Verma
3e740cfa4e Merge branch 'nv/dashboard-migration' into nv/sql-dashboard-migration 2026-07-25 11:47:44 +05:30
Naman Verma
218bdd281e fix: add image validation and base64 to asset conversion 2026-07-25 11:46:16 +05:30
Naman Verma
b968201277 refactor(dashboard): drop ConvertAllV1ToV2 module scaffolding from base
Remove the temporary v1→v2 bulk-conversion module method (ConvertAllV1ToV2 +
migrateOneV1ToV2), its result types (V1ToV2MigrationResult / V1ToV2MigrationItem),
the Module interface entry, and the EE delegation. This scaffolding lives on the
migration feature branches, not the base branch that merges to main.

V1Name and store.UpdateName are kept (small helpers; UpdateName is on the shared
Store interface).
2026-07-25 10:48:43 +05:30
Naman Verma
6be31ea620 refactor(dashboard): re-host ConvertAllV1ToV2 off the Module interface
Mirror base's removal of ConvertAllV1ToV2 from the shared files (Module
interface, v2_module.go, EE delegation, result types) so future base→branch
merges stay conflict-free, and re-host the capability in dedicated branch-only
files (V1ToV2Migrator interface + module method + result types). Migration 103
now obtains it via a dashboard.V1ToV2Migrator type assertion.
2026-07-25 10:48:34 +05:30
Naman Verma
470139dde5 Merge branch 'nv/dashboard-migration' into nv/sql-dashboard-migration 2026-07-25 10:24:45 +05:30
Naman Verma
33fcc6147e fix: use slog for logging 2026-07-25 10:15:48 +05:30
Naman Verma
26bd1210fc Merge branch 'main' into nv/dashboard-migration 2026-07-25 10:14:12 +05:30
Nikhil Mantri
d22ee8f7aa fix(telemetrymetadata): update fallback to false for positive operators in related values queries (#12250)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
* chore: update fallback to false for positive operators

* chore: changed to distributed table

* chore: flip AND to or in outside condition

* chore: added data_source condition

* chore: added integration tests

* chore: simplified comment

* chore: more simplified comment

* chore: test case both columns contain host.name -> resources take precedence
2026-07-25 04:08:25 +00:00
Ashwin Bhatkal
5b004d1efc refactor(dashboard): store system icons as SVG assets with a safe image resolver (#12259)
* feat(dashboard-v2): add file-based dashboard icons and logos with a safe image resolver

* refactor(dashboard-v2): render dashboard icons via the safe resolver

* fix(dashboard-v2): clamp dashboard icon size to 16px

* fix(dashboard-v2): reject invalid dashboard image before saving

* fix(dashboard-v2): validate dashboard image server-side

* test(dashboard-v2): stub the Vite-only icon glob for jest and align image fixtures

* test(dashboard-v2): update patch test for image validation

* refactor(dashboard-v2): hoist dashboard image consts/regex and tidy validation tests

* refactor(dashboard-v2): drop the validateImage doc comment
2026-07-25 03:19:19 +00:00
Naman Verma
7828a5ca3b Merge branch 'nv/dashboard-migration' into nv/sql-dashboard-migration 2026-07-24 21:58:31 +05:30
Naman Verma
8e3ca59b88 Merge branch 'main' into nv/dashboard-migration 2026-07-24 21:42:15 +05:30
Naman Verma
ecb77af4cb chore: fix schema and validations based on migration errors (#12263)
Some checks failed
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / staging (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
* chore: fix schema based on migration errors

* test: add rejection based integration tests for new validations

* fix: remove datasource field from schema
2026-07-24 15:37:04 +00:00
Tushar Vats
973429a22f fix(logs): stop hasToken and hasAny/hasAll 500s on separator and big-int needles (#12261)
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
hasToken(body, <needle>) returned a 500 whenever the needle contained a
token separator or whitespace (ClickHouse code 36). Validate the needle
up front in conditionForHasToken (the single choke point for both legacy
and JSON modes) and return a clean 400 pointing at CONTAINS instead.

hasAny/hasAll on a legacy body array returned a 500 for a quoted integer
needle >= 2^32: clickhouse-go binds it as a concrete Array(UInt64), which
has no supertype with the Array(Nullable(Int64)) extraction that the
function must unify (code 386). CAST the needle array to Array(Int64) to
match the extraction. Scalar has() and the scalar OR-fallback are
value-level and unaffected.
2026-07-24 12:56:56 +00:00
Naman Verma
f4492b1a40 Merge branch 'main' into nv/dashboard-migration 2026-07-24 16:42:28 +05:30
Naman Verma
5b12238759 chore: add orgid name unique index post dashboard migration 2026-07-24 16:41:38 +05:30
Swapnil Nakade
9f5ad1ba26 fix: list gcp accounts was missing config (#12175)
* fix: list gcp accounts was missing config

* refactor: updating cloudintegration integration tests

* refactor: moving variables to test file

* chore: removing verbose comments
2026-07-24 10:56:33 +00:00
Naman Verma
b2574e7797 fix: remove requirement for links to be always present (#12260)
* fix: remove requirement for links to be always present

* fix: make links use defined type

* test: add integration test for link omission roundtrip

* test: change not in to None for links in panel

---------

Co-authored-by: Ashwin Bhatkal <ashwin96@gmail.com>
2026-07-24 10:54:47 +00:00
Naman Verma
f0bcc0e331 Merge branch 'nv/dashboard-migration' into nv/sql-dashboard-migration 2026-07-24 16:05:12 +05:30
Naman Verma
bd1befb5e5 fix: migrate name column even if data column migration fails 2026-07-24 16:04:57 +05:30
Naman Verma
4600098bcb Merge branch 'nv/dashboard-migration' into nv/sql-dashboard-migration 2026-07-24 13:21:31 +05:30
Naman Verma
48daa9d7ce fix: force allowAll to be false if allowMultiple is false 2026-07-24 13:10:14 +05:30
Naman Verma
b63ec74b88 fix: keep thresholds that do not have an optional label 2026-07-24 12:49:05 +05:30
Naman Verma
0766ed6fcd fix: restore formula fields that wrapinv5envelope removes 2026-07-24 12:33:13 +05:30
Naman Verma
e3a0d8faa3 fix: run order by handling after default aggregation handling 2026-07-24 11:58:04 +05:30
Naman Verma
10f06ab0a7 test: add unit test for count with attributes 2026-07-24 11:29:39 +05:30
Naman Verma
423d27ca7f Merge branch 'main' into nv/dashboard-migration 2026-07-24 11:19:17 +05:30
Ashwin Bhatkal
cbde4dca41 fix(dashboard-v2): templates header, long-title handling, variable icon & unified delete modal (#12256)
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
* feat(dashboard-v2): add "Request a new template" header to templates tab

* fix(dashboard-v2): keep toolbar tags intact when the dashboard title is long

* fix(dashboard-v2): use a subtle outline info icon in the variables bar

* fix(dashboard-v2): truncate long dashboard title in the breadcrumb

* refactor(dashboard-v2): share one delete-confirm modal for views and dashboards

* fix(dashboard-v2): reduce panel area horizontal padding

* fix(dashboard-v2): show loader while deleting a saved view

Deleting a saved view was fire-and-forget: the confirm modal closed instantly
and the row vanished later on refetch, with no loading feedback. Make the
delete awaitable end-to-end (deleteView -> removeView -> handleRemoveView ->
ViewsRail onConfirm) so the shared confirm modal keeps its Delete button
loading until the backend call settles, then closes — matching dashboard
delete. The active view resets only on success.

* fix(dashboard-v2): fire delete success toast after the list refetches

Move the dashboard delete success toast to after the list is invalidated and
refetched, so it appears once the row is gone, and use a plain success message.

* fix(dashboard-v2): centre the delete-confirm alert icon to match v1

The shared delete-confirm modal top-aligned the CircleAlert (align-items:
flex-start + a margin-top nudge) at size 20; centre it against the body
(align-items: center) at size 3xl, matching the v1 dashboard delete button.
2026-07-23 21:36:22 +00:00
Ashwin Bhatkal
aabe59103d fix(dashboard-v2): variable selection (default reset + multiselect commit-on-close) (#12254)
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
* feat(NewSelect): make multi-select Only/All + Toggle real clickable pills

* fix(dashboard-v2): reset variable default only on real option-set changes

* fix(dashboard-v2): multiselect value selection — commit on close, skip unchanged cascade, allow clear

* fix(dashboard-v2): fix multiselect tag layout in the variable pill

* fix: format issue

* fix(dashboard-v2): clear invalid variable default and fix useVariableForm test OOM

Stabilize the useVariableForm test props so the reset-on-`initial` effect no
longer loops into a heap OOM (the hook relies on a stable `initial` reference,
which memoized callers already provide). With the suite now running, align the
QUERY/CUSTOM default-reset to clear an invalidated default to empty rather than
pinning the first option, matching the DYNAMIC path and letting resolution fall
back to the first option / ALL.

* refactor(NewSelect): extract option-row button handler out of JSX

Pull the repeated stopPropagation/preventDefault/select/reset logic on the
option row's checkbox, Only/All and Toggle buttons into a single
`selectFromButton(e, source)` helper so the JSX stays readable.
2026-07-23 20:08:56 +00:00
Ashwin Bhatkal
6c957d1e8b feat(dashboard-v2): ANTLR-composed apply-to-panels with an impact review dialog (#12181)
* fix(dashboard-v2): compose panel filter via ANTLR so apply/remove respect precedence

* feat(dashboard-v2): review apply-to-panels query changes in the impact dialog

* feat(dashboard-v2): surface non-builder panel queries in the apply-to-panels review

findApplyUsages now emits an editable row for every selected panel query that
does not yet reference the variable — PromQL/ClickHouse rows default to their
current text (never auto-injected) so the review dialog opens for them instead
of silently no-op'ing. Adds isVariableAppliedToAllPanels, a cheap boolean scan
(no ANTLR) for the 'apply to all' disabled state.

* feat(dashboard-v2): route apply-to-all through the review dialog

The list-row 'Apply to all' now opens the same VariableImpactDialog review (all
panels as target) instead of a plain confirm, and is disabled with an 'Already
applied to all panels' tooltip once every panel query references the variable.
Removes the now-redundant ApplyToAllDialog and the dead build*VariableToPanelsPatch
helpers.
2026-07-23 19:42:49 +00:00
Ashwin Bhatkal
de1e21a050 fix(dashboard-v2): variable, panel-layout, and dashboards-list bug-bash fixes (#12178)
* feat(dashboard-v2): compact panel layout before persisting drag and JSON edits

* feat(dashboard-v2): resolve and persist user-typed variable selection across refetch

* feat(dashboard-v2): expose image field and close-on-Escape in the JSON editor

* fix(dashboard-v2): keep a custom dashboard image selectable in the overview picker

* fix(dashboard-v2): variable form & bar fixes

* fix(dashboard-v2): unify section title modal, keep open on error

* refactor(dashboard-v2): dashboard-list tags (popover to tooltip, TagBadge, DashboardRowTags)

* fix(dashboard-v2): sync locked state to the detail cache after list-page toggle

* fix(dashboard-v2): stop the variable value control from showing scrollbars

* fix(dashboard-v2): hide the not-recommended pill below 1280px, not 1024px
2026-07-23 18:21:12 +00:00
Naman Verma
93e6b49438 Merge branch 'nv/dashboard-migration' into nv/sql-dashboard-migration 2026-07-23 23:42:39 +05:30
Naman Verma
b99a646ed1 fix: change EXISTS to exists so that migrateCommon handles it properly 2026-07-23 23:18:41 +05:30
Nikhil Soni
85abfd626e fix(tracedetail): correct EndTimestampMillis in flamegraph API response (#12251)
* fix(tracedetail): use span-derived bounds for flamegraph EndTimestampMillis

The trace_summary MV stores max(timestamp) as 'end', which is the maximum
span *start* time. For the final span of a trace, this misses the span's
duration entirely, making EndTimestampMillis shorter than the actual trace end.

FlamegraphTrace already computed the correct end via updateTimeRange
(end = max(span_start + duration)) but only used it internally for bucket
sampling. Add TimeRange() to expose these span-derived bounds and use them
in both getFullFlamegraph and getWindowedFlamegraph instead of summary.End.

The waterfall was unaffected — NewWaterfallTraceFromSpans already computes
endTime = max(span.TimeUnix + span.DurationNano) from actual spans.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GxTqz7CqqmoFEPHFqhJUCa

* chore: remove unnecessary comments

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-23 17:21:29 +00:00
Naman Verma
5566723c9e Merge branch 'nv/dashboard-migration' into nv/sql-dashboard-migration 2026-07-23 22:36:06 +05:30
Naman Verma
e5d7ba2649 test: add unit test to show v5 takes precedence over v4 2026-07-23 22:35:08 +05:30
Naman Verma
bbfbcbb73d Merge branch 'nv/dashboard-migration' into nv/sql-dashboard-migration 2026-07-23 18:58:09 +05:30
Naman Verma
df18ba2a00 fix: add empty links array in migration 2026-07-23 18:47:44 +05:30
Naman Verma
3875a104cb Merge branch 'main' into nv/dashboard-migration 2026-07-23 18:44:46 +05:30
Aditya Singh
66c7e2d157 feat(trace-details): download full trace from trace details page (#12115)
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: add blob-stitching and part-fetch primitives for chunked downloads

* feat: add bounded worker-pool runner for chunked downloads

* feat: add trace download store, query and progress panel

* feat: add download trace option to trace options menu

* feat: update tests
2026-07-23 12:39:27 +00:00
Vinicius Lourenço
3c5c2cdcd9 refactor(infrastructure-monitoring-v2): remove map between dot and underscore attributes (#12214)
* refactor(infrastructure-monitoring-v2): remove map between dot and underscore attributes

* test(entity-metrics): fix test
2026-07-23 11:34:38 +00:00
Gaurav Tewari
cd1861ff87 chore: bump package versions (#12213)
* chore: bump axios version

* chore: bump more packages

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-07-23 11:03:19 +00:00
Nikhil Mantri
f0a0b95ecd chore: removed code and tests (#12243)
Co-authored-by: Pandey <vibhupandey28@gmail.com>
2026-07-23 10:48:44 +00:00
Ashwin Bhatkal
38340b5027 fix(dashboards-v2): dedicated "all" dynamic-variable signal + required non-nullable links (#12201)
* fix(dashboards-v2): make panel/dashboard links required and non-nullable

* fix: validate links on read from db as well

* fix: allow all as value for signal

* fix: dont allow empty string for signal

* fix(dashboards-v2): dedicated `all` dynamic-variable signal (frontend + client)

* test: add empty links to new payloads in integration tests

---------

Co-authored-by: Naman Verma <naman.verma@signoz.io>
2026-07-23 10:34:18 +00:00
Naman Verma
7520068c33 Merge branch 'nv/dashboard-migration' into nv/sql-dashboard-migration 2026-07-23 15:05:59 +05:30
Naman Verma
2a8eee15dc test: fix unit test for empty layouts 2026-07-23 15:05:39 +05:30
Naman Verma
a476309c9f test: fix unit test for empty layouts 2026-07-23 15:05:00 +05:30
Naman Verma
c5176743d8 Merge branch 'nv/dashboard-migration' into nv/sql-dashboard-migration 2026-07-23 14:41:56 +05:30
Naman Verma
9874d188c9 fix: return empty objects instead of null if no variables/panels/layouts are there 2026-07-23 14:41:44 +05:30
Naman Verma
1400f736b8 Merge branch 'nv/dashboard-migration' into nv/sql-dashboard-migration 2026-07-23 14:31:28 +05:30
Naman Verma
6be1b7cb50 feat: handle v1 payloads in v2 create api 2026-07-23 14:30:04 +05:30
Naman Verma
ce223573d6 fix: fill in ref path when building grid layout 2026-07-23 14:29:38 +05:30
Abhi kumar
fba56e9a64 fix(dashboard): filter-quote escaping, panel export encoding, right-legend width, number-panel query warning (#12246)
* fix(qb): stop doubling escaped quotes on filter round-trip

formatSingleValue escapes ' -> \' when quoting a filter value, but its inverse
(unquote, in formatSingleValueForFilter) stripped the quotes without unescaping.
Each expression <-> filters round-trip therefore added a backslash. "Create
alert from panel" re-reads the query through the URL twice, so a filter like
name = 'it\'s a ribbon' arrived at the alert page as name = 'it\\'s a ribbon'
and failed the filter parser. Unescape \' -> ' so the round-trip is lossless.

* fix(dashboard): double-encode compositeQuery in panel export link

useGetCompositeQueryParam decodes the compositeQuery param twice, so a single
encode left a bare %/+ from a filter expression (e.g. ILIKE 'Inf%') that made
the reader's second decode throw and drop the whole query. Encode twice in
buildExportPanelLink so the query survives the round-trip.

* fix(dashboard): size right legend column to the longest label

A right-positioned legend is a single vertical column, so give it its own width
budget and size it to fit the longest series label, clamped to
[150px, min(320, 40% of container width)], instead of the shared bottom-legend
cap. Long service names no longer ellipsize in the pie/donut legend.

* feat(dashboard): warn when a number panel has more than one query enabled

A Number panel renders only the first query's value, so leaving extra
queries enabled (e.g. a formula plus its inputs) silently shows the wrong
value with no feedback. Surface a warning in the panel editor and on the
rendered panel when a Number panel has more than one enabled query.

Refs #9512
2026-07-23 08:47:25 +00:00
Naman Verma
9d7f2f9b89 Merge branch 'nv/dashboard-migration' into nv/sql-dashboard-migration 2026-07-23 12:58:04 +05:30
Naman Verma
55fba142e4 Merge branch 'main' into nv/dashboard-migration 2026-07-23 12:57:33 +05:30
Naman Verma
7ab675614f Merge branch 'main' into nv/sql-dashboard-migration 2026-07-23 12:56:00 +05:30
Naman Verma
150736b047 chore: add sql migration for dashboards 2026-07-23 12:42:16 +05:30
Naman Verma
911e70d433 feat: add scaffolding to migrate dashboards 2026-07-23 12:40:39 +05:30
Tushar Vats
cbc8b5424b feat(qb): parse search() in the filter grammar (not yet implemented) (#12129)
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
Add a search('needle') function to the filter grammar, recognized but not
yet executed - using it returns "search is not yet supported" instead of a
syntax error. Alert links, dashboard variables, and the contradiction check
pass it through untouched. `search` is now reserved, like has/hasAny.
2026-07-22 17:03:30 +00:00
Naman Verma
36ee71298f Merge branch 'main' into nv/dashboard-migration 2026-07-22 21:32:28 +05:30
Aditya Singh
7ac6df6fb0 feat(query-builder): fieldContext prefixed key suggestions (#12200)
* feat(query-builder): fieldContext-prefixed key suggestions and type-aware operator filtering

* test(query-builder): extract shared CodeMirror DOM mocks for jsdom specs

* test(query-builder): add CodeMirror+MSW behavior specs for suggestion flows
2026-07-22 13:55:01 +00:00
Swapnil Nakade
8c6a0de8c3 refactor: gcp integration memorystore redis dashboard update (#12229)
Some checks failed
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (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
2026-07-22 11:42:12 +00:00
Naman Verma
c7abe0fe7b fix: add rank column to tag relations to keep note of tag ordering in dashboards (#12170)
* fix: always sort dashboard tags in alphabetical order

* fix: add rank column to tag relations to keep note of tag ordering in dashboards

* fix: dont disable FK just for adding a new column

* fix: add back tag relation unique index
2026-07-22 11:34:25 +00:00
Abhi kumar
b236273c5f test(e2e): cover alert edit-page threshold persistence (#12227)
* test(e2e): cover alert edit-page threshold persistence

Adds an E2E test that seeds a notification channel + v2 threshold alert
(target 245) via API, opens the alert overview/edit page, and asserts the
condition editor shows the persisted threshold value once loaded.

New helper tests/e2e/helpers/alerts.ts (channel/alert seed + delete +
navigate) mirrors the existing dashboards seeding pattern; the test lives
alongside the existing tabs test in tests/e2e/tests/alerts/alerts.spec.ts.

Verified against a local --with-web stack (passes with the loaded value,
fails when the edit page resets it).

* chore: pr review fixes
2026-07-22 10:13:19 +00:00
Naman Verma
14bb4a8437 fix: remove omitempty/omitzero and explicit empty value handling in dashboard v2 spec (#12197)
* fix: remove omitempty and omitzero where not needed in dashboard v2 spec

* fix: reject explicitly empty enum values if invalid
2026-07-22 09:34:52 +00:00
Swapnil Nakade
842d44d874 feat: adding gcp memorystore redis service (#12118)
* feat: adding gcp memorystore redis service

* refactor: updating dashboard title

* refactor: extending width of uptime gauge panel

* refactor: updating cpu utilization panel

* refactor: updating dashboard panel to use rate function instead of hack
2026-07-22 07:08:26 +00:00
Abhi kumar
0d7aff9ce5 fix(alerts-v2): preserve loaded thresholds on edit-page refresh (#12226)
PR #12137 changed the URL-prefill effect dependency from the primitive
`thresholdsFromURL` string to the memoized `urlPrefill` object, which gets a
fresh reference on every `location.search` change. On the alert overview/edit
page the query builder normalizes the loaded query back into the URL after
mount, so the effect re-ran and its unconditional threshold RESET wiped the
value loaded via SET_INITIAL_STATE (245 -> 0 on refresh).

URL-declared prefill is a create-flow concern; in edit mode the alert state is
owned by SET_INITIAL_STATE. Skip the effect when isEditMode is true.
2026-07-22 06:32:55 +00:00
Naman Verma
fe101a1835 fix: fetch latest monotonicty for metrics instead of any random (#12205)
* fix: fetch latest monotonicty for metrics instead of any random

* test: add integration test for is monotonic flag
2026-07-22 06:01:23 +00:00
Nityananda Gohain
69c2f8d73f feat: span mapper test endpoint (#11795)
* feat: span mapper test endpoint

* feat: integrate with collector

* fix: more cleanup and integration test

* fix: lint

* fix: cleanup go.mod

* fix: update openapi

* fix: more cleanup

* fix: update go.mod

* fix: minor cleanup

* fix: update group_id to groupId

* fix: cleanup integration test

---------

Co-authored-by: Gaurav Tewari <gauravtewari111@gmail.com>
2026-07-22 04:52:13 +00:00
Ashwin Bhatkal
fc6f398828 fix: show the backend error message in the error modal instead of a generic axios error (#12215)
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-21 19:29:13 +00:00
Ashwin Bhatkal
8cd2077522 fix(dashboard-v2): validate key:value tags in the editor (inline-edit errors + backend-rule space/char/length checks) (#12062) 2026-07-21 19:27:24 +00:00
Vinicius Lourenço
7b23286aab refactor(infrastructure-monitoring-v2): ui/ux improvements (#12191)
* fix(pods): min width for pod restarts

* fix(disk-usage): not have minimum width correctly

* perf(infra-monitoring-v2): set cache before opening details modal

* feat(tanstack): add custom markers for perf review

* feat(tanstack): add hover tooltip + integrate on infra monitoring

* perf(grouped-status-counts): use tanstack tooltip hover

* fix(k8s-base-list): adding table def to classnames

* perf(grouped-status-counts): reduce node count on DOM

* feat(infrastructure-monitoring): use single component instead of unmount/mount all content

* chore(infrastructure-monitoring-k8s): remove old list components

* fix(hosts): make it double line header

* fix(infrastructure-monitoring): show scrollbar only on hover

* chore(pod-metrics): add docs for charts

* fix(perf-lib): address comments

* fix(infra-monitoring): rename entityConfig to entity.config
2026-07-21 18:51:20 +00:00
Ashwin Bhatkal
6bdf054a05 test(e2e): fix flaky dashboards-detail specs (sidenav overlay + slow hydration) (#12180)
* test(e2e): pin the sidenav so its flyout stops occluding dashboard content

* test(e2e): harden dashboards-detail specs against slow CI hydration
2026-07-21 17:50:44 +00:00
Ashwin Bhatkal
c7f4aa01c4 feat(dashboard-v2): product analytics instrumentation for list + detail pages (#12199)
* feat(dashboard-v2): add analytics event catalog for list + detail

Introduce DashboardListEvents and DashboardDetailEvents enums as the single
source of truth for V2 dashboard product-analytics event names. Grouped
action-discriminator events (RowAction, SectionAction, PanelAction,
DrilldownAction, JsonEditorAction, PublicDashboardAction) mirror the existing
'Dashboard Detail: Panel action' pattern. PanelAction reuses the existing V1
string so panel actions stay comparable across versions.

* feat(dashboard-v2): instrument dashboards list page analytics

Fire logEvent for create/import funnel (tab switch, created success, import
failed), find/navigate (search, filter, sort, columns, pin, pagination),
Views rail (select/save/rename/delete/reset, rail toggle), row actions
(view/open-new-tab/copy-link/rename/edit-tags/duplicate/lock/delete),
legacy-dashboard dialog, and error-state actions.

* feat(dashboard-v2): instrument dashboard detail page analytics

Fire logEvent across the detail page: lifecycle (open, rename, clone, delete,
lock, full-screen, stale-reload, public-url), sections (add/rename/clone/delete,
collapse, reorder, first-section migration), panels (add, edit/clone/delete/move,
export csv/png/svg, view, editor save/discard, type change, search, no-data),
layout drag/resize (rate-limited), variables setup (save/delete/reorder/test/
apply-to-all) and runtime selection (value selected rate-limited, multiselect
clear, options-fetch failed), drill-down (open + view-logs/traces/breakout/
filter/set-variable/create-variable), panel zoom, JSON editor actions, settings
overview save/discard, cursor sync, and public-dashboard actions.

* fix: lint

* refactor(dashboard-v2): extract inline analytics handlers into named functions

Address PR review: pull the inline JSX arrow handlers that wrap logEvent out
into named handlers across both V2 dashboard pages (public-url open, cursor/
tooltip sync, settings/JSON/full-screen actions, panel search, drilldown
view-logs/traces, create-modal tab, error retry/contact-support, row open-in-
new-tab/copy-link, legacy contact-support, save-view actions). Behavior
unchanged. Handlers defined inside .map()/per-row renderers and pre-existing
complex value-mapping handlers (ValueSelector) are intentionally left inline.
2026-07-21 14:13:26 +00:00
Vinicius Lourenço
e93792d427 fix(overlayscrollbars): leaking memory due to img.load event listener (#12179)
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-21 12:07:55 +00:00
Vinicius Lourenço
6d0f618a9e feat(infra-monitoring): persist custom time on drawer across categories (#12038)
* feat(globalTime): add method to reset time of child to parent

* refactor(infra-monitoring): extract content from K8sBaseDetails

* feat(entity-details): add support to persist custom time across categories of k8s

* test(entity-details): update existing tests

* test(entity-details): add regression test for time conversion

* fix(k8s-list): ensure default value for list is 30m

* fix(entity-counts-section): address todo around inherit selected time

* fix(pr): missing fix for tab validation, lost of moving/merge
2026-07-21 11:58:26 +00:00
Ashwin Bhatkal
564d479aae test(e2e): fix teardown crashes (network endpoint race + eager keeper lookup) (#12204)
* test(e2e): retry network teardown to survive async endpoint detach

`--teardown` fails on CI with `docker.errors.APIError: 403 ... error while
removing network ... has active endpoints`: containers are torn down before
the network, but Docker detaches endpoints asynchronously, so the network
can still report active endpoints for a moment. Retry the removal,
force-disconnecting any stragglers each round.

* test(e2e): resolve keeper coordinator lazily so --teardown doesn't crash

`create_clickhouse` / `create_clickhouse_cluster` computed
`next(iter(keeper.container_configs.values()))` eagerly. Under `--teardown`
the keeper fixture is empty, so it raised StopIteration during teardown
setup, before any finalizer ran. Move the lookup inside create().
2026-07-21 11:44:30 +00:00
Vikrant Gupta
253ca7dd7e fix(session): validate ref and callback state against global allowed_origins (#12172)
* fix(session): use global external_url instead of ref param for SSO state

The sessions/context endpoint no longer reads the client-controlled ref
query param to build the SSO state and callback URLs. Each callback
authn provider now derives the site URL from the server-configured
global external_url, and siteURL is removed from the CallbackAuthN and
session Module interfaces.

* fix(session): validate ref and callback state against global allowed_origins

Instead of deriving SSO urls from the global external_url, keep the ref
roundtrip and validate its origin against the new optional
global.allowed_origins config. The callback state url is re-validated
before tokens are attached to it, closing the forged RelayState/state
exfiltration path. When allowed_origins is not configured, redirect
targets are not validated, preserving existing installs.

* fix(session): scope ref origin validation to sso auth domains

Move the allowed_origins check from the sessions/context handler to
getOrgSessionContext, right before the SSO login URL is built. A
disallowed ref no longer fails the whole request; only orgs with an
SSO-enabled auth domain get a per-org warning with password fallback.
2026-07-21 10:28:49 +00:00
Ashwin Bhatkal
cc45c1bef1 feat(dashboard-v2): share a dashboard link with the current variable selection (#12198)
* feat(share): support a page-specific extra option in the share dialog

* feat(dashboard-v2): share a dashboard link with the current variable selection
2026-07-21 07:08:20 +00:00
Abhi kumar
e70b3a0b52 chore: pr review fixes for 12137 (#12142) 2026-07-21 06:43:10 +00:00
Abhi kumar
f514af469e fix(dashboards-v2): panel editor Save/dirty, refresh retention, dropdown clipping & time-window fixes (#12146)
* fix(dashboards-v2): carry active time window across panel editor navigation

Opening, creating, or leaving the V2 panel editor now preserves the selected time
range (relative or custom) via URL params derived from Redux global time, so a
custom range picked in the editor isn't reset to the dashboard default on return.

Adds timeParamsFromGlobalTime + useTimeSearchParams; useOpenPanelEditor now takes an
options object ({ handoffState, search }) and appends the time params, and
useCreatePanel routes through it.

* fix(dashboards-v2): sync panel editor preview to the staged query on browser back/forward

The query builder reverts both currentQuery and stagedQuery via initQueryBuilderData on a
URL re-stage (chiefly browser Back/Forward), but the preview kept the last Run's result.
Commit the staged query into the draft whenever it re-stages outside an explicit Run, so the
preview follows. Live edits touch only currentQuery, so they still wait for Run; commitQuery
no-ops when unchanged.

* fix(dashboards-v2): stop query-builder dropdowns being clipped in the panel editor

The panel editor renders the query builder inside an overflow:hidden resizable pane, so
antd Select popups (group-by, order-by, having, metric name, aggregator, units) were cut
off. Make the shared QB filters defer to an ancestor ConfigProvider's popup container
(falling back to trigger.parentNode) via useSelectPopupContainer, and wrap the editor's
builder in a ConfigProvider that portals popups to document.body. Scoped to the editor
host, so the View modal keeps its own container and other surfaces are unchanged.

* fix(dashboards-v2): anchor panel editor dirty check to saved panel; retain query edits on refresh

The editor re-derived its dirty baseline from the incoming panel prop, which
is seeded from transient state — a stale URL compositeQuery after a refresh or
the View-mode handoff spec — instead of the persisted panel. So the draft was
compared against an already-edited baseline: after a refresh the panel showed
the saved query yet read dirty (inverted), and a View->Edit query change read
clean with Save disabled.

- Thread a savedPanel baseline (existingPanel) through PanelEditorPage ->
  PanelEditorContainer -> usePanelEditSession, distinct from the seed panel.
- usePanelEditorDraft compares isSpecDirty against savedPanel; the draft still
  seeds from the (possibly handed-off) panel.
- usePanelEditorQuerySync computes isQueryDirty as a V5-envelope comparison of
  the live query against the saved queries, routing both sides through the same
  fromPerses->toPerses round-trip so builder-added defaults absent from an older
  stored query never read an untouched panel as modified. Replaces the racy
  captured-first-stagedQuery baseline.
- Drop the mount forceReset on useShareBuilderUrl (both its reasons are now
  moot) so a refresh / browser Back-Forward hydrates the edited query from the
  URL and the staged-query effect syncs it into the draft — query edits survive
  a refresh instead of reverting to the saved query.

Add real-QueryBuilderProvider integration tests covering untouched-not-dirty
(incl. an older/minimal stored query), refresh retention, and handoff-dirty;
update the draft + query-sync unit tests.
2026-07-21 06:40:43 +00:00
Vikrant Gupta
806853798d feat(authz): add support for telemetry resource provisioning (#12168)
* feat(authz): provision telemetry roles with plaintext selectors and hashed tuples

Accept a user-facing telemetry selector string (<query_type>/<key>/<value>
with trailing wildcards), validate and canonicalize it, store it as-is in the
role JSON record, and hash it only at the OpenFGA boundary in Object(). Grant
and check both flow through Object() so the hashes match; the plaintext record
stays the readable source of truth for display and recreation.

Because the hash is one-way, role Update diffs at the tuple level via
DiffTuples instead of reconstructing transaction groups from stored tuples.

* refactor(authz): rename telemetry selector helpers and unexport hash

Rename grant_selector.go to selector.go, CanonicalizeTelemetryGrantSelector to
NewTelemetryGrantSelector, and CanonicalTelemetryGrantKey to NewTelemetryGrantKey.
Unexport telemetrySelectorHash since Object() is its only caller.

* fix(authz): expand telemetry ladder in the permission check API

The check API only probed the exact selector plus the full wildcard, so a
scoped grant like builder_query/service.name/* reported a concrete query as
unauthorized even though enforcement allowed it. Relocate the grant selector
ladder to telemetrytypes as NewTelemetryGrantSelectors so both enforcement and
the check API share it, and canonicalize plus fan out the ladder for telemetry
check transactions. Non-telemetry resources keep the exact-plus-wildcard probe.

* refactor(authz): move newCheckSelectors to the bottom of tuple.go

* fix(authz): reject key-scoped selectors for promql and clickhouse_sql

Only builder_query and builder_sub_query extract key-scoped selectors on the
check side, so a grant like clickhouse_sql/service.name/signoz could never
match anything. Track key-scope support per query type and reject the
<query_type>/<key>/<value> form for query types that only emit <query_type>/*.

* test(authz): use a valid promql selector in the check API test

The promql/service.name/service-a case became invalid input after key-scoped
promql/clickhouse_sql selectors were rejected, so the check endpoint returned
400 instead of 200. Use promql/* to keep exercising the different-query-type
denial with a valid selector.

* feat(authz): allow signoz.workspace.key.id as a telemetry grant key

Add signoz.workspace.key.id to the telemetry grant key allowlist so roles can
scope telemetry access by ingestion key, alongside service.name. Grant
validation and check-side extraction both pick it up through the shared
NewTelemetryGrantKey path; bare and resource.-prefixed spellings fold to the
same canonical key.

* Revert "feat(authz): allow signoz.workspace.key.id as a telemetry grant key"

This reverts commit c50d122b54.

* feat(authz): scope telemetry grants by signoz.workspace.key.id

Make signoz.workspace.key.id the sole telemetry grant key instead of
service.name, so roles scope telemetry access by ingestion key. The allowlist
is the only production change; grant validation and check-side extraction are
name-agnostic. Update the querierauthz integration suite and unit tests to the
new key.

* test(authz): update query_range_resources extractor tests to signoz.workspace.key.id

The grant-key switch left this extractor test filtering on and expecting
builder_query/service.name/... IDs, which now fall back to builder_query/*.
Rename the filters and expectations to signoz.workspace.key.id.
2026-07-21 06:13:13 +00:00
Jugal Kishore
f002b29685 fix(onboarding): list Temporal Cloud Metrics under metrics, not APM/Traces (#12203)
Temporal Cloud Metrics scrapes an OpenMetrics endpoint but only showed
under APM/Traces, bundled with the Go and TypeScript SDKs. Split it into
a standalone metrics card and trimmed the Temporal APM card to Go and
TypeScript.

Closes SigNoz/growth-pod#1122
2026-07-21 06:08:36 +00:00
Vinicius Lourenço
a5d4ef4498 fix(uplot): missing destroy uplot on unmount (#12182)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
2026-07-21 03:02:32 +00:00
Tushar Vats
967289ebc6 fix(metrics): resolve any filter key context instead of erroring (#12188)
* fix(metrics): resolve any filter key context instead of erroring

The metrics condition builder hard-errored ("key <name> not found") whenever a
filter key was absent from the metadata keys. This hit the full-text search
column (metric_name in the Metrics Explorer, labels in query_range) and every
explicit label context (resource./scope./attribute.), so a bare/partial filter
token 400'd the Metrics Explorer treemap and metric queries.

Metrics keeps every label in the `labels` JSON with no per-context storage, so
FieldFor already maps any key on its own (intrinsics incl. metric_name to their
column, every label context to a labels JSON extract). Use the key directly in
the len(keys)==0 branch so filters resolve and run instead of 400ing - matching
group-by, which already collapses all label contexts to labels.

Update the label-context test to expect the collapse (resource./scope. now
resolve like bare/attribute.), add unit coverage for the branch, and add a
queriermetrics test that a free-text filter no longer errors.

* fix(metrics): resolve free-text and unregistered filter keys instead of erroring

The metrics condition builder hard-errored ("key <name> not found") whenever a
filter key was absent from the metadata keys: the full-text search column
(metric_name on the Metrics Explorer, labels on query_range) and every explicit
label context (resource./scope.). So a bare/partial filter token 400'd the
Metrics Explorer treemap and metric query-builder filters.

Metrics has no per-context storage - every label lives in the `labels` JSON, and
FieldFor maps any key on its own (intrinsics incl. metric_name to their column,
every label context to a labels extract). Add an opt-in WithIgnoreNotFoundKeys
to the metrics condition builder that resolves such keys directly (matching
nothing) instead of erroring:

- query-builder (querier provider) and Metrics Explorer opt in, so free-text and
  unregistered contexts resolve - matching the statement builder's own fallback
  and group-by, which already collapses all label contexts to labels;
- infra-monitoring list APIs keep the default (strict), so a typo'd attribute
  still surfaces as a 400.

Intrinsics always resolve regardless of the flag. Adds unit coverage for both
modes and a queriermetrics test that a free-text filter no longer errors; the
label-context test now expects the collapse.

* chore: updated integration tests

* fix: dont assert warning == []

* chore: removed warning scaffolding for now

* fix: add back removed integration test

---------

Co-authored-by: nikhilmantri0902 <nikhil.mantri1999@gmail.com>
2026-07-21 01:48:54 +00:00
Naman Verma
2161ea9ef0 test: use appendf instead of sprintf 2026-07-21 00:53:30 +05:30
Naman Verma
61700665f6 test: remove test for floating pt value 2026-07-21 00:52:26 +05:30
Naman Verma
8ad07d758c fix: fix build errors post merge conflicts 2026-07-21 00:39:34 +05:30
Naman Verma
c7cfc58a36 Merge branch 'main' into nv/dashboard-migration 2026-07-21 00:30:11 +05:30
Srikanth Chekuri
bf0130a983 fix(clickhouseprometheus): resolve __name__ matchers against metric_name and anchor matcher regexes (#12154)
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
The read client reduced every __name__ matcher to metric_name = <value>
(empty string when absent), so nameless selectors ({job="api"}), regex
names ({__name__=~".+"}), and negations silently returned empty. Map the
__name__ matcher onto the metric_name column per matcher type, drop the
condition when no __name__ matcher exists, and narrow the samples query
by the metric names the series lookup discovered.

Matcher regexes are now anchored (^(?:...)$) the way Prometheus compiles
them; ClickHouse match() is a substring search, so {job=~"api"} also
matched job="api-server" before.

The lookup and samples SQL is built with huandu/go-sqlbuilder (ClickHouse
flavor) as in telemetrymetrics and the v2 transpiler: the samples query
embeds the series lookup as a nested builder, with argument order merged
by the builder in render position instead of hand-numbered placeholders.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 18:59:34 +00:00
Tushar Vats
9d4349c9d1 fix(querybuilder): resolve data-type collisions for numeric intrinsic columns (#12176)
A span/log query that aggregates or filters a numeric intrinsic column
(e.g. duration_nano) failed with ClickHouse NO_COMMON_TYPE (386) when a
same-named, type-consistent attribute existed in the metadata: field-key
resolution unioned the intrinsic column with the attribute into a multiIf/OR
whose branches had incompatible types (UInt64 intrinsic vs Float64 attribute).
This broke /api/v1/span_percentile on affected tenants.

- fallback_expr: DataTypeCollisionHandledFieldName now handles the unspecified
  data type in the projection/aggregation path (operator Unknown), coercing the
  column so collision multiIf branches share a supertype. Comparison contexts
  are left bare so the column index stays usable.
- telemetrytraces/condition_builder: run collision handling for duration_nano
  (so a same-named string attribute is cast in comparisons) and coerce numeric
  duration values to int64, keeping the intrinsic comparison bare/index-friendly
  while preserving the duration-string QoL parsing.
- tests: add a type-consistent "collision" trace_noise variant; cover it in the
  percentile aggregation test and add a duration_nano QoL filter regression test.
2026-07-20 16:57:39 +00:00
Pandey
d095645d61 fix(querybuildertypesv5): round-trip unset stepInterval and empty field-key values (#12171)
* fix(querybuildertypesv5): omit unset stepInterval on the wire

Step is a struct (struct{ time.Duration }), so omitempty had no effect — an
unset stepInterval serialized as 0 instead of being omitted, so a typed
client reading a query back saw a 0 it never sent (create -> GET drift).

Tag stepInterval with ,omitzero so an unset value is dropped while a set
value still serializes (as seconds), on all three sites: builder query,
trace-operator, and secondary aggregation. Schema-invisible (no OpenAPI /
client change). source and the metric enums were already handled in #12164.

* fix(telemetrytypes): round-trip empty fieldContext/fieldDataType on field keys

A TelemetryFieldKey can deliberately leave fieldContext/fieldDataType empty
to match across any context / data type, but ,omitzero dropped that empty
value on serialize, so a typed client that sent "" read it back as absent
(create -> GET drift).

Make both fields always serialize and add the empty member to their Enum()s
so "" is a valid schema value that round-trips verbatim — the same approach
#12164 used for source. Signal keeps ,omitzero: its empty value is invalid
for the query/variable signal contexts that share the enum (adding "" there
breaks those consumers), and a field key's signal is not deliberately empty.

Regenerate the OpenAPI spec + client (fieldContext/fieldDataType enums gain
"") and update the ScalarData marshal test (column keys now echo the fields).

* fix(telemetrytypes): round-trip empty signal on field keys

Extend the field-key round-trip fix to Signal: add the empty member to
Signal.Enum() and make TelemetryFieldKey.Signal always serialize, so an
empty ("any") field-key signal round-trips as a valid value instead of
being dropped — matching the fieldContext/fieldDataType treatment.

The Signal enum is shared with query/variable signals, where "" is invalid.
Narrow the frontend's TelemetrySignal type to logs/traces/metrics (so the
variable/panel signal selectors stay exhaustive), label the empty member in
the panel type switcher's map, and fold an empty drilldown signal into "all".

Regenerate the OpenAPI spec + client (Signal enum gains ""), and update the
ScalarData marshal test and the querierlogs aggregation label assertions to
include the now-serialized empty signal.
2026-07-20 16:31:47 +00:00
Vinicius Lourenço
429fca62be fix(query-search): do not override token classes (#12139)
* fix(query-search): do not override token classes

* test(token-highlight): add regression test

* fix(pr): address comments
2026-07-20 13:07:07 +00:00
Aditya Singh
20518a98e9 fix(logs-explorer): prevent long json body from overflowing (#12173)
* feat: allow body content to wrap preventing content overflow

* feat: align key name and pin to the top
2026-07-20 13:03:28 +00:00
Gaurav Tewari
0b7c266b09 fix: css fixes for llm o11y model pricing and overview (#12145)
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: css fixes for llm o11y model pricing and overview

* fix: spacing issue

* chore: update modal drawer cost

* chore: remove comments

* chore: spacing between chips right

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-07-20 11:19:48 +00:00
Gaurav Tewari
8014c065e8 feat: rename llm o11y to ai o11y [2/2] (#12141)
* feat: add llm o11y to side nav

* chore: rename route

* chore: review comments

* chore: update constants

* chore: change name to ai

* chore: rename draft

* chore: move model pricing out of settings

* chore: update comment

* revert: undo LLM->AI file/dir moves, keep constant rename

Reverts the two file-relocation commits (c25060950 rename LLMObservability
dir -> AIObservability; 1a18bc848 move ModelPricing out of Settings) while
preserving the LLM->AI string/constant renames (route constants + paths,
side-nav entry/labels, page titles, permission keys). Component files stay
under container/LLMObservability with their original names.

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-07-20 11:18:08 +00:00
Gaurav Tewari
c2c2552cc6 feat: add llm observability to side navigation [1/3] (#12123)
* feat: add llm o11y to side nav

* chore: update comment

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-07-20 08:00:39 +00:00
Pandey
88bd737758 fix(dashboards-v2): round-trip all zero-valued spec fields (#12162)
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(dashboardtypes): round-trip zero-valued variable/display fields

omitempty dropped explicit zero values from the create -> GET response, so
a typed client (Terraform/SDK) that sent them read back null and reported
drift. Remove the tag so these always serialize:

- Display.Description ("" round-trips; applies to dashboard/panel/variable
  displays)
- TextVariableSpec.Constant (constant: false, like the disabled fix)
- ListVariableSpec.CustomAllValue / CapturingRegexp ("" round-trips)

Scalars carry no nullability, so the OpenAPI spec and generated client are
unchanged. Sort stays omitzero: its "no sort" value is "none", not "", so
omitzero only omits the invalid unset state.

* fix(dashboardtypes): round-trip panel and dashboard links

`links` used omitempty (dropped an explicit []) and its element type was the
imported perses dashboard.Link, whose own fields tag name/tooltip/
renderVariables/targetBlank omitempty — so a link's false/"" were dropped
too, and a typed client read them back as null.

- Replicate dashboard.Link as a SigNoz Link type (same pattern as
  ListVariableSpec/TextVariableSpec) with every field always serialized.
- Use ,omitzero on PanelSpec.Links and DashboardSpec.Links so an explicit
  [] round-trips while an unset list stays omitted (never null).

Regenerate the OpenAPI spec and frontend client: the element schema is now
DashboardtypesLink (was the perses DashboardLink) and links is nullable.
Update the frontend consumers to the renamed type and coalesce the now
type-nullable spec.links (never null on the wire) at its two boundaries.

* test(dashboard): cover variable/display/link round-trip cases

Extend the v2 dashboard round-trip test with the spec-wide zero values this
PR fixes: a display description "", a text variable's constant false, a list
variable's customAllValue/capturingRegexp "", an explicit [] of panel links
that round-trips, a link whose own zero-valued fields (name/tooltip "",
renderVariables/targetBlank false) echo back, and a linkless panel whose
links stay omitted (never null).

* test(dashboard): accept null-or-absent for unset panel links

A panel with no links round-trips as "links": null rather than being
omitted (the panel serialization path differs from the query slices, which
omit). Both mean "no links" and neither drifts for a typed client, so assert
the value is None (null or absent) instead of strictly absent. The explicit
[] case still asserts a verbatim round-trip, which is the guarantee the fix
provides.

* fix(dashboardtypes): round-trip remaining zero-valued spec fields

Complete the dashboards-v2 create -> GET round-trip audit:

- DashboardSpec.Datasources: ,omitempty -> ,omitzero so an explicit {}
  round-trips (omitempty dropped it) while an unset map stays omitted.
- DashboardV2 Image, DashboardSpec.Duration/RefreshInterval: drop ,omitempty
  so an explicit "" round-trips (same class as Display.Description). The
  server accepts "": DurationString.validate() returns nil for len 0, and
  Image/Duration/RefreshInterval have no create-time validation, so a
  GET-then-PUT of "" is not rejected.

Scalars carry no nullability (no spec change); the datasources map is now
nullable: true in the regenerated OpenAPI spec and client.

* test(dashboard): cover datasources/image/duration/refreshInterval round-trip

Extend the round-trip test with the spec-wide zero values just fixed: a
dashboard-level image "", spec duration/refreshInterval "", and an explicit
empty datasources {} that must echo back as {} (omitzero) rather than being
dropped.
2026-07-19 20:42:27 +00:00
Pandey
8c40d8c2ca fix(querybuildertypesv5): allow empty source, temporality and timeAggregation in the metric query spec (#12164)
* fix(querybuildertypesv5): omit unset metric enum fields on the wire

A metric builder query serialized empty strings for its enum fields
because omitempty has no effect on struct-backed valuer types:

  "source":"", "aggregations":[{"temporality":"","timeAggregation":"","spaceAggregation":""}]

Those "" values are not members of the corresponding OpenAPI enums
(source=[meter], temporality=[delta,cumulative,unspecified], etc.), so a
typed client reading a rule back rejected it (create -> GET round-trip
drift; terraform-provider-signoz generate-config failed schema validation).

Tag Source/Temporality/TimeAggregation/SpaceAggregation with ,omitzero so
an unset value is dropped instead of emitted as an invalid "", matching the
existing convention (dashboardtypes Sort, telemetrytypes field keys). Valid
values still serialize. The OpenAPI spec regenerates byte-identical, which
confirms the enums were already correct.

* test(querybuildertypesv5): cover client-sent empty enum values

A client (e.g. terraform) may send explicit source:"" / temporality:"" for
an unset enum. Assert unmarshaling accepts them, normalizes to the zero
value, and re-marshaling drops them so the round-trip never echoes an
invalid "" back.

* fix(querybuildertypesv5): allow empty metric enum values in the spec

The server accepts and echoes back an unset source, temporality, and
timeAggregation for a metric query (a create -> GET returns "" for them), but
their OpenAPI enums omitted "". A typed client (terraform-provider-signoz
generate-config) therefore rejected the config generated for an imported rule.

Add "" as a valid member of the Source, Temporality, and TimeAggregation enums
so the spec matches what the server actually accepts and returns. spaceAggregation
is left unchanged: an empty value is rejected with 400 at creation (IsValid), so
it is never stored or echoed and "" must stay out of its enum.

Drop the earlier ,omitzero tags: these fields already always-serialize, so an
accepted "" round-trips faithfully instead of being silently dropped (silent
mutation is itself drift). source loses its no-op omitempty for the same reason.

Regenerate the OpenAPI spec and frontend client (both git-diff gated).

* test(querybuildertypesv5): assert accepted empty enums round-trip

Empty source/temporality/timeAggregation are echoed back (not dropped) and are
stable across marshal -> unmarshal -> marshal; spaceAggregation carries a valid
value since an empty one is 400'd at creation.

* test(querybuildertypesv5): merge and rename metric enum round-trip test

Fold the unmarshal-echo case into the table-driven marshal round-trip test
(its marshal -> unmarshal -> marshal check already covers the client-sends-""
path) and rename to TestQueryBuilderQuery_MetricAggregation_MarshalJSONEnumRoundTrip.

* style(metrictypes): drop explanatory comments on enum changes

Remove the comments added to Temporality/TimeAggregation Enum() and the metric
enum round-trip test case.

* style(telemetrytypes): drop explanatory comment on Source enum change

Remove the comment added to Source.Enum(), keeping the pre-existing doc/TODO.
2026-07-19 19:52:51 +00:00
Srikanth Chekuri
7be340ce69 fix(clickhouseprometheus): include hour-floored registration rows at the query window end (#12153)
Some checks failed
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
The series lookup bounded registration rows with unix_milli < end while
the samples query uses <= end. The exporter writes registration rows
hour-floored, so a series first registered in the hour starting exactly
at end was invisible even though its samples were fetchable — queries
ending exactly on an hour boundary missed newly-registered series.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:27:13 +00:00
Tushar Vats
852ca779c9 fix: convert key not found to warnings for logs (#12134)
* fix: convert key not found to warnings

* fix(logs): has-family body-only errors, not-found warnings, and condition-builder cleanup

- has/hasAny/hasAll/hasToken on a non-body key now return "supports only body
  JSON search" (both modes); a not-found body path warns and queries the
  underlying data instead of 400 (JSON mode)
- capture the two has-family 500s (hasToken separator/whitespace needle;
  hasAny/hasAll quoted int >= 2^32) as regression tests
- use telemetrytypes.NewTelemetryFieldKey for derived keys (fresh identity-only
  keys, no stale resolved metadata) instead of struct copies of the field key
- rename ConditionForKeys -> ConditionFor and inline conditionsForKeys into it
  across the builders; rename private conditionFor -> conditionForResolvedKey
- move the trace_noise fixture from queriertraces/conftest.py to fixtures/traces.py

* fix: convert trace_noise fixture to util
2026-07-19 18:41:53 +00:00
Srikanth Chekuri
05b35dd4ee fix(clickhouseprometheus): merge fingerprints sharing a labelset instead of injecting a fingerprint label (#12152)
#8563 worked around duplicate-labelset collisions (a label value going
empty re-fingerprints a series; empty-valued labels are dropped at read)
by appending a synthetic fingerprint label to every series and stripping
it from results. The label participates in evaluation before any strip
runs: without() grouped per fingerprint (duplicate identical-labeled
output series) and unaggregated vector matching never matched.

Resolve series identity at the adapter instead: drop empty-valued labels
at unmarshal, merge fingerprints that share the resulting labelset
(timestamp-ordered k-way merge, highest fingerprint wins equal
timestamps), and delete the injection and every RemoveExtraLabels strip.
Engine-semantic duplicates (e.g. rate over {__name__=~"a|b"}) still
error, matching upstream Prometheus.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 17:43:47 +00:00
Srikanth Chekuri
bcea24f449 fix(querybuildertypesv5): keep subnormal values finite in roundToNonZeroDecimals (#12157)
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
The sub-1 branch's scale (10^(-order+n-1)) overflows to +Inf for
subnormal inputs (order below ~-308), and Inf/Inf turned a finite stored
value into NaN — which then serializes as the string "NaN". Same guard
as the >=1 branch got in #12151; adds the unit coverage for both.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 09:49:08 +00:00
Pandey
22e55340d7 fix(dashboards-v2): round-trip zero-valued threshold + query fields (#12158)
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(dashboardtypes): accept threshold value of 0 on create

A NumberPanel/TimeSeries/Table threshold with `value: 0` (a legitimate
value the SigNoz UI emits by default) was rejected on create with
`dashboard_invalid_input` "Field validation for 'Value' failed on the
'required' tag".

go-playground/validator's `required` treats a numeric field equal to its
zero value as "missing", so `validate:"required"` on the float `Value`
wrongly rejected 0. Drop `validate:"required"` from `Value` on
ThresholdWithLabel and ComparisonThreshold; keep `required:"true"` since
the field is always present in the schema (0 is a valid present value, not
an absent one), so the OpenAPI/generated client are unaffected. `Color`
keeps both tags — an empty colour is genuinely invalid.

Drop the two "missing value" cases from TestValidateRequiredFields, which
asserted the removed invariant.

* fix(querybuildertypesv5): round-trip zero-valued query spec fields

A dashboard/alert query that sets a zero-valued field — `disabled: false`,
`legend: ""`, or an explicit empty `groupBy`/`order`/`selectFields`/etc. —
created fine but the GET response omitted it, so a typed client that echoes
what it sent (Terraform, SDKs, PUT-after-GET) read back `null`/absent and
reported drift. `,omitempty` dropped these zero values on the way out.

Fix the create -> GET asymmetry:

- Slice fields use `,omitzero` instead of `,omitempty`. `omitzero` omits a
  nil slice (field never set stays absent) but keeps an explicit non-nil
  `[]`, so an empty array round-trips as `[]` and there is no `null`
  regression. Applied to groupBy, order, selectFields, aggregations,
  functions, secondaryAggregations and function args across the builder,
  formula, trace-operator and join specs, plus ListPanelSpec.selectFields.
- Scalars `disabled` (bool) and `legend` (string) drop the tag entirely;
  `omitzero`/`omitempty` both suppress false/"", so the only way to
  round-trip them is to always serialize.

Result types in resp.go keep `,omitempty` — they are server-computed and
never round-tripped. Regenerate docs/api/openapi.yml and the frontend
client: the omitzero slices are now `nullable: true` in the schema (never
null on the wire, but the generated types gain `| null`, which existing
consumers already handle via `?? []`).

* test(dashboard): round-trip serialization for zero-valued fields

Add a v2 dashboards integration test that creates one minimal dashboard
(stripped from SigNoz/dashboards cicd-perses.json) and asserts the
create -> GET round-trip preserves every zero-valued field the fix targets:

- threshold value 0 (ComparisonThreshold + ThresholdWithLabel) is accepted
  on create and echoed back
- builder slices set to an explicit [] (groupBy/order/selectFields/functions)
  round-trip as [], while a bare builder's unset slices stay absent (never
  null) on read
- scalars disabled/legend always echo false/""

Table-driven: one equality table for round-tripped values and one absence
table for omitted slices.

* test(dashboard): fold round-trip test into 03_v2_dashboard

Move test_dashboard_v2_roundtrip_preserves_zero_values alongside the other
v2 dashboard tests (test_create_rejects_*, lifecycle, ...) instead of a
standalone file, with the dashboard payload inlined per this suite's style.
2026-07-18 19:52:54 +00:00
Tushar Vats
dd8cbe0844 fix: has function family revamp (#12089)
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-18 16:43:26 +00:00
Srikanth Chekuri
6b065aa54c fix(querybuildertypesv5): guard float overflow in roundToNonZeroDecimals (#12151)
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
math.Round(val*multiplier)/multiplier overflows to +Inf for finite values
near math.MaxFloat64, turning a valid series value into JSON-unmarshalable
Inf downstream. Return the value unrounded when the scaled intermediate
overflows.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 08:48:33 +00:00
Vikrant Gupta
ac5b5947eb feat(authz): enable FGA for telemetry resources (#12095)
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(authz): enable FGA for telemetry resources on v5 query_range

Authorize /api/v5/query_range and /preview at the telemetry-resource level,
derived from the request body:

- coretypes: ResourceWithID + ResourceExtractor as the resource-level analogue
  of the id extractors; NewResolvedResourceWithID/NewResolvedResourceWithError;
  telemetryresource selector regex widened to query-type selectors with up to
  two hashed segments (metric name, where clause) or wildcards
- telemetrytypes: QueryRangeResources maps each query to its telemetry
  resource (signal/source aware: audit-logs, meter-metrics) with a hierarchical
  selector id (query_type/<hash(metric)>/<hash(where)>); PrefixSelector expands
  the id into the grant ladder [exact, prefix/*..., *]
- handler: generic TelemetryResourceDef fans out an injected ResourceExtractor;
  fails closed when extraction errors or resolves nothing
- audit: log and skip resolved resources that carry a resolution error
- querier routes: ViewAccess -> CheckResources with telemetry read scopes;
  substitute_vars stays ViewAccess (no telemetry access)
- sqlmigration 099: backfill telemetry read tuples for existing orgs
  (admin: logs/traces/metrics/audit-logs/meter-metrics; editor/viewer:
  logs/traces/metrics)

* feat(authz): widen telemetry selector segments to 128 bits

64-bit truncation permits chosen-collision attacks at ~2^32 work; 128 bits
pushes this to 2^64. No hashed selector is persisted yet, so the change is
free.

* chore(docs): regenerate openapi spec with telemetry read scopes

* feat(telemetry): add where clause visitor

* refactor(telemetry): restructure normalizer file and quote bare values

* feat(authz): gate v5 query_range on service.name telemetry selectors

* feat(authz): encode telemetry grants as query-type qualified atom selectors

* feat(authz): move telemetry grant key to plaintext selector segment

* feat(authz): use escaped plaintext telemetry selectors with mechanical ladder

* revert(authz): restore transaction group diff in role update

* test(authz): add querierauthz integration suite for telemetry query_range gating

* test(authz): seed logs so service.name resolves in allowed querierauthz cases

* feat(authz): backfill telemetry read tuples for existing orgs

* chore(authz): reword empty composite query error message

* feat(authz): add meter metrics and audit logs to clickhouse sql

* Revert "feat(authz): add meter metrics and audit logs to clickhouse sql"

This reverts commit c9d870e0ee.

* feat(authz): grant meter-metrics to editor/viewer, keep clickhouse admin-only

* feat(authz): remove the audit logs from clickhouse check altogether until it's introduced
2026-07-17 10:59:58 +00:00
Naman Verma
9bfd293774 chore: final set of error fixes/hacks 2026-07-17 16:23:04 +05:30
Vinicius Lourenço
c563d79ee0 feat(quick-filters): add support for /v1/fields/values (#11788)
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(quick-filters): add field values hook

* feat(quick-filters): add support to handle indeterminate state of checkbox

* feat(quick-filters): add checkbox header v2

* feat(quick-filters): add item rules to represent the states of checkbox

* feat(quick-filters): add checkbox row v2

* feat(quick-filters): add sectioned values hook to correctly calculate checkboxes to render

* feat(quick-filters): add hook to get existingQuery for field values

* feat(quick-filters): add checkbox filter v2

* test(quick-filters): add tests for checkbox filter v2

* feat(quick-filters): add flag to enable v2

* feat(infra-monitoring): enable quick filters v2

* fix(infra-monitoring): change the prefix of metric namespaces

* fix(checkbox-v2): update to improve ux

* fix(quick-filters): move from flat list to sections

* fix(quick-filters): render related/all values when searching

* Revert "fix(infra-monitoring): change the prefix of metric namespaces"

This reverts commit 176dc6d3bc.

* Revert "feat(infra-monitoring): enable quick filters v2"

This reverts commit 8902a157c1.

* fix(pr): address comments

* feat(infra-monitoring-v2): enable use new quick filters

* fix(pr): cleanup old file

* fix(quick-filters): avoid flick of old selected value on related value
2026-07-17 07:38:33 +00:00
Naman Verma
f26681091d Merge branch 'main' into nv/dashboard-migration 2026-07-17 13:00:05 +05:30
Aditya Singh
b9b92f951b feat: table export in Logs and Traces explorer table tabs (4) (#12071)
* refactor(data-export): extract the withUnit header helper

Both the timeseries and the upcoming table serializers append units to headers (and skip display-only ids like 'short'/'none') — move the helper to its own module.

* feat(data-export): add generic table-model serializer

exportTableData serializes any prepared antd-style table model ({name, key, isValueColumn} columns + record rows) into a SerializedTable — raw values in display order, units on value columns only, blanks for missing cells. Surface-agnostic: QueryTable-based tables, dashboard tables and plain antd tables all adapt in a line or two.

* feat(data-export): add scalar queryRange serializer

exportScalarData takes the queryRange response object (formatForWeb webTables payload) + the builder query and serializes via createTableColumnsFromQuery — the exact preparer QueryTable renders from — so exports inherit the on-screen merge, naming and column order 1:1.

* feat(data-export): dispatch client exports from the queryRange response

useClientExport now takes the queryRange response object views already hold and picks the serializer from what it carries: rawV5Response (time_series) or the formatForWeb scalar payload (tables) — mounts pass their data without choosing serializers. TimeseriesExportMenu generalizes into components/ExportMenu with the same uniform inputs, so the timeseries views and the table tabs share one menu.

* feat(explorer): enable table export in Logs and Traces table tabs

Mounts ExportMenu on the Logs and Traces Table tabs, feeding the same query the rendered table uses (stagedQuery fallback parity) — exports match the on-screen table's merge, naming and column order. Header rows are class-driven, right-aligned, gated on data presence.

* test(data-export): assert scalar export filename via the naming helper

Same review cleanup as the foundation PR: frozen clock + delegation to getTimestampedFileName instead of a duplicated format regex.
2026-07-17 06:16:53 +00:00
Abhi kumar
6bcc2d66d9 feat(dashboards-v2): pre-populate alert condition from panel Reduce To + thresholds (#5291) (#12137)
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
* refactor(alerts-v2): extract alert condition normalizers to a leaf module

normalizeOperator/normalizeMatchType move from CreateAlertV2/utils.tsx into a
dependency-free context/conditionNormalizers.ts (re-exported from utils for existing
callers), so the CreateAlert context can consume them without an index<->utils cycle.

* feat(alerts-v2): apply alert-condition prefill declaratively from the URL

The CreateAlertV2 context now resolves a declarative prefill from the URL
(resolveUrlAlertPrefill) and applies each field iff present, instead of branching on
the request source. Producers own their semantics: ingestion settings now explicitly
declares matchType=in_total + evaluationWindowPreset=meter, so adding a new producer
needs no consumer change.

* feat(dashboards-v2): pre-populate alert condition from panel Reduce To + thresholds (#5291)

Creating an alert from a V2 panel now seeds the condition: Reduce To -> occurrence
type (sum->in total, avg->on average; Value panel only), and the highest-danger
threshold (Red>Orange>Green>Blue) -> operator, target value, and unit. Formula panels
apply the reduce only when all queries agree. The seed rides the /alerts/new URL and is
applied by the declarative CreateAlertV2 consumer.
2026-07-16 22:08:24 +00:00
Naman Verma
d798164da9 chore: undo schema change in threshold value 2026-07-17 00:49:43 +05:30
Pandey
5ee1ca7d05 fix(ruletypes): round-trip empty notificationSettings fields (#12138)
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(ruletypes): always serialize notificationSettings.usePolicy

usePolicy is a plain bool tagged omitempty, so a false value is dropped
from the GET response. Clients that pin usePolicy to false read it back as
absent/null. Drop omitempty (matching Renotify.Enabled) so false always
serializes and round-trips.

* test(ruletypes): assert usePolicy round-trips in minimal read shape

usePolicy now always serializes, so the minimal read shape includes it with a false value rather than omitting it.

* fix(ruletypes): always serialize notificationSettings.groupBy

Like usePolicy, groupBy dropped its omitempty so an explicitly-set empty value round-trips instead of reading back as absent. A nil groupBy now serializes as null.

* fix(ruletypes): use omitzero for notificationSettings.groupBy

omitzero omits groupBy only when nil (unset) while preserving an explicitly-set empty array as [], so unset and empty stay distinguishable instead of every response carrying groupBy: null. Matches the sibling newGroupEvalDelay tag.

* fix(ruletypes): use omitzero for renotify.alertStates

Symmetric to groupBy: omitzero omits alertStates only when nil (unset renotify configs stay absent, no regression) while echoing an explicitly-set empty array as [].

* chore(ruletypes): regenerate openapi spec and api client

groupBy and renotify.alertStates switched to omitzero, so the generated spec marks both slices nullable and the orval client types them as [] | null.
2026-07-16 18:13:24 +00:00
Gaurav Tewari
98c0c53a2b feat: add dashboard in llm o11y (#12136)
* feat: add locked dashboard

* feat: update test cases

* chore: add comments

* chore: update comment

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-07-16 15:31:06 +00:00
Gaurav Tewari
6d3861d60f feat(llm-attribute-mapping): group + mapper drawers + diff-save [3-4/5] (#11780)
* 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

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

* feat(llm-attribute-mapping): read-only groups and mappers listing

* feat(llm-attribute-mapping): group create/edit/delete via drawer with diff-save

* fix: css styling

* style(llm-attribute-mapping): use design-token css variables for spacing and typography

* fix(llm-attribute-mapping): wrap long condition keys so the context badge isn't clipped

* fix(llm-attribute-mapping): unlock filters column shrink so long keys wrap within the cell

* fix: text overflow in attribute mapping

* fix: fetch data only when tab is open

* feat: add attribute mapping tab

* fix: update col count

* chore: review comments

* fix: input

* fix: update logic

* fix: update drawer

* feat: update draft and mapper

* refactor: migrate to css moduel

* refactor: migrate to css module

* refactor: migrate to css module

* refactor: css module

* feat(llm-attribute-mapping): read-only listing on CSS modules [2/5]

Rebase the listing slice onto the foundation's CSS-module refactor
(which deleted the global stylesheet) and migrate it accordingly:

- Merge listing styles into LLMObservabilityAttributeMapping.module.scss
  (groups/mappers tables, source chips, index badge, error/footer).
- Convert all listing components from global BEM classNames to
  styles.* module access; drop dead/style-less classes (am-table,
  am-row-actions, am-add-row, *_edited, mappers-table__error).
- Adopt theme-aware semantic tokens (--l2/l3-*, --accent-primary,
  --callout-error-*) in place of --bg-* primitives.

* refactor(llm-attribute-mapping): migrate styles to CSS modules

* 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

* feat(llm-attribute-mapping): read-only listing on CSS modules [2/5]

Rebase the listing slice onto the foundation's CSS-module refactor
(which deleted the global stylesheet) and migrate it accordingly:

- Merge listing styles into LLMObservabilityAttributeMapping.module.scss
  (groups/mappers tables, source chips, index badge, error/footer).
- Convert all listing components from global BEM classNames to
  styles.* module access; drop dead/style-less classes (am-table,
  am-row-actions, am-add-row, *_edited, mappers-table__error).
- Adopt theme-aware semantic tokens (--l2/l3-*, --accent-primary,
  --callout-error-*) in place of --bg-* primitives.

* chore: migrate tanstack table

* chore(llm-attribute-mapping): remove orphaned IndexBadge

The TanStack mappers table dropped the leading index column, leaving
IndexBadge and its .indexBadge style unused. Remove both.

* 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

* chore: update ui and add animation

* refactor: components update

* chore: typography changes

* chore: typography changes

* chore: use badge

* refactor: basic components

* chore: remove hardcoded value

* chore: add comments & tests

* chore: update mapper

* chore: update test

* chore: minior ux update

* chore: lodash isEqual

* chore: use lodash

* chore: update save draft

* 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

* chore: update test cases

* chore: remove unused component

* chore: update types

* chore: update design

* chore: update ui

* refactor: minor things

* chore: break down thingsinto comps

* chore: update files

* fix: update mapping

* chore: remove draft logic no need for now

* chore: more refactor

* chore: remove comments

* chore: refactor route

* chore: update query refech on mount

* chore: self review changes

* chore: update skeleton

* refactor: code

* chore: sync up with group mappers

* chore: only 1 mapper row at a time

* refactor: code

* refactor: eslint disable

* chore: update selector

* chore: sort

* refactor: review changes

* chore: use refetch group

* feat(llm-attribute-mapping): mapper drawer + persistMappers [4/5] (#11781)

* feat(llm-attribute-mapping): mapper drawer + persistMappers [4/5]

Core of the mapper drawer work: the MapperFormDrawer + SourceAttributeRow
UI, persistMappers store logic, mapper row/actions, and supporting
utils/types/saveDraft changes.

Removes the barrel (index.ts) files introduced by this PR
(MapperFormDrawer, MapperActionsMenu, SourceAttributeRow) and imports
those modules directly instead.

Tests, public locales, the conditions tooltip, and the KeySearchInput
relocation follow in a stacked follow-up PR.

* test(llm-attribute-mapping): tooltip, locales, tests + drop barrels [4/5 follow-up]

Follow-up to the mapper drawer core PR. Contains:
- AttributeMappingsTab + LLMObservabilityAttributeMapping test suites
- public locale (titles.json) updates
- the group ConditionsTooltip and its GroupHeader wiring
- relocation of KeySearchInput to the shared components/ dir
- removal of the remaining barrel (index.ts) files across the
  AttributeMapping feature, importing each module directly

* chore: remove comments

* chore: reduce width

* fix: no refetch on open

* chore: add discard dialog

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>

* chore: remove reduced motion

* chore: self review changes

* chore: c should be capital

* chore: review changes

* refactor: sync server store

* chore: update tests

* Revert "chore: update tests"

This reverts commit 5f39c910df.

* Revert "refactor: sync server store"

This reverts commit b92f902d9f.

* chore: rename to editor

* chore: resolve review comments

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-07-16 14:32:35 +00:00
Naman Verma
4f8c555eef test: fix span gaps test 2026-07-16 18:47:33 +05:30
Nikhil Mantri
20e13f66e0 fix(infra-monitoring): use proper metric names corresponding to entities to be counted (#12130)
* chore: write proper lists of metricNames to lookup for counts

* chore: updated integration tests
2026-07-16 12:54:21 +00:00
Naman Verma
e5eb47e360 chore: handle no string value for variable sort 2026-07-16 18:13:03 +05:30
Naman Verma
474ecba392 chore: add handling for trace operators around wrapinv5env 2026-07-16 18:12:43 +05:30
Naman Verma
c7d116b130 chore: pass threshold format raw without typecasting 2026-07-16 16:53:44 +05:30
Naman Verma
b611e3adc7 chore: skip empty type variables 2026-07-16 16:42:46 +05:30
Naman Verma
598f466682 chore: drop widgets with >1 and all aggregations as malformed 2026-07-16 16:15:07 +05:30
Naman Verma
7a29fdbcaf chore: skip unknown panel kind 2026-07-16 16:14:32 +05:30
Ashwin Bhatkal
07fe353b94 refactor(dashboard-v2): organize variable subsystem into components/ hooks/ utils/ (#12135)
* chore(dashboard-v2): drop self-explanatory comments from the variable subsystem

Remove comments that restate a well-named symbol or narrate obvious steps, and
trim verbose JSDoc to the non-obvious "why". No behavior change.

* refactor(dashboard-v2): organize VariablesBar into components/ hooks/ utils/

Split the flat VariablesBar directory into components/ (each component in its
own folder, co-locating a component-specific .module.scss), hooks/ (all use*
including useVariableOptions, moved out of selectors/), and utils/ (pure
modules). The bar's shared cross-cutting classes and .control stay in the root
VariablesBar.module.scss. Import paths updated across the subsystem; no
behavior change. Also updates a stale URL assertion in the seed test left over
from the store-source-of-truth change.

* refactor(dashboard-v2): organize DashboardSettings/Variables into components/ hooks/ utils/

Move the loose files out of the Variables root: pure modules into utils/
(rename variableDependencies -> variableCycleDetection to end the name clash
with VariablesBar's graph engine), useSaveVariables into hooks/, and VariableRow
and VariablesList into their own component folders with co-located .module.scss.
The shared model, adapters and types stay at the root as the folder's public
API. Import paths updated across the subsystem; no behavior change.

* refactor(dashboard-v2): factor the variable tooltip ref lists into one directional component

Extract TooltipRefSection so "Depends on" and "Used by" share one component,
differing only by color and an up/down arrow that depicts the dependency
direction (up = parents this variable depends on, down = children that use it).

* refactor(dashboard-v2): stack the variable name above its control

Move each variable's name into a notch label above the control: fixed-width
columns, name truncated with a hover title and a spaced/centered info icon,
single-line multi-select, ~33px control height, and a visible dropdown caret.
Nudge the toolbar spacing so the time selector aligns with the shorter bar.
Also render the impact dialog's "still references" warning via Typography
color="warning" instead of a bespoke CSS class.

* fix: top margin
2026-07-16 10:27:44 +00:00
Ashwin Bhatkal
9fb89fafc8 feat(dashboard-v2): variable runtime — resolver, fetch engine, panel loading, impact dialog (#12125)
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): single default resolver for variable selection

One source of truth for a variable's value (seed → reconcile → payload), shared by the bar, the fetch gate and the query payload so they can't disagree. Precedence stays URL → store → default; ALL materializes once, without an extra round-trip.

* feat(dashboard-v2): dependency-ordered, value-gated variable fetch engine

Query roots + dynamics fetch immediately; query children unblock only once their parents hold a committed value, so a chain fetches each variable once. The cycle is idempotent per (dashboard·order·time) and resets on page unmount, so a re-mount or editor return can't double it or inherit stale state.

* refactor(dashboard-v2): consolidate variable selectors; add bar tooltip + loading bar

Fold the per-type Query/Dynamic/Custom selectors into one VariableValueControl backed by useVariableOptions. Add a hover tooltip showing a variable's dependencies (Depends on / Used by) and a loading bar flush along the control's bottom edge; refine name/dependency colors to design-system tokens.

* feat(dashboard-v2): hold panels in loading until referenced variables resolve

A panel waits until every QUERY/DYNAMIC variable it references is ready — a concrete pick and a DYNAMIC ALL are ready immediately, an unselected value or a QUERY ALL waits until it resolves. The wait folds into the panel's loading flags (a disabled query reports neither), so it never flashes "no data". Only referenced variables re-key the cache.

* feat(dashboard-v2): rename/delete variable impact dialog

Renaming or deleting a referenced variable is blocked behind a dialog listing every usage (QB / PromQL / ClickHouse panels and other variable defs), with a current → resulting preview, per-usage selection, and inline edits; applied as one JSON-Patch.

* fix(dashboard-v2): don't flash ALL on a multi-select while its options load

CustomMultiSelect derives "all selected" from options + the current value; with options still empty a concrete selection reads as covering everything. Only enable ALL detection once options have loaded.

* refactor(dashboard-v2): make the store the source of truth for variable selection

The persisted store (localStorage) is now the sole source of truth for runtime
variable values. Stop writing ?variables= on every selection change; the URL
param is read once on load (a share link) to seed the store and then dropped,
so dashboard navigation and drilldown no longer round-trip through the URL. The
panel editor reads the selection from the store (a lagging URL snapshot could
clobber a just-changed value). Drops the now-unused withVariablesSearch.

* refactor(dashboard-v2): address review feedback on the variable subsystem

- consolidate the default resolver: one configuredDefault normalizer shared by
  resolveDefaultSelection and configuredDefaultValue, plus a textDefault helper
  for TEXT (drops the dead textValue/empty-string fallbacks)
- reuse computeVariableDependencies for the bar tooltip's depends-on / used-by
- split the fetch machinery out of useVariableOptions into useFetchedVariableOptions
- extract useVariableListActions from the variables settings page
- reuse removeVariableFromExpression for delete-clause removal (handles dangling
  syntax / parentheses) and drop the local removeVariableReferenceClause
- grey the impact dialog's Current field on uncheck; comment + spacing nits;
  guard useIsPanelWaitingOnVariable so it reads variableTypes without optional chaining
- fix the seed test's read-once URL assertion
2026-07-16 08:54:38 +00:00
Naman Verma
8fe094a2a4 Merge branch 'nv/dashboard-migration' of https://github.com/SigNoz/signoz into nv/dashboard-migration 2026-07-16 11:45:50 +05:30
Abhi kumar
0655328fa9 feat(dashboard-v2): export panels & dashboards from explorers (#12043)
* feat(dashboard-v2): new-panel editor route + query-preserving seed

Add the /dashboard/:id/panel/new route helpers (buildExportPanelLink,
parseNewPanelKind, layoutIndex) and buildNewPanelSeed, which converts an
exported explorer compositeQuery into a seeded panel.

Coerces a builder-only kind (List) to a query-compatible one (PromQL ->
TimeSeries, ClickHouse -> Table) so a non-builder query is preserved rather
than dropped. PanelEditorPage consumes the effective kind; new List panels
seed columns from the query's resolved signal.

* feat(dashboard-v2): flag-aware export-to-dashboard hooks

Add the hooks shared by the explorer "Add to dashboard" flow:

- useGetExportToDashboardLink: builds the V2 panel-editor link (or the V1
  new-widget link) based on the use_dashboard_v2 flag.
- useExportDashboards: flag-aware, search-filtered dashboard source. V2
  searches server-side via the name-contains filter DSL (debounced); V1
  filters its already-complete list in memory.
- useCreateExportDashboard: flag-aware "create a new dashboard" - V2 via the
  Perses createDashboardV2, V1 via the legacy create - normalized to Dashboard.

* feat(dashboard-v2): rebuild "Add to dashboard" export dialog

Replace the antd Modal-based ExportPanel with a @signozhq/ui DialogWrapper
dialog driven by the export hooks. The picker searches server-side and pins the
selected dashboard as an option so it survives a narrowing search; "New
dashboard" creates via the flag-aware hook and navigates to the panel editor.

Removes the old index.tsx / styles.ts.

* feat(dashboard-v2): wire explorers to flag-aware export

Point the Logs/Traces/Metrics explorer "Add to dashboard" actions at
useGetExportToDashboardLink, so an exported query lands in the V2 panel editor
when use_dashboard_v2 is on (the V1 new-widget link otherwise). ExplorerOptions
now renders the rebuilt ExportPanel dialog directly instead of wrapping it in an
antd Modal.

* chore: fixed failing tests

* refactor(dashboard-v2): address explorer-export review feedback

- rename isQueryTypeSupported → isQueryTypeSupportedByPanelKind for clarity
- buildExportPanelLink returns null (no silent TimeSeries default) when a
  panel type has no V2 kind; explorers guard navigation on the null link
- introduce a neutral ExportDashboard {id,title} for the picker instead of
  reusing the V1 Dashboard entity with faked fields
- note why an exported Query's single unit doesn't seed columnUnits

* chore: pr review changes
2026-07-16 05:49:06 +00:00
Vinicius Lourenço
f24102c0db feat(infrastructure-monitoring-v2): pod status/counts & couple more fixes (#12127)
* refactor(infrastructure-monitoring-v2): remove pod phase and add pod status/counts

* fix(column-header): align to left

* fix(cell-value): ensure tooltip is correctly aligned

* fix(entity-traces): use - instead of N/A

* test(entity-traces): fix broken test
2026-07-16 03:34:12 +00:00
Vinicius Lourenço
edee102b52 feat(infrastructure-monitoring): add pod metrics tab (#12106)
* fix(k8s-base-details): do not allow untoggle tabs

* feat(constants): add base queries for all workloads

Ref: https://github.com/SigNoz/engineering-pod/issues/4032#issuecomment-4893103716

* feat(daemon-sets): add pod metrics tab

* feat(deployments): add pod metrics tab

* feat(jobs): add pod metrics tab

* feat(namespaces): add pod metrics tab

* feat(statefulsets): add pod metrics tab

* fix(k8s-base-details): do not keep selected invalid tab

* fix(pod-metrics): add new badge

* Revert "fix(pod-metrics): add new badge"

This reverts commit 09b8624f52.

* chore(k8s-base-details): mark tab as possible null
2026-07-16 03:30:33 +00:00
Srikanth Chekuri
9a26998d18 chore: add integration tests for metrics under reduction - query part (#11979)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore: add integration tests for metrics under reduction - query part

* chore: add to ci

* chore: add todos

* chore: update tests

* chore: trigger build

* chore: address review comments

* chore: undo the changes to tests/fixtures/http.py

* chore: remove __normalized and add fast path

* chore: fix unit tests

* chore: update test

* chore: sunset 25.5.6, update migrator version, and remove version arg

* chore: also update devenv

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 21:27:39 +00:00
Vinicius Lourenço
fbf2e62044 feat(infrastructure-monitoring-v2): add onboarding checks (#12119)
* feat(infrastructure-monitoring): add onboarding checks

* refactor(infrastructure-monitoring): use error content to show error message

* test(infrastructure-monitoring): add tests for onboarding checks
2026-07-15 19:47:30 +00:00
Tushar Vats
62c44f5eac chore: pass org id for flagger (#12126) 2026-07-15 19:47:24 +00:00
Vinicius Lourenço
65fde71b72 feat(infrastructure-monitoring-v2): add counts cards (#12120)
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(entity-count): add base structure to show count

* feat(entity-count): add count for clusters

* feat(entity-count): add count for namespaces

* chore(infra-monitoring): move component to be under components folder

* fix(pr): address comments
2026-07-15 13:39:48 +00:00
Vinicius Lourenço
7f5f63b20a feat(infra-monitoring): add docs for every chart (#12037)
* feat(infra-monitoring): add docs for every chart

* fix(docs): add missing docs for each column/chart

* fix(infra-monitoring): keep referer

---------

Co-authored-by: Nikhil Mantri <nikhil.mantri1999@gmail.com>
2026-07-15 13:25:53 +00:00
Tushar Vats
63cfbe8bfb fix: convert key not found to warnings for traces (#12091) 2026-07-15 12:33:47 +00:00
Srikanth Chekuri
527cc5b002 Merge branch 'main' into nv/dashboard-migration 2026-07-15 16:50:24 +05:30
Aditya Singh
0c28b8b251 feat: scalar/table serializers for client-side export (3) (#12070)
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
* refactor(data-export): extract the withUnit header helper

Both the timeseries and the upcoming table serializers append units to headers (and skip display-only ids like 'short'/'none') — move the helper to its own module.

* feat(data-export): add generic table-model serializer

exportTableData serializes any prepared antd-style table model ({name, key, isValueColumn} columns + record rows) into a SerializedTable — raw values in display order, units on value columns only, blanks for missing cells. Surface-agnostic: QueryTable-based tables, dashboard tables and plain antd tables all adapt in a line or two.

* feat(data-export): add scalar queryRange serializer

exportScalarData takes the queryRange response object (formatForWeb webTables payload) + the builder query and serializes via createTableColumnsFromQuery — the exact preparer QueryTable renders from — so exports inherit the on-screen merge, naming and column order 1:1.
2026-07-15 05:37:15 +00:00
Aditya Singh
10a0d262d9 feat: add download option to trace view (#12116) 2026-07-15 05:36:59 +00:00
Vinicius Lourenço
334155a226 fix(infra-monitoring-v2): ensure workloads has filters by cluster+namespace (#12076)
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(tanstack): add support for itemKey be object

* feat(infra-monitoring): add base structure for multiple selected items

* refactor(infra-monitoring): add cluster for namespaces, cluster/namespace for volumes + update other entities

* refactor(infra-monitoring): add cluster + namespace for statefulsets

* refactor(infra-monitoring): add cluster + namespace for jobs

* refactor(infra-monitoring): add cluster + namespace for deployments

* refactor(infra-monitoring): add cluster + namespace for daemonsets

* test(infra-monitoring): add new tests for changes of k8s base list

* fix(pr): address comments
2026-07-14 18:24:42 +00:00
Pandey
42de840534 fix(rules): scope alert rule store operations by org (#12117)
* fix(rules): scope alert rule store operations by org

The rule store predicates filtered on the rule id only, so on multi-org
deployments (Cloud/EE/multi-org self-hosted, where orgs share the
instance via the noop sharder) an authenticated user could read, edit or
delete another org's alert rules by supplying the target rule UUID.
ViewAccess/EditAccess only check the caller's own-org role, never the
resource's org, so nothing enforced tenant isolation on the rule itself.

Enforce org scoping at the store layer, which is the durable fix:

- GetStoredRule and DeleteRule now take the caller's orgID and add
  `org_id = ?` to their predicates.
- EditRule adds `org_id = ?` (from the model, which is now always
  sourced from an org-scoped read).
- The manager passes claims.OrgID on every by-id path; GetRule now
  derives the org from claims (it previously fetched by id alone).

Cross-org ids now resolve to NotFound instead of leaking or mutating
another org's rule. Single-org OSS instances are unaffected.

CWE-639 (authorization bypass through user-controlled key).

* refactor(rules): derive claims org id with valuer.MustNewUUID

Claims are pre-validated by the auth middleware, so the NewUUID error
branch is dead code. Follow the handler convention (docs/contributing/
go/handler.md) and use the Must constructor on the by-id rule paths.
2026-07-14 18:00:09 +00:00
Naman Verma
dbbaa4d037 chore: drop invalid formulas 2026-07-14 23:10:06 +05:30
Naman Verma
e818e667a2 chore: drop trace operators with no expression 2026-07-14 22:10:40 +05:30
Nityananda Gohain
891106d1ca fix: set correct openapi response struct for span mapper list (#12094)
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: set correct opapi response model for span mapper list

* fix: change group_id to groupId in response

* fix: format properly

* fix: update fixtures
2026-07-14 15:34:52 +00:00
Naman Verma
e390eef74b chore: remove bad aggregations that accompany valid ones 2026-07-14 18:45:38 +05:30
Naman Verma
707f01bd9c chore: support non-dsl search in dashboards v2 list apis (#12051)
* chore: support non-dsl search in dashboards v2 list apis

* chore: move free text compilation to visitor

* chore: shorten comments

* fix: support both plain text and dsl together

* fix: ensure empty description dashboards are not excluded in NOT checks
2026-07-14 13:02:58 +00:00
Vinicius Lourenço
3cd0c21703 feat(daemonsets): add ready/misscheduled columns (#12102) 2026-07-14 12:33:21 +00:00
Naman Verma
a6490661d1 chore: add a.count to malformed order by keys list 2026-07-14 17:53:11 +05:30
Abhi kumar
181feb3eb9 fix(dashboard-v2): only refetch on-screen panels on time change/auto-refresh (#12113)
Panel visibility was latched (isObserverOnce), so once every panel had
scrolled into view they all stayed query-enabled. A time change or
auto-refresh re-keys every panel's query, refetching all of them at once.

Track the live viewport instead so off-screen panels stay query-disabled
and skip the refetch; they fetch lazily when scrolled back in. Add
staleTime: Infinity so re-entering an unchanged window serves cache rather
than thrash-refetching (key changes and manual refetch still run).
2026-07-14 12:23:04 +00:00
Naman Verma
e8883c4f65 chore: add wdiget id type check in layout migration 2026-07-14 17:34:14 +05:30
Naman Verma
aed3d096f0 chore: skip variables in wrong type 2026-07-14 17:18:45 +05:30
Naman Verma
bf35d12398 chore: add functional unique index for tags (#12029)
* feat: add functional unique index

* chore: add tag migration

* chore: add failing scratch test (to discuss)

* chore: fetch expressions as well in getindices call

* fix: remove tag unique index (will be added in a separate PR)

* fix: remove tag unique index (will be added in a separate PR)

* chore: remove temporary tests

* chore: add functional unique index for tags

* fix: go lint fix

* fix: go lint fix

* chore: better comment for unique index

* test: add test for case insensitive expression equality

* test: add equality test for columns with quotes and capital letters

* test: add test for quoted columns

* chore: add separate type for unique indices with expressions

* fix: update migration file to use correct index

* test: add test for postgres provider

* test: add test for postgres provider

* fix: remove round trip tests
2026-07-14 11:08:59 +00:00
Naman Verma
5ffe4ec1f8 chore: remove invalid functions 2026-07-14 16:16:16 +05:30
Abhi kumar
1a1f7b2d78 fix(dashboard-v2): bug bash fixes IV (#12056)
* fix(context-menu): keep overlay clickable when opened inside a modal

Portal the popover to body and set pointer-events:auto so a modal's dialog/backdrop can't trap it.

* fix(dashboard-v2): center scrolled-to panels and sections

Default the scroll block to 'center' so a targeted panel lands mid-viewport.

* fix(dashboard-v2): make the view modal refresh button secondary

Use outlined/secondary so refresh doesn't compete with the primary action.

* fix(dashboard-v2): fetch only on-screen panels on dashboard load

RGL unfolds panels from the top-left origin on mount, briefly piling them into the viewport.

The latched observer then marked all visible; suppress the item transition for the first frame.

* chore: readded entry animation
2026-07-14 10:43:14 +00:00
Naman Verma
3e095b710b chore: restore function args malformed by v4->v5 migration 2026-07-14 15:37:13 +05:30
dependabot[bot]
a2778aa45f chore(deps): bump golang.org/x/net from 0.54.0 to 0.55.0 (#11965)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.54.0 to 0.55.0.
- [Commits](https://github.com/golang/net/compare/v0.54.0...v0.55.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.55.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-14 10:01:13 +00:00
Naman Verma
c11061b0ae chore: remove limits that cross max limit 2026-07-14 14:46:21 +05:30
Naman Verma
c413d17594 chore: remove repeat occurences of widgets in layouts 2026-07-14 14:31:44 +05:30
Srikanth Chekuri
16c1fce326 fix(ruletypes): address rule read round-trip gaps (#12110)
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-14 08:36:30 +00:00
Gaurav Tewari
06b762b7ef fix(llm o11y): update unpriced models tab test case (#12108)
* fix: update unpriced models tab test case

* chore: update ci timming

* chore: update comment

* chore: remove comment

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-07-14 08:22:03 +00:00
Naman Verma
9f8b1be83b chore: handle misformed threshold operators 2026-07-14 13:19:31 +05:30
Aditya Singh
466edf1f1c feat: timeseries export in logs, traces and metrics explorers (2) (#12055)
* feat(data-export): surface raw V5 response and legend map from GetMetricQueryRange

Keeps the pre-conversion V5 queryRange response and the per-query legend map on the returned MetricQueryRangeSuccessResponse so explorer views can feed client-side export without a refetch. Additive optional fields; the legacy V3 render path is untouched. One chokepoint covers Logs, Traces and Metrics.

* fix(data-export): omit display-only format ids from export headers

'short'/'none' are Grafana-style display formats, not physical units — meaningful on a chart axis but misleading as 'value (short)' in a CSV header. Real units (ms, bytes, ...) are unchanged.

* feat(data-export): timeseries export menu and opt-in TimeSeriesView header

New TimeseriesExportMenu (csv/jsonl, LONG shape default; same look as DownloadOptionsMenu, which stays raw/list-only) rendered from a new opt-in TimeSeriesView header row: unit selector left (onYAxisUnitChange — rendering only, unit state stays with each view), download right (allowExport, needs the surfaced raw V5). Both default off, so views opt in explicitly. Includes gating + menu tests.

* feat(explorer): enable timeseries export in Logs, Traces and Metrics

Logs and Traces pass allowExport + onYAxisUnitChange and drop their external unit-filter headers (one header row: unit left, download right). Metrics passes allowExport only, per chart — its YAxisUnitSelector stays screen-level (persists to the metric, spans N charts). convertDataValueToMs now converts the raw V5 tree too, so Traces duration exports carry the ms values the chart shows instead of raw nanoseconds.

* refactor(data-export): use @signozhq/ui components in the export popover

Review feedback: swap antd Button/Popover/Tooltip for the design-system equivalents (radix-style Popover with controlled open, TooltipSimple, Button with native loading). Styles restructured for the DS DOM with periscope font tokens; test renders via tests/test-utils for the TooltipProvider.
2026-07-14 06:54:51 +00:00
Nikhil Mantri
849353b5c0 feat(infra-monitoring): counts query for counting related entities (#12000)
* 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

* chore: added function

* chore: added struct changes

* chore: added count attr keys

* chore: wired counts to the response fields

* chore: regenerated API spec

* chore: merged main, resolved conflicts

* chore: nodes count surfacing

* chore: integration tests added

* fix: use tuple mapping to uniquely identify attrs:

* chore: integration tests update
2026-07-14 06:38:27 +00:00
Naman Verma
f0d54cedd4 chore: fill valid span gaps 2026-07-14 11:53:06 +05:30
Naman Verma
be56b929b4 chore: replace faulty order by keys with aggregation expression 2026-07-14 11:52:43 +05:30
Naman Verma
20019d835c chore: add more panel id sanitization 2026-07-14 11:05:51 +05:30
Naman Verma
c49396944b chore: replace em dash with hyphen in widget id 2026-07-14 11:01:46 +05:30
Ashwin Bhatkal
8e4d7c68b3 feat(dashboard-v2): query-first DSL filter for the dashboards list (#12046)
* feat(dashboard-v2): stage-aware DSL autocomplete engine for the list filter

* feat(dashboard-v2): single query-string list-filter state with reflect/splice

* feat(dashboard-v2): drive saved/built-in views off the query string

* feat(dashboard-v2): CodeMirror query box with DSL autocomplete popup

* feat(dashboard-v2): antd Created-by/Updated filter selects, drop the Tags dropdown

* feat(dashboard-v2): wire the DSL filter into the dashboards list (sticky header, pagination)

* fix: small css elevation

* refactor(dashboard-v2): reuse shared operator constants in the DSL grammar

Build STRING_OPS / OPERATOR_MATRIX / VALUELESS_OPERATORS from the shared OPERATORS + negateOperator (constants/antlrQueryConstants) instead of hardcoded strings; drop a stale lint-disable in FilterChips. Addresses review comments.

* fix(dashboard-v2): wrap IN/NOT IN autocomplete values in a bracketed list

Picking a value for an IN/NOT IN clause in the DSL search box inserted a
bare literal ('x') instead of a list (['x']). The value stage now wraps a
fresh pick in a closed bracketed list, appends subsequent picks inside the
existing brackets (deduping already-entered values), and keeps the caret
inside the list so multi-select continues.

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-07-14 05:29:21 +00:00
Naman Verma
39d6880cf7 chore: add more error notes 2026-07-14 10:44:47 +05:30
Naman Verma
3f20c04c97 chore: remove expressions from metric aggregations 2026-07-14 10:44:33 +05:30
Gaurav Tewari
2704317c6a feat(llm-pricing): add Unpriced Models tab with billing-model mapping [6/6] (#11813)
* 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

* feat: llm pricing unpriced model

* 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

* feat: add search to search model name

* 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

* feat(llm-pricing): batch-map unpriced models via top Save + confirm drawer

Replace the per-row "Map to model" button + inline confirm with a single
top-level Save button that opens a confirm drawer listing every pending
mapping in a table (model -> billing model + pricing).

- Batch the save: group selections by target rule so multiple models mapped
  to the same rule append their patterns in one PUT instead of clobbering
- Capped, skeleton-loaded unpriced table (max-height) to avoid layout shift,
  matching the model-costs table
- Restore the dropped .modelCellName style; drop dead inline-confirm styles
- Tests: batch/clobber hook test, tab integration flow, and fix the stale
  'unpriced tab disabled' assertion (the tab is now enabled)

* chore: remove worktree

* chore: revert env.ts

* chore: update llm pricing

* chore: update ui

* chore: sync unpriced modal

* chore(llm-pricing): address Copilot review on unpriced confirm table

- formatSpanCount: use Intl.NumberFormat compact so 999,999 renders as
  "1M" (not "1000.0K") and trailing ".0" is dropped
- confirm drawer extra-buckets container: non-wrapping inline-flex so
  buckets stay on one line at the fixed row height
- soften the misleading "renders identically" comment (extra buckets are
  plain inline text here, not the model-costs Badge chips)

* chore: flex wrap

* chore: self review changes

* feat: add support to remove model

* chore: update ux

* chore: update test cases

* chore: remove unused test cases

* chore: update tests

* chore: remove extra integration

* chore: refactor

* refactor: imports

* refactor: un priced modal

* chore: update unpriced modal

* chore: update map billing mode select

* refactor: code

* refactor: remove comments

* chore: make bg great again

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-07-14 04:43:28 +00:00
Vinicius Lourenço
9476d8f70f feat(infrastructure-monitoring-v2): add button to open chart at metrics explorer (#12107)
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-13 19:29:46 +00:00
Vinicius Lourenço
69a240fdb2 chore(frontend): bump deps (#12052)
* chore(frontend): bump deps

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* chore(pnpm-lock): missing update

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-13 17:14:22 +00:00
Naman Verma
70e556bd38 chore: normalize group by 2026-07-13 19:58:54 +05:30
Naman Verma
0a59a8eb04 chore: make migration drop widgets that have no aggregations for metrics 2026-07-13 19:34:24 +05:30
Naman Verma
6fee60833a Merge branch 'main' into nv/dashboard-migration 2026-07-13 19:12:01 +05:30
Vinicius Lourenço
6975c4d90c chore(codeowners): update infra monitoring file owner (#12073)
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-13 11:47:09 +00:00
Vikrant Gupta
9bfa6e6d59 feat(authz): store role transaction groups as JSON document (#12028)
* feat(authz): store role transaction groups as document of record

Persist a role's transaction groups as JSON on the role row so the role
details page is reconstructed deterministically from SQL instead of being
rebuilt from OpenFGA tuples (which will soon carry opaque hashed telemetry
selectors):

- authtypes: TransactionGroups gains Value/Scan (validated via
  NewTransactionGroups) and MarshalJSON (nil renders as []); NewRole takes
  transactionGroups; NewManagedRoles fills managed docs from the registry;
  RoleWithTransactionGroups removed - Role carries the wire field and the
  AuthZ interface, handler, and OpenAPI responses use *Role; GettableRole
  (without transactionGroups) is the list response
- sqlmigration 099: add role.transaction_groups, backfill custom roles from
  their permission tuples (dual dialect) and managed roles from the registry
- sqlmigration 059: pin insert columns so the live Role model addition does
  not break fresh installs (059 runs before 099)
- ee provider: writes persist the doc alongside FGA tuples (FGA first, SQL
  second, as before); GetWithTransactionGroups reads the doc; the per-type
  ReadTuples fan-out (readAllTuplesForRole) is removed
- audit middleware: log and skip resolved resources carrying a resolution
  error
- frontend: regenerated OpenAPI spec and API types; role list consumers
  retyped to GettableRole; role GET keeps transactionGroups

* fix(authz): reconcile role tuples from openfga state, decouple migration 059

- Update and Delete derive their diff/deletion base from the tuples openfga
  actually holds for the role (readAllTuplesForRole) instead of the stored
  JSON record, so every mutation sweeps drift and residue; the record stays
  a display-only artifact written after the tuple write
- ReadTuples restored on the AuthZ interface with plain passthroughs in both
  providers and the ee server
- TransactionGroups.Value marshals unconditionally (nil renders as [] via
  MarshalJSON) instead of returning a nil driver.Value
- migration 059 uses a migration-local role struct and constructor so live
  Role model changes cannot alter its insert; migration 099 drops the manual
  column-exists guard (AddColumn emits IF NOT EXISTS)

* refactor(authz): split role into domain Role and StorableRole

Replace the Scan/Value/MarshalJSON codecs on TransactionGroups with the
storable pattern: StorableRole is the bun model carrying transaction groups
as raw JSON text, Role is the pure domain/wire type, and
NewStorableRoleFromRole/NewRoleFromStorableRole convert at the store
boundary (nil groups persist as [], reads parse through the validating
constructor). RoleStore and sqlauthzstore speak StorableRole; both
providers convert; handlers and the wire contract are unchanged.

* revert(authz): restore TransactionGroups codecs over the storable split

Role is a bun relation target (UserRole.Role, ServiceAccountRole.Role), so
splitting it into StorableRole/Role cascaded: relations must point at the
bun model, which broke the user-roles join and leaked the storable shape
into user and service account responses. Keep the single Role model with
Scan/Value/MarshalJSON on TransactionGroups; the storable split fits leaf
models only.

This reverts commit 73aa7d32b1 and keeps transaction_test.go deleted.

* refactor(authz): use bun models in migration 099, wire oss role get

- migration 099 follows the migration-local row struct pattern: bun
  NewSelect/NewUpdate for the role table reads and backfill writes; the
  openfga store and tuple lookups stay raw like 081/083
- oss provider Get reads the role from the store instead of returning
  unsupported

* refactor(authz): org-scoped backfill in migration 099, empty groups on null scan

- migration 099 iterates organizations: per org it backfills custom roles
  from their permission tuples (readRoleTuples helper) and managed roles
  from the registry (JSON precomputed per role name)
- TransactionGroups scans SQL NULL as an empty slice so the api always
  renders transactionGroups as []; nullzero keeps writing NULL for nil

* fix(authz): pass unique constraints to add column in migration 099
2026-07-13 11:42:42 +00:00
Naman Verma
b81bb42be6 chore: drop legacy filter fields that ui also ignores 2026-07-11 19:06:25 +05:30
Naman Verma
d8717ee466 chore: assign query names to queries if missing 2026-07-11 18:06:33 +05:30
Naman Verma
d0d81b778f chore: assign missing formula names like ui does 2026-07-11 17:49:41 +05:30
Naman Verma
b6a0431049 chore: remove widgets that have a single query that has no metric name 2026-07-11 17:10:16 +05:30
Naman Verma
54560b6f63 chore: skip over unreferenced widgets 2026-07-11 16:43:22 +05:30
Naman Verma
41a68cd17e fix: add fixes based on backup migration testing 2026-07-11 15:48:56 +05:30
Naman Verma
af96aef5e1 Merge branch 'main' into nv/dashboard-migration 2026-07-11 15:00:49 +05:30
Naman Verma
171f95ac25 Merge branch 'main' into nv/dashboard-migration 2026-07-10 15:07:21 +05:30
Naman Verma
93387b38d2 fix: add empty list tolerance to v1 variables 2026-07-09 09:46:24 +05:30
Srikanth Chekuri
d4384eba1b Merge branch 'main' into nv/dashboard-migration 2026-07-08 20:04:56 +05:30
Naman Verma
6151467b3e fix: add default aggregation count() if none is present in logs/traces 2026-07-07 11:18:49 +05:30
Naman Verma
daa999d1d6 Merge branch 'main' into nv/dashboard-migration 2026-07-07 10:10:53 +05:30
Naman Verma
b400846193 fix: ditch dynamic vars with no attr, rearrange overlapping layouts 2026-07-04 01:43:13 +05:30
Naman Verma
f5220d078f chore: add explanation comment for continuing on empty id widget 2026-07-04 00:16:12 +05:30
Naman Verma
ab750ffffd chore: better comments and names in layout migrator 2026-07-04 00:06:04 +05:30
Naman Verma
4a2b508636 chore: improve variable names 2026-07-03 23:52:07 +05:30
Naman Verma
33795999cf fix: match shape safe version to existing version 2026-07-03 23:38:59 +05:30
Naman Verma
9aac37eb94 fix: remove unused method 2026-07-03 22:48:31 +05:30
Naman Verma
12642e27da fix: validate dashboard spec after migration 2026-07-03 21:21:51 +05:30
Naman Verma
82f8bf88fc fix: add nil check on limit value when normalizing page size 2026-07-03 20:23:01 +05:30
Naman Verma
3befc9e8c6 fix: make none the default fill mode during migration 2026-07-03 20:22:36 +05:30
Naman Verma
ee5b85f7a4 revert: remove md file 2026-07-03 20:22:10 +05:30
Naman Verma
a05164f225 fix: fix go lint issue 2026-07-03 17:56:33 +05:30
Naman Verma
35e4013826 chore: reuse transition package (adds dependency, need to see what to do) 2026-07-03 17:40:50 +05:30
Naman Verma
5884c6aa90 Merge branch 'main' into nv/dashboard-migration 2026-07-03 16:38:22 +05:30
Naman Verma
533a430714 fix: add more malformation handling 2026-07-03 15:25:09 +05:30
Naman Verma
ba6af34714 fix: add more malformed query handling 2026-07-02 13:49:49 +05:30
Naman Verma
851c7b0ad7 chore: add temporary doc for other malformation handlers needed 2026-07-02 13:19:51 +05:30
Naman Verma
ef5a67495c fix: normalize telemetry field keys in query 2026-07-02 12:58:14 +05:30
Naman Verma
9f540ca84b fix: remove metric aggregation from non metric queries (malformed json) 2026-07-02 12:25:57 +05:30
Naman Verma
40a6b22aed fix: normalize having array from v4 schema 2026-07-02 11:38:37 +05:30
Naman Verma
6f16416f27 fix: add better error note down for failed conversions 2026-07-02 11:10:02 +05:30
Naman Verma
f8aa1c1c34 Merge branch 'main' into nv/dashboard-migration 2026-07-02 10:52:00 +05:30
Naman Verma
65835394c0 Merge branch 'main' into nv/dashboard-migration 2026-06-30 18:39:53 +05:30
Naman Verma
f132b7e53a fix: handle list of widget IDs in panel maps 2026-06-29 23:51:55 +05:30
Naman Verma
d4ae156dc4 fix: make threshold value non-nullable 2026-06-29 23:18:17 +05:30
Naman Verma
d6bdf9c2b2 fix: catch error from decodeTelemetryFields 2026-06-29 23:02:40 +05:30
Naman Verma
7ea654f1aa test: fix unit tests 2026-06-29 22:57:40 +05:30
Naman Verma
3fd7d013a1 fix: adjust to schema changes in variables 2026-06-29 22:56:36 +05:30
Naman Verma
fb921dd381 fix: read threshold value properly during migration 2026-06-29 22:56:12 +05:30
Naman Verma
58020d9e00 fix: note error in query parsing 2026-06-29 22:53:40 +05:30
Naman Verma
7a5933e822 fix: ignore react placeholders in layout 2026-06-29 22:52:48 +05:30
Naman Verma
2533683de6 fix: allow zero value for threshold values 2026-06-29 22:51:56 +05:30
Naman Verma
2670d53170 Merge branch 'main' into nv/dashboard-migration 2026-06-29 20:28:39 +05:30
Naman Verma
8943a9454b Merge branch 'main' into nv/dashboard-migration 2026-06-26 02:15:22 +05:30
Naman Verma
9a7ed5b711 feat: note down all errors in migration 2026-06-26 02:07:06 +05:30
Naman Verma
2d75e3d32d chore: separate error type for migration 2026-06-26 01:07:59 +05:30
Naman Verma
1d6eabf927 chore: remove spec md file 2026-06-25 22:37:52 +05:30
Naman Verma
082d7b1b77 test: fix ut to check for v2 internal name 2026-06-25 22:34:39 +05:30
Naman Verma
5019dee2d7 Merge branch 'main' into nv/dashboard-migration 2026-06-25 22:30:55 +05:30
Naman Verma
216de973fb fix: remove nil nil return 2026-06-25 03:07:58 +05:30
Naman Verma
18c0eec5e2 chore: catch typecast errors 2026-06-19 15:49:01 +05:30
Naman Verma
2ccdeb3631 chore: add a catch all panic check to log migration error 2026-06-19 15:15:17 +05:30
Naman Verma
ad12e50bbc fix: extract row and widget positions to build expanded sections 2026-06-19 15:00:32 +05:30
Naman Verma
e247bf3864 fix: sanitize tags instead of throwing error 2026-06-19 14:17:36 +05:30
Naman Verma
f4651ea134 fix: match with lower case signal for variables 2026-06-19 12:17:42 +05:30
Naman Verma
d449a2dbf2 fix: generate internal name from title 2026-06-19 11:24:40 +05:30
Naman Verma
d4b9f91062 Merge branch 'main' into nv/dashboard-migration 2026-06-18 12:28:22 +05:30
Naman Verma
530710b7bc Merge branch 'nv/dashboard-migration' of https://github.com/SigNoz/signoz into nv/dashboard-migration 2026-06-17 12:42:24 +05:30
Naman Verma
4fb5eec08d fix: move WrapInV5Envelope to types package 2026-06-17 12:42:20 +05:30
Naman Verma
f889d36f0f Merge branch 'main' into nv/dashboard-migration 2026-06-17 12:32:08 +05:30
Naman Verma
db12d44523 Merge branch 'main' into nv/dashboard-migration 2026-06-17 07:30:34 +05:30
Naman Verma
86fc0e81ba chore: add migration script from current to perses dashboard 2026-06-14 22:58:08 +05:30
1023 changed files with 63193 additions and 18921 deletions

View File

@@ -1,6 +1,6 @@
services:
init-clickhouse:
image: clickhouse/clickhouse-server:25.5.6
image: clickhouse/clickhouse-server:25.12.5
container_name: init-clickhouse
command:
- bash
@@ -18,7 +18,7 @@ services:
volumes:
- ${PWD}/fs/tmp/var/lib/clickhouse/user_scripts/:/var/lib/clickhouse/user_scripts/
clickhouse:
image: clickhouse/clickhouse-server:25.5.6
image: clickhouse/clickhouse-server:25.12.5
container_name: clickhouse
volumes:
- ${PWD}/fs/etc/clickhouse-server/config.d/config.xml:/etc/clickhouse-server/config.d/config.xml
@@ -67,7 +67,7 @@ services:
timeout: 5s
retries: 3
telemetrystore-migrator:
image: signoz/signoz-otel-collector:v0.142.0
image: signoz/signoz-otel-collector:v0.144.6
container_name: telemetrystore-migrator
environment:
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000

2
.github/CODEOWNERS vendored
View File

@@ -189,7 +189,9 @@ go.mod @therealpandey
## Infrastructure Monitoring
/frontend/src/pages/InfrastructureMonitoring/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringHosts/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringHostsV2/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringK8s/ @SigNoz/pulse-frontend
/frontend/src/container/InfraMonitoringK8sV2/ @SigNoz/pulse-frontend
## Alerts
/frontend/src/pages/AlertList/ @SigNoz/pulse-frontend

View File

@@ -54,22 +54,25 @@ jobs:
- querierscalar
- queriercommon
- rawexportdata
- querierauthz
- role
- rootuser
- serviceaccount
- spanmapper
- querier_json_body
- querier_skip_resource_fingerprint
- ttl
- clickhousecluster
- metricreduction
sqlstore-provider:
- postgres
- sqlite
sqlite-mode:
- wal
clickhouse-version:
- 25.5.6
- 25.12.5
schema-migrator-version:
- v0.144.3
- v0.144.6
postgres-version:
- 15
if: |

View File

@@ -9,6 +9,11 @@ global:
# the path component (e.g. /signoz in https://example.com/signoz) is used
# as the base path for all HTTP routes (both API and web frontend).
external_url: <unset>
# origins (scheme://host[:port], no path) allowed as login redirect targets. include
# the origin the signoz ui is served on. when not configured, redirect targets are
# not validated.
# allowed_origins:
# - https://signoz.example.com
# the url where the SigNoz backend receives telemetry data (traces, metrics, logs) from instrumented applications.
ingestion_url: <unset>
# the url of the SigNoz MCP server. when unset, the MCP settings page is hidden in the frontend.

File diff suppressed because it is too large Load Diff

View File

@@ -119,10 +119,6 @@ func (provider *provider) CheckTransactions(ctx context.Context, subject string,
return results, nil
}
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType coretypes.Type) ([]*coretypes.Object, error) {
return provider.openfgaServer.ListObjects(ctx, subject, relation, objectType)
}
func (provider *provider) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {
return provider.openfgaServer.Write(ctx, additions, deletions)
}
@@ -131,10 +127,6 @@ func (provider *provider) ReadTuples(ctx context.Context, tupleKey *openfgav1.Re
return provider.openfgaServer.ReadTuples(ctx, tupleKey)
}
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
return provider.pkgAuthzService.Get(ctx, orgID, id)
}
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.Role, error) {
return provider.pkgAuthzService.GetByOrgIDAndName(ctx, orgID, name)
}
@@ -183,7 +175,7 @@ func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context,
return provider.Write(ctx, tuples, nil)
}
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.RoleWithTransactionGroups) error {
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -208,86 +200,42 @@ func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *a
return err
}
if err := provider.store.Create(ctx, role.Role); err != nil {
return err
}
return nil
return provider.store.Create(ctx, role)
}
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) (*authtypes.Role, error) {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
existingRole, err := provider.store.GetByOrgIDAndName(ctx, role.OrgID, role.Name)
if err != nil {
if !errors.Ast(err, errors.TypeNotFound) {
return nil, err
}
}
if existingRole != nil {
return existingRole, nil
}
err = provider.store.Create(ctx, role)
if err != nil {
return nil, err
}
return role, nil
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
return provider.store.Get(ctx, orgID, id)
}
func (provider *provider) GetWithTransactionGroups(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.RoleWithTransactionGroups, error) {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
role, err := provider.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
tuples, err := provider.readAllTuplesForRole(ctx, role.Name, orgID)
if err != nil {
return nil, err
}
transactionGroups := authtypes.MustNewTransactionGroupsFromTuples(tuples)
return authtypes.MakeRoleWithTransactionGroups(role, transactionGroups), nil
}
func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updatedRole *authtypes.RoleWithTransactionGroups) error {
func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updatedRole *authtypes.Role) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
existingRole, err := provider.GetWithTransactionGroups(ctx, orgID, updatedRole.ID)
existingRole, err := provider.Get(ctx, orgID, updatedRole.ID)
if err != nil {
return err
}
additions, deletions := existingRole.TransactionGroups.Diff(updatedRole.TransactionGroups)
additionTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, additions)
existingTuples, err := provider.readAllTuplesForRole(ctx, existingRole.Name, orgID)
if err != nil {
return err
}
deletionTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, deletions)
desiredTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, updatedRole.TransactionGroups)
if err != nil {
return err
}
additionTuples, deletionTuples := authtypes.DiffTuples(existingTuples, desiredTuples)
err = provider.Write(ctx, additionTuples, deletionTuples)
if err != nil {
return err
}
return provider.store.Update(ctx, orgID, updatedRole.Role)
return provider.store.Update(ctx, orgID, updatedRole)
}
func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
@@ -296,7 +244,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
role, err := provider.GetWithTransactionGroups(ctx, orgID, id)
role, err := provider.Get(ctx, orgID, id)
if err != nil {
return err
}
@@ -312,7 +260,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
}
}
tuples, err := authtypes.NewTuplesFromTransactionGroups(role.Name, orgID, role.TransactionGroups)
tuples, err := provider.readAllTuplesForRole(ctx, role.Name, orgID)
if err != nil {
return err
}
@@ -324,6 +272,24 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
return provider.store.Delete(ctx, orgID, id)
}
func (provider *provider) readAllTuplesForRole(ctx context.Context, roleName string, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
subject := authtypes.MustNewSubject(coretypes.NewResourceRole(), roleName, orgID, &coretypes.VerbAssignee)
tuples := make([]*openfgav1.TupleKey, 0)
for _, objectType := range provider.registry.Types() {
typeTuples, err := provider.openfgaServer.ReadTuples(ctx, &openfgav1.ReadRequestTupleKey{
User: subject,
Object: objectType.StringValue() + ":",
})
if err != nil {
return nil, err
}
tuples = append(tuples, typeTuples...)
}
return tuples, nil
}
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) []*openfgav1.TupleKey {
tuples := []*openfgav1.TupleKey{}
@@ -375,21 +341,3 @@ func (provider *provider) getManagedRoleTransactionTuples(orgID valuer.UUID) []*
return tuples
}
func (provider *provider) readAllTuplesForRole(ctx context.Context, roleName string, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
subject := authtypes.MustNewSubject(coretypes.NewResourceRole(), roleName, orgID, &coretypes.VerbAssignee)
tuples := make([]*openfgav1.TupleKey, 0)
for _, objectType := range provider.registry.Types() {
typeTuples, err := provider.ReadTuples(ctx, &openfgav1.ReadRequestTupleKey{
User: subject,
Object: objectType.StringValue() + ":",
})
if err != nil {
return nil, err
}
tuples = append(tuples, typeTuples...)
}
return tuples, nil
}

View File

@@ -105,10 +105,6 @@ func (server *Server) BatchCheck(ctx context.Context, tupleReq map[string]*openf
return server.pkgAuthzService.BatchCheck(ctx, tupleReq)
}
func (server *Server) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType coretypes.Type) ([]*coretypes.Object, error) {
return server.pkgAuthzService.ListObjects(ctx, subject, relation, objectType)
}
func (server *Server) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {
return server.pkgAuthzService.Write(ctx, additions, deletions)
}

View File

@@ -240,17 +240,17 @@ func TestManager_TestNotification_SendUnmatched_PromRule(t *testing.T) {
mock := mockStore.Mock()
// Mock the fingerprint query (for Prometheus label matching)
// args: $1=metric_name (the __name__ matcher maps onto the column)
mock.ExpectQuery("SELECT fingerprint, any").
WithArgs("test_metric", "__name__", "test_metric").
WithArgs("test_metric").
WillReturnRows(fingerprintRows)
// Mock the samples query (for Prometheus metric data)
// args: metric_name IN (discovered names), subquery metric_name, start, end
mock.ExpectQuery("SELECT metric_name, fingerprint, unix_milli").
WithArgs(
"test_metric",
"test_metric",
"__name__",
"test_metric",
queryStart,
queryEnd,
).

View File

@@ -0,0 +1,11 @@
// Test stub for the Vite-only icon glob module: `import.meta.glob` can't be
// parsed by jest, so every test that transitively imports it is redirected here
// (see moduleNameMapper in jest.config.ts). Provides a minimal name → URL map so
// the resolver stays deterministic under test.
export const ICON_URLS: Record<string, string> = {
'eight-ball': 'mock-eight-ball-url',
};
export const LOGO_URLS: Record<string, string> = {
'aws-dark': 'mock-logo-url',
};

View File

@@ -1,29 +0,0 @@
import { PropsWithChildren } from 'react';
type CommonProps = PropsWithChildren<{
className?: string;
minSize?: number;
maxSize?: number;
defaultSize?: number;
direction?: 'horizontal' | 'vertical';
autoSaveId?: string;
withHandle?: boolean;
}>;
export function ResizablePanelGroup({
children,
className,
}: CommonProps): JSX.Element {
return <div className={className}>{children}</div>;
}
export function ResizablePanel({
children,
className,
}: CommonProps): JSX.Element {
return <div className={className}>{children}</div>;
}
export function ResizableHandle({ className }: CommonProps): JSX.Element {
return <div className={className} />;
}

View File

@@ -15,13 +15,15 @@ const config: Config.InitialOptions = {
moduleNameMapper: {
'\\.(png|jpg|jpeg|gif|svg|webp|avif|ico|bmp|tiff)$':
'<rootDir>/__mocks__/fileMock.ts',
// The icon glob module uses `import.meta.glob` (Vite-only); jest can't parse
// it, so redirect any import of it to a stub.
'(^|/)iconAssets$': '<rootDir>/__mocks__/iconAssetsMock.ts',
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss)$': '<rootDir>/__mocks__/cssMock.ts',
'\\.module\\.mjs$': '<rootDir>/__mocks__/cssMock.ts',
'\\.md$': '<rootDir>/__mocks__/cssMock.ts',
'^uplot$': '<rootDir>/__mocks__/uplotMock.ts',
'^motion/react$': '<rootDir>/__mocks__/motionMock.tsx',
'^@signozhq/resizable$': '<rootDir>/__mocks__/resizableMock.tsx',
'^hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^src/hooks/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,
'^.*/useSafeNavigate$': USE_SAFE_NAVIGATE_MOCK_PATH,

View File

@@ -49,7 +49,6 @@
"@sentry/vite-plugin": "5.3.0",
"@signozhq/design-tokens": "2.1.4",
"@signozhq/icons": "0.4.0",
"@signozhq/resizable": "0.0.2",
"@signozhq/ui": "0.0.23",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "3.13.22",
@@ -65,7 +64,7 @@
"antd": "5.11.0",
"antd-table-saveas-excel": "2.2.1",
"antlr4": "4.13.2",
"axios": "1.16.0",
"axios": "1.18.0",
"babel-jest": "^29.6.4",
"chart.js": "3.9.1",
"chartjs-adapter-date-fns": "^2.0.0",
@@ -75,7 +74,7 @@
"crypto-js": "4.2.0",
"d3-hierarchy": "3.1.2",
"dayjs": "^1.10.7",
"dompurify": "3.4.11",
"dompurify": "3.4.12",
"event-source-polyfill": "1.0.31",
"eventemitter3": "5.0.1",
"history": "4.10.1",
@@ -90,7 +89,7 @@
"lodash-es": "^4.17.21",
"motion": "12.4.13",
"nuqs": "2.8.8",
"overlayscrollbars": "^2.8.1",
"overlayscrollbars": "^2.16.0",
"overlayscrollbars-react": "^0.5.6",
"papaparse": "5.4.1",
"posthog-js": "1.298.0",
@@ -128,7 +127,7 @@
"timestamp-nano": "^1.0.0",
"typescript": "5.9.3",
"uplot": "1.6.31",
"uuid": "^8.3.2",
"uuid": "14.0.1",
"vite": "npm:rolldown-vite@7.3.1",
"vite-plugin-html": "3.2.2",
"zod": "4.3.6",
@@ -200,9 +199,9 @@
"react-resizable": "3.0.4",
"redux-mock-store": "1.5.4",
"sass": "1.97.3",
"sharp": "0.34.5",
"sharp": "0.35.0",
"stylelint": "17.7.0",
"svgo": "4.0.1",
"svgo": "4.0.2",
"ts-jest": "29.4.9",
"typescript-plugin-css-modules": "5.2.0",
"use-sync-external-store": "1.6.0",
@@ -223,33 +222,17 @@
"stylelint"
]
},
"resolutions": {
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
"debug": "4.3.4",
"semver": "7.5.4",
"xml2js": "0.5.0",
"phin": "^3.7.1",
"body-parser": "1.20.3",
"http-proxy-middleware": "4.1.1",
"cross-spawn": "7.0.5",
"cookie": "^0.7.1",
"serialize-javascript": "6.0.2",
"prismjs": "1.30.0",
"got": "11.8.5",
"form-data": "4.0.6",
"brace-expansion": "^2.0.3",
"on-headers": "^1.1.0",
"js-cookie": "^3.0.7",
"tmp": "0.2.7",
"vite": "npm:rolldown-vite@7.3.1",
"dompurify": "3.4.11",
"js-yaml@3": "3.15.0",
"js-yaml@4": "4.2.0",
"yaml@1": "1.10.3",
"react-router@6": "6.30.4",
"markdown-it": "14.2.0",
"mdast-util-to-hast@13": "13.2.1",
"protocol-buffers-schema": "3.6.1"
"overrides": {
"@babel/core@<=7.29.0": ">=7.29.6 <8",
"@istanbuljs/load-nyc-config>js-yaml": ">=4.2.0 <5",
"cookie@<0.7.0": ">=0.7.1 <1",
"dompurify@<=3.4.10": ">=3.4.11 <4",
"esbuild@>=0.27.3 <0.28.1": ">=0.28.1 <0.29.0",
"js-cookie@<=3.0.5": ">=3.0.7 <4",
"js-yaml@>=4.0.0 <=4.1.1": ">=4.2.0 <5",
"prismjs@<1.30.0": ">=1.30.0 <2",
"react-router@>=6.7.0 <6.30.4": ">=6.30.4 <7",
"tmp@<0.2.6": ">=0.2.6 <0.3.0",
"yaml@>=1.0.0 <1.10.3": ">=1.10.3 <2"
}
}
}

2321
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,53 @@
trustPolicy: no-downgrade
blockExoticSubdeps: true
minimumReleaseAge: 2880 # 2d
minimumReleaseAgeStrict: true
minimumReleaseAgeExclude:
- '@signozhq/*'
blockExoticSubdeps: true
minimumReleaseAgeStrict: true
# Security floors for vulnerable transitive deps. Where possible, targets are
# capped to avoid crossing breaking versions (major; and minor for 0.x).
# Some entries may still force a breaking bump when no safe release exists
# within the consumers constraint—see per-entry notes.
overrides:
# via: direct devDep @babel/core ^7.22.11 (+ babel plugin peers)
# remove: bump @babel/core in package.json to ^7.29.6
'@babel/core@<=7.29.0': '>=7.29.6 <8'
# via: jest > babel-plugin-istanbul > @istanbuljs/load-nyc-config@1.1.0 (js-yaml ^3.13.1)
# remove: blocked — 1.1.0 is latest and still depends on js-yaml 3.x
'@istanbuljs/load-nyc-config>js-yaml': '>=4.2.0 <5'
# via: msw@1.3.2 (devDep) > cookie ^0.4.2
# remove: upgrade msw to >=2 (ships cookie ^1). Do NOT open the cap: cookie >=1 is
# ESM-only and breaks msw under jest's CJS sandbox (kills every test suite)
cookie@<0.7.0: '>=0.7.1 <1'
# via: direct dep dompurify 3.4.0; @grafana/data@11.6.15 (3.4.0/3.2.4 exact);
# @monaco-editor/react > monaco-editor@0.55.1 (3.2.7 exact)
# remove: bump direct dep to 3.4.11; @grafana/data (latest 13.1.0) and
# monaco-editor (latest 0.55.1) still pin vulnerable versions — blocked
dompurify@<=3.4.10: '>=3.4.11 <4'
# via: rolldown-vite@7.3.1 (esbuild ^0.27.0); orval@8.9.1 (^0.27.4); ts-jest@29.4.9 (~0.27.4)
# remove: blocked on rolldown-vite (7.3.1 is latest, still ^0.27.0);
# orval >=8.20.0 and ts-jest >=29.4.11 already fixed on their side
esbuild@>=0.27.3 <0.28.1: '>=0.28.1 <0.29.0'
# via: react-use@17.5.1 (direct, js-cookie ^2.2.1); @grafana/data > react-use@17.6.0
# remove: bump react-use to >=17.6.1 (js-cookie ^3); @grafana/data side blocked
js-cookie@<=3.0.5: '>=3.0.7 <4'
# via: @orval/core@8.9.1 (devDep, js-yaml 4.1.1 EXACT pin — not deletable);
# json-schema-to-typescript@15 > @apidevtools/json-schema-ref-parser (^4.1.0)
# remove: upgrade orval to >=8.20.0 (drops js-yaml dependency entirely)
js-yaml@>=4.0.0 <=4.1.1: '>=4.2.0 <5'
# via: react-syntax-highlighter@15.5.0 (prismjs ^1.27.0 + refractor@3 ~1.27.0 tilde-pinned)
# remove: bump react-syntax-highlighter to >=16.1.1 (prismjs ^1.30.0, refractor@5)
prismjs@<1.30.0: '>=1.30.0 <2'
# via: direct dep react-router-dom-v5-compat@6.30.3 (react-router 6.30.3 exact)
# remove: bump react-router-dom-v5-compat to 6.30.4. Do NOT open the cap:
# react-router >=7 requires React 19 and breaks the app-wide CompatRouter
react-router@>=6.7.0 <6.30.4: '>=6.30.4 <7'
# via: msw@1.3.2 (devDep) > inquirer@8 > external-editor@3.1.0 (tmp ^0.0.33)
# remove: upgrade msw to >=2 (drops the inquirer/external-editor chain)
tmp@<0.2.6: '>=0.2.6 <0.3.0'
# via: jest > babel-plugin-macros > cosmiconfig@7 (yaml ^1.10.0);
# typescript-plugin-css-modules > postcss-load-config@3 (^1.10.2)
# remove: blocked — babel-plugin-macros 3.1.0 (latest) still uses cosmiconfig@7
yaml@>=1.0.0 <1.10.3: '>=1.10.3 <2'
trustPolicy: no-downgrade
trustPolicyExclude:
- 'semver@6.3.1 || 5.7.2'

View File

@@ -62,6 +62,7 @@
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
"LLM_OBSERVABILITY_OVERVIEW": "SigNoz | LLM Observability Overview",
"LLM_OBSERVABILITY_CONFIGURATION": "SigNoz | LLM Observability Configuration"
"AI_OBSERVABILITY_OVERVIEW": "SigNoz | AI Observability Overview",
"AI_OBSERVABILITY_CONFIGURATION": "SigNoz | AI Observability Configuration",
"AI_OBSERVABILITY_ATTRIBUTE_MAPPING": "SigNoz | AI Observability Attribute Mapping"
}

View File

@@ -87,6 +87,7 @@
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
"LLM_OBSERVABILITY_OVERVIEW": "SigNoz | LLM Observability Overview",
"LLM_OBSERVABILITY_CONFIGURATION": "SigNoz | LLM Observability Configuration"
"AI_OBSERVABILITY_OVERVIEW": "SigNoz | AI Observability Overview",
"AI_OBSERVABILITY_CONFIGURATION": "SigNoz | AI Observability Configuration",
"AI_OBSERVABILITY_ATTRIBUTE_MAPPING": "SigNoz | AI Observability Attribute Mapping"
}

View File

@@ -134,8 +134,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
}
if (
(pathname.startsWith(`${ROUTES.LLM_OBSERVABILITY_BASE}/`) ||
pathname === ROUTES.LLM_OBSERVABILITY_BASE) &&
(pathname.startsWith(`${ROUTES.AI_OBSERVABILITY_BASE}/`) ||
pathname === ROUTES.AI_OBSERVABILITY_BASE) &&
!isAIObservabilityEnabled
) {
return <Redirect to={ROUTES.HOME} />;

View File

@@ -514,24 +514,24 @@ const routes: AppRoutes[] = [
isPrivate: true,
},
{
path: ROUTES.LLM_OBSERVABILITY_ATTRIBUTE_MAPPING,
path: ROUTES.AI_OBSERVABILITY_ATTRIBUTE_MAPPING,
exact: true,
component: LLMObservabilityPage,
key: 'LLM_OBSERVABILITY_ATTRIBUTE_MAPPING',
key: 'AI_OBSERVABILITY_ATTRIBUTE_MAPPING',
isPrivate: true,
},
{
path: ROUTES.LLM_OBSERVABILITY_OVERVIEW,
path: ROUTES.AI_OBSERVABILITY_OVERVIEW,
exact: true,
component: LLMObservabilityPage,
key: 'LLM_OBSERVABILITY_OVERVIEW',
key: 'AI_OBSERVABILITY_OVERVIEW',
isPrivate: true,
},
{
path: ROUTES.LLM_OBSERVABILITY_CONFIGURATION,
path: ROUTES.AI_OBSERVABILITY_CONFIGURATION,
exact: true,
component: LLMObservabilityPage,
key: 'LLM_OBSERVABILITY_CONFIGURATION',
key: 'AI_OBSERVABILITY_CONFIGURATION',
isPrivate: true,
},
];

View File

@@ -0,0 +1,20 @@
import { ENVIRONMENT } from 'constants/env';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
export function mockFieldsAPIsWithEmptyResponse(): void {
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 } }),
),
),
);
}

View File

@@ -136,7 +136,7 @@ export const invalidateGetChecks = async (
};
/**
* Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes clusters with key aggregated metrics derived by summing per-node values within the group: CPU usage, CPU allocatable, memory working set, memory allocatable. Each row also reports per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready value) and per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each pod's latest kubectl-style display status). Each cluster includes metadata attributes (k8s.cluster.name). The response type is 'list' for the default k8s.cluster.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates nodes and pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (clusterCPU, clusterCPUAllocatable, clusterMemory, clusterMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* @summary List Clusters for Infra Monitoring
*/
export const listClusters = (
@@ -219,7 +219,7 @@ export const useListClusters = <
return useMutation(getListClustersMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on), currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on), readyNodes (k8s.daemonset.ready_nodes, the number of nodes running at least one ready daemon pod) and misscheduledNodes (k8s.daemonset.misscheduled_nodes, the number of nodes running the daemon pod but not supposed to) — note these are node counts, not pod counts. It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes / ready_nodes / misscheduled_nodes, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes, readyNodes, misscheduledNodes) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes DaemonSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the daemonset, plus average CPU/memory request and limit utilization (daemonSetCPURequest, daemonSetCPULimit, daemonSetMemoryRequest, daemonSetMemoryLimit). Each row also reports the latest known node-level counters from kube-state-metrics: desiredNodes (k8s.daemonset.desired_scheduled_nodes, the number of nodes the daemonset wants to run on), currentNodes (k8s.daemonset.current_scheduled_nodes, the number of nodes the daemonset currently runs on), readyNodes (k8s.daemonset.ready_nodes, the number of nodes running at least one ready daemon pod) and misscheduledNodes (k8s.daemonset.misscheduled_nodes, the number of nodes running the daemon pod but not supposed to) — note these are node counts, not pod counts. It also reports per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each pod's latest kubectl-style display status). Each daemonset includes metadata attributes (k8s.daemonset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.daemonset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by daemonsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_nodes / current_nodes / ready_nodes / misscheduled_nodes, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (daemonSetCPU, daemonSetCPURequest, daemonSetCPULimit, daemonSetMemory, daemonSetMemoryRequest, daemonSetMemoryLimit, desiredNodes, currentNodes, readyNodes, misscheduledNodes) return -1 as a sentinel when no data is available for that field.
* @summary List DaemonSets for Infra Monitoring
*/
export const listDaemonSets = (
@@ -302,7 +302,7 @@ export const useListDaemonSets = <
return useMutation(getListDaemonSetsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes Deployments with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the deployment, plus average CPU/memory request and limit utilization (deploymentCPURequest, deploymentCPULimit, deploymentMemoryRequest, deploymentMemoryLimit). Each row also reports the latest known desiredPods (k8s.deployment.desired) and availablePods (k8s.deployment.available) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each deployment includes metadata attributes (k8s.deployment.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.deployment.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by deployments in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / available_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit, deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods, availablePods) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes Deployments with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the deployment, plus average CPU/memory request and limit utilization (deploymentCPURequest, deploymentCPULimit, deploymentMemoryRequest, deploymentMemoryLimit). Each row also reports the latest known desiredPods (k8s.deployment.desired) and availablePods (k8s.deployment.available) replica counts and per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each pod's latest kubectl-style display status). Each deployment includes metadata attributes (k8s.deployment.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.deployment.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by deployments in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / available_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (deploymentCPU, deploymentCPURequest, deploymentCPULimit, deploymentMemory, deploymentMemoryRequest, deploymentMemoryLimit, desiredPods, availablePods) return -1 as a sentinel when no data is available for that field.
* @summary List Deployments for Infra Monitoring
*/
export const listDeployments = (
@@ -468,7 +468,7 @@ export const useListHosts = <
return useMutation(getListHostsMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes Jobs with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the job, plus average CPU/memory request and limit utilization (jobCPURequest, jobCPULimit, jobMemoryRequest, jobMemoryLimit). Each row also reports the latest known job-level counters from kube-state-metrics: desiredSuccessfulPods (k8s.job.desired_successful_pods, the target completion count), activePods (k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative). It also reports per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value); note podCountsByPhase.failed (current pod-phase) is distinct from failedPods (cumulative job kube-state-metric). Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.job.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by jobs in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_successful_pods / active_pods / failed_pods / successful_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes Jobs with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the job, plus average CPU/memory request and limit utilization (jobCPURequest, jobCPULimit, jobMemoryRequest, jobMemoryLimit). Each row also reports the latest known job-level counters from kube-state-metrics: desiredSuccessfulPods (k8s.job.desired_successful_pods, the target completion count), activePods (k8s.job.active_pods), failedPods (k8s.job.failed_pods, cumulative across the lifetime of the job), and successfulPods (k8s.job.successful_pods, cumulative). It also reports per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each pod's latest kubectl-style display status); note podCountsByStatus.failed (current pod status) is distinct from failedPods (cumulative job kube-state-metric). Each job includes metadata attributes (k8s.job.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.job.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by jobs in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_successful_pods / active_pods / failed_pods / successful_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (jobCPU, jobCPURequest, jobCPULimit, jobMemory, jobMemoryRequest, jobMemoryLimit, desiredSuccessfulPods, activePods, failedPods, successfulPods) return -1 as a sentinel when no data is available for that field.
* @summary List Jobs for Infra Monitoring
*/
export const listJobs = (
@@ -634,7 +634,7 @@ export const useListContainers = <
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.
* 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 podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each pod's latest kubectl-style display status in the window). Each namespace includes metadata attributes (k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.namespace.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / memory, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (namespaceCPU, namespaceMemory) return -1 as a sentinel when no data is available for that field.
* @summary List Namespaces for Infra Monitoring
*/
export const listNamespaces = (
@@ -717,7 +717,7 @@ export const useListNamespaces = <
return useMutation(getListNamespacesMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes nodes with key metrics: CPU usage, CPU allocatable, memory working set, memory allocatable, per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready in the window) and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } for pods scheduled on the listed nodes). Each node includes metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is 'list' for the default k8s.node.name grouping (each row is one node with its current condition string: ready / not_ready / no_data) or 'grouped_list' for custom groupBy keys (each row aggregates nodes in the group; condition stays no_data). Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes nodes with key metrics: CPU usage, CPU allocatable, memory working set, memory allocatable, per-group nodeCountsByReadiness ({ ready, notReady } from each node's latest k8s.node.condition_ready in the window) and per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } for pods scheduled on the listed nodes, reflecting each pod's latest kubectl-style display status). Each node includes metadata attributes (k8s.node.uid, k8s.cluster.name). The response type is 'list' for the default k8s.node.name grouping (each row is one node with its current condition string: ready / not_ready / no_data) or 'grouped_list' for custom groupBy keys (each row aggregates nodes in the group; condition stays no_data). Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_allocatable / memory / memory_allocatable, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (nodeCPU, nodeCPUAllocatable, nodeMemory, nodeMemoryAllocatable) return -1 as a sentinel when no data is available for that field.
* @summary List Nodes for Infra Monitoring
*/
export const listNodes = (
@@ -800,7 +800,7 @@ export const useListNodes = <
return useMutation(getListNodesMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes pods with key metrics: CPU usage, CPU request/limit utilization, memory working set, memory request/limit utilization, current pod phase (pending/running/succeeded/failed/unknown/no_data), and pod age (ms since start time). Each pod includes metadata attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob, cluster). Supports filtering via a filter expression, custom groupBy to aggregate pods by any attribute, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid grouping (each row is one pod with its current phase) or 'grouped_list' for custom groupBy keys (each row aggregates pods in the group with per-phase counts under podCountsByPhase: { pending, running, succeeded, failed, unknown } derived from each pod's latest phase in the window). Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes pods with key metrics: CPU usage, CPU request/limit utilization, memory working set, memory request/limit utilization, current pod status (kubectl-style display status such as Running/Pending/CrashLoopBackOff, or no_data), and pod age (ms since start time). Each pod includes metadata attributes (namespace, node, workload owner such as deployment/statefulset/daemonset/job/cronjob, cluster). Supports filtering via a filter expression, custom groupBy to aggregate pods by any attribute, ordering by any of the six metrics (cpu, cpu_request, cpu_limit, memory, memory_request, memory_limit), and pagination via offset/limit. The response type is 'list' for the default k8s.pod.uid grouping (each row is one pod with its current status) or 'grouped_list' for custom groupBy keys (each row aggregates pods in the group with per-status counts under podCountsByStatus: { pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } derived from each pod's latest kubectl-style display status in the window). Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (podCPU, podCPURequest, podCPULimit, podMemory, podMemoryRequest, podMemoryLimit, podAge) return -1 as a sentinel when no data is available for that field.
* @summary List Pods for Infra Monitoring
*/
export const listPods = (
@@ -966,7 +966,7 @@ export const useListVolumes = <
return useMutation(getListVolumesMutationOptions(options));
};
/**
* Returns a paginated list of Kubernetes StatefulSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the statefulset, plus average CPU/memory request and limit utilization (statefulSetCPURequest, statefulSetCPULimit, statefulSetMemoryRequest, statefulSetMemoryLimit). Each row also reports the latest known desiredPods (k8s.statefulset.desired_pods) and currentPods (k8s.statefulset.current_pods) replica counts and per-group podCountsByPhase ({ pending, running, succeeded, failed, unknown } from each pod's latest k8s.pod.phase value). Each statefulset includes metadata attributes (k8s.statefulset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.statefulset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by statefulsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / current_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel when no data is available for that field.
* Returns a paginated list of Kubernetes StatefulSets with key aggregated pod metrics: CPU usage and memory working set summed across pods owned by the statefulset, plus average CPU/memory request and limit utilization (statefulSetCPURequest, statefulSetCPULimit, statefulSetMemoryRequest, statefulSetMemoryLimit). Each row also reports the latest known desiredPods (k8s.statefulset.desired_pods) and currentPods (k8s.statefulset.current_pods) replica counts and per-group podCountsByStatus ({ pending, running, failed, unknown, crashLoopBackOff, imagePullBackOff, errImagePull, createContainerConfigError, containerCreating, oomKilled, completed, error, containerCannotRun, evicted, nodeAffinity, nodeLost, shutdown, unexpectedAdmissionError } reflecting each pod's latest kubectl-style display status). Each statefulset includes metadata attributes (k8s.statefulset.name, k8s.namespace.name, k8s.cluster.name). The response type is 'list' for the default k8s.statefulset.name grouping or 'grouped_list' for custom groupBy keys; in both modes every row aggregates pods owned by statefulsets in the group. Supports filtering via a filter expression, custom groupBy, ordering by cpu / cpu_request / cpu_limit / memory / memory_request / memory_limit / desired_pods / current_pods, and pagination via offset/limit. Also reports whether the requested time range falls before the data retention boundary. Numeric metric fields (statefulSetCPU, statefulSetCPURequest, statefulSetCPULimit, statefulSetMemory, statefulSetMemoryRequest, statefulSetMemoryLimit, desiredPods, currentPods) return -1 as a sentinel when no data is available for that field.
* @summary List StatefulSets for Infra Monitoring
*/
export const listStatefulSets = (

File diff suppressed because it is too large Load Diff

View File

@@ -30,8 +30,10 @@ import type {
RenderErrorResponseDTO,
SpantypesPostableSpanMapperDTO,
SpantypesPostableSpanMapperGroupDTO,
SpantypesPostableSpanMapperTestDTO,
SpantypesUpdatableSpanMapperDTO,
SpantypesUpdatableSpanMapperGroupDTO,
TestSpanMappers200,
UpdateSpanMapperGroupPathParameters,
UpdateSpanMapperPathParameters,
} from '../sigNoz.schemas';
@@ -780,3 +782,86 @@ export const useUpdateSpanMapper = <
> => {
return useMutation(getUpdateSpanMapperMutationOptions(options));
};
/**
* Tests how span mappers would transform sample spans
* @summary Test span mappers against sample spans
*/
export const testSpanMappers = (
spantypesPostableSpanMapperTestDTO?: BodyType<SpantypesPostableSpanMapperTestDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<TestSpanMappers200>({
url: `/api/v1/span_mapper_groups/test`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: spantypesPostableSpanMapperTestDTO,
signal,
});
};
export const getTestSpanMappersMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testSpanMappers>>,
TError,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof testSpanMappers>>,
TError,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
TContext
> => {
const mutationKey = ['testSpanMappers'];
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 testSpanMappers>>,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> }
> = (props) => {
const { data } = props ?? {};
return testSpanMappers(data);
};
return { mutationFn, ...mutationOptions };
};
export type TestSpanMappersMutationResult = NonNullable<
Awaited<ReturnType<typeof testSpanMappers>>
>;
export type TestSpanMappersMutationBody =
| BodyType<SpantypesPostableSpanMapperTestDTO>
| undefined;
export type TestSpanMappersMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Test span mappers against sample spans
*/
export const useTestSpanMappers = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testSpanMappers>>,
TError,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof testSpanMappers>>,
TError,
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
TContext
> => {
return useMutation(getTestSpanMappersMutationOptions(options));
};

View File

@@ -1,26 +1,50 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { AxiosError, AxiosResponse } from 'axios';
import { ErrorV2Resp } from 'types/api';
import { ExportRawDataProps } from 'types/api/exportRawData/getExportRawData';
export interface FetchExportDataProps extends ExportRawDataProps {
signal?: AbortSignal;
}
async function postExportRawData({
format,
body,
signal,
}: FetchExportDataProps): Promise<AxiosResponse<Blob>> {
return axios.post<Blob>(
`export_raw_data?format=${encodeURIComponent(format)}`,
body,
{
responseType: 'blob',
decompress: true,
headers: {
Accept: 'application/octet-stream',
'Content-Type': 'application/json',
},
timeout: 0,
signal,
},
);
}
/**
* Fetches a single export_raw_data page and returns it as a Blob.
* Callers own retry/cancel/error UX.
*/
export async function fetchExportData(
props: FetchExportDataProps,
): Promise<Blob> {
const response = await postExportRawData(props);
return response.data;
}
export const downloadExportData = async (
props: ExportRawDataProps,
): Promise<void> => {
try {
const response = await axios.post<Blob>(
`export_raw_data?format=${encodeURIComponent(props.format)}`,
props.body,
{
responseType: 'blob',
decompress: true,
headers: {
Accept: 'application/octet-stream',
'Content-Type': 'application/json',
},
timeout: 0,
},
);
const response = await postExportRawData(props);
if (response.status !== 200) {
throw new Error(

View File

@@ -0,0 +1,7 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.9844 5.0511C7.9844 5.0511 3.17466 6.38853 2.79968 6.90476C2.4247 7.42098 1.77848 8.37718 1.71974 10.4246C1.67224 12.067 2.58844 16.2905 7.09195 16.4305C11.6905 16.5742 13.4029 13.5444 14.2241 11.6683C14.964 9.97584 14.9515 8.0322 15.4203 7.13974C15.889 6.24729 16.3115 5.47358 16.289 4.46488C16.2665 3.45619 15.8902 2.58748 15.2328 2.16501C14.5753 1.74253 14.0128 1.81252 13.7316 1.74253C13.4504 1.67253 7.9844 5.0511 7.9844 5.0511Z" fill="#757E40"/>
<path d="M8.82817 3.36241C6.83328 4.18361 4.18217 4.95732 2.91599 6.74098C1.7723 8.3509 1.39107 10.3533 2.93973 12.6994C4.35341 14.8418 8.12196 15.7555 10.9406 13.7319C13.5217 11.8782 13.7554 9.1571 14.5304 7.3497C15.3053 5.54229 16.9352 3.16742 14.9766 1.99998C12.888 0.757545 10.4043 2.71369 8.82817 3.36241Z" fill="#AFB42A"/>
<path d="M14.5304 2.51733C13.5817 1.96986 12.3717 2.21234 11.0343 2.86981C9.69688 3.52728 8.66444 4.01975 8.19571 4.18349C7.72574 4.34723 3.73345 5.80966 2.96349 7.74955C2.32977 9.34447 2.49476 11.4331 4.15967 12.958C5.82459 14.4829 10.0256 14.3892 11.6443 12.0893C13.2629 9.78945 13.3329 7.93704 13.6854 7.11584C14.0379 6.29463 14.8054 5.09219 15.0229 4.44098C15.2803 3.66727 15.1403 2.86981 14.5304 2.51733Z" fill="#FFF69D"/>
<path d="M9.93064 6.97475C9.17943 6.08355 6.31708 6.36478 5.33214 7.77221C4.34719 9.17963 4.75717 10.3233 5.23839 10.9395C5.82461 11.6908 7.42078 12.3707 8.87445 11.362C10.3281 10.3533 10.7543 7.9522 9.93064 6.97475Z" fill="#855C52"/>
<path d="M6.91449 8.57723C6.54451 9.1197 6.7295 9.99465 6.09328 10.0359C5.45707 10.0772 5.08708 9.05595 5.53831 8.201C6.02579 7.27855 7.18072 6.86732 7.50946 7.3048C7.83944 7.74352 7.26572 8.06226 6.91449 8.57723Z" fill="#D67659"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,47 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.7781 9.56835C12.7781 9.56835 16.2542 8.88588 16.4192 10.1983C16.5154 10.972 16.2729 12.3944 14.963 13.5631C13.8756 14.5331 12.0582 15.3493 9.05957 15.2943C6.61595 15.2505 4.95354 14.6643 3.82485 14.0156C1.93495 12.9294 1.56122 11.457 1.65996 10.5333C1.80121 9.20836 3.92484 8.6009 4.02109 8.5459L12.7781 9.56835Z" fill="#F1B34F"/>
<path d="M1.91504 10.8869C1.91504 10.8869 3.91993 14.2142 9.13716 14.2317C14.7856 14.2517 16.5118 10.8781 16.418 10.1969C16.2405 8.89075 13.2307 8.62451 13.2307 8.62451L6.04857 10.3169L1.91504 9.8632V10.8869Z" fill="#FADFB1"/>
<path d="M1.93617 9.68343C1.93617 9.68343 1.61119 10.0734 1.66868 10.8171C1.71993 11.4771 2.3399 12.4495 3.42359 12.8008C4.38229 13.112 5.086 12.7545 6.07845 13.0757C7.0709 13.397 7.98585 14.3719 9.25203 14.2957C10.5182 14.2194 11.1907 13.3145 12.1044 13.182C13.4768 12.9833 14.133 13.352 15.1705 12.3283C16.3142 11.1996 16.3204 10.1484 16.3142 9.88716C16.3017 9.35969 15.9279 9.07471 15.9279 9.07471L3.3311 9.53593L1.93617 9.68343Z" fill="white"/>
<path d="M9.07957 2.68481C4.12483 2.68481 1.64246 5.27593 1.44747 8.23077C1.37248 9.36821 1.74246 10.1132 2.54867 10.8231C3.95609 12.0606 6.52471 12.8355 9.17832 12.8643C12.0219 12.8955 14.1793 12.1756 15.8305 9.99693C16.3692 9.28697 16.4279 8.88324 16.4504 8.09328C16.5192 5.61591 14.2905 2.68481 9.07957 2.68481Z" fill="url(#paint0_radial_721_145)"/>
<path d="M6.53842 7.24459C6.48217 7.12084 6.91715 6.9696 7.13339 6.81211C7.37088 6.63837 7.41462 6.43338 7.50087 6.42213C7.58711 6.41088 7.96584 6.52837 8.93954 6.54087C9.7945 6.55212 10.3357 6.32463 10.4107 6.38963C10.487 6.45463 10.5945 6.63837 10.8332 6.87711C11.0394 7.08334 11.2769 7.22334 11.2882 7.29958C11.2994 7.37583 10.7519 7.49207 10.6557 7.51332C10.5582 7.53457 7.43462 7.52582 7.43462 7.52582C7.43462 7.52582 7.25213 7.47207 7.10089 7.42958C6.94965 7.38458 6.59217 7.36333 6.53842 7.24459Z" fill="#FCDE8C"/>
<path d="M8.97213 7.99091C9.52335 7.99091 9.82458 7.82967 10.1946 7.69842C10.5808 7.56218 10.8983 7.44969 10.8983 7.44969C10.8983 7.44969 10.3246 6.86597 9.05837 6.86597C7.79219 6.86597 7.24097 7.46094 7.24097 7.46094C7.24097 7.46094 8.32341 7.99091 8.97213 7.99091Z" fill="white"/>
<path d="M5.67215 4.62494C5.52341 4.65869 5.51841 4.88618 5.47341 5.10117C5.38217 5.53365 5.58716 5.88363 5.97589 5.77238C6.42086 5.64489 6.31962 5.20866 6.05838 4.91868C5.90589 4.75119 5.82464 4.58994 5.67215 4.62494Z" fill="#F5E5C7"/>
<path d="M7.75965 4.31862C7.71965 4.42611 7.36842 4.49986 7.01094 4.45986C6.74595 4.42986 6.52346 4.16488 6.6647 3.86989C6.80845 3.57116 7.16593 3.53616 7.40841 3.77365C7.65715 4.01738 7.80339 4.19987 7.75965 4.31862Z" fill="#F5E5C7"/>
<path d="M8.52576 4.12493C8.44077 4.10743 8.16203 4.34617 8.10079 4.6124C8.03079 4.90989 8.23078 5.03238 8.39577 5.04988C8.59951 5.07113 8.747 4.91364 8.7595 4.7099C8.7695 4.50741 8.61701 4.14368 8.52576 4.12493Z" fill="#F5E5C7"/>
<path d="M4.20737 7.25711C4.26611 7.39835 4.4911 7.3621 4.71109 7.36835C5.15232 7.38085 5.46105 7.11836 5.28356 6.75463C5.07982 6.3384 4.66734 6.51715 4.42985 6.82463C4.29111 7.00462 4.14737 7.11336 4.20737 7.25711Z" fill="#F5E5C7"/>
<path d="M2.6774 7.23603C2.52491 7.22853 2.46116 7.44727 2.36117 7.64351C2.15993 8.03599 2.26492 8.42846 2.66865 8.42347C3.13113 8.41847 3.14738 7.96974 2.97239 7.62226C2.86989 7.41977 2.83364 7.24228 2.6774 7.23603Z" fill="#F5E5C7"/>
<path d="M14.0117 6.92965C14.163 6.9109 14.1892 6.68466 14.2567 6.47467C14.3892 6.05344 14.2205 5.68471 13.823 5.75721C13.3668 5.8397 13.4255 6.28468 13.6567 6.59841C13.7917 6.7809 13.8567 6.9484 14.0117 6.92965Z" fill="#F5E5C7"/>
<path d="M5.79346 9.21074C5.72096 9.15824 5.49097 9.22698 5.28598 9.30823C4.876 9.47072 4.69601 9.83445 5.00225 10.0969C5.35348 10.3982 5.66221 10.0719 5.75971 9.69571C5.8172 9.47697 5.88095 9.27448 5.79346 9.21074Z" fill="#F5E5C7"/>
<path d="M5.07238 8.79214C5.10738 8.72214 5.01364 8.53215 4.91989 8.38091C4.81115 8.20467 4.6874 8.02468 4.51491 7.97593C3.95994 7.81969 3.75995 8.52715 4.13243 8.75339C4.26868 8.83588 4.41242 8.84088 4.57116 8.84963C4.80865 8.85963 5.03864 8.85963 5.07238 8.79214Z" fill="#F5E5C7"/>
<path d="M3.81117 6.75339C3.78867 6.89463 3.16996 7.01212 2.85372 6.82088C2.47499 6.5909 2.67873 6.06092 3.12996 6.09967C3.51619 6.13467 3.83617 6.5934 3.81117 6.75339Z" fill="#F5E5C7"/>
<path d="M4.13105 5.98216C4.27729 6.02216 4.69977 5.70593 4.82351 5.47594C4.91351 5.30845 4.986 4.89222 4.69352 4.76098C4.36729 4.61474 4.08855 4.82972 4.00606 5.09846C3.90106 5.45344 4.00106 5.94717 4.13105 5.98216Z" fill="#F5E5C7"/>
<path d="M6.631 7.70587C6.65475 7.81462 6.44476 8.40959 5.95478 8.43208C5.6173 8.44708 5.32482 7.99336 5.74604 7.69962C6.06603 7.47838 6.61475 7.62713 6.631 7.70587Z" fill="#F5E5C7"/>
<path d="M7.05344 8.65468C6.9572 8.57343 6.45098 8.93591 6.29848 9.2009C6.20849 9.35839 6.14599 9.64588 6.51847 9.81462C6.87845 9.97836 7.07594 9.70212 7.12094 9.47713C7.16594 9.25215 7.12719 8.71717 7.05344 8.65468Z" fill="#F5E5C7"/>
<path d="M7.56088 10.4421C7.68462 10.6733 8.32584 10.5733 8.47833 10.5271C8.69832 10.4596 9.00955 10.2546 8.81081 9.82835C8.64707 9.47962 8.21959 9.55212 7.96085 9.7721C7.73712 9.96084 7.47588 10.2846 7.56088 10.4421Z" fill="#F5E5C7"/>
<path d="M9.62712 9.62576C9.49463 9.72326 9.59963 10.3195 9.67837 10.4982C9.77962 10.7295 10.1058 10.8195 10.3258 10.6507C10.5796 10.4557 10.5396 10.1332 10.3771 9.95825C10.2533 9.827 9.75587 9.52952 9.62712 9.62576Z" fill="#F5E5C7"/>
<path d="M10.5782 9.25952C10.5295 9.30327 10.0945 9.33326 9.78451 9.06828C9.60577 8.91579 9.61077 8.55455 9.79076 8.42081C9.98825 8.27457 10.2582 8.31957 10.432 8.5733C10.5407 8.72955 10.6345 9.20827 10.5782 9.25952Z" fill="#F5E5C7"/>
<path d="M11.2369 9.04573C11.3244 9.12198 11.8781 9.02823 12.0868 8.787C12.2006 8.6545 12.3631 8.37077 12.1431 8.16828C11.9231 7.96579 11.6643 8.00454 11.4394 8.28077C11.2719 8.48451 11.1469 8.96699 11.2369 9.04573Z" fill="#F5E5C7"/>
<path d="M13.3819 7.3848C13.3394 7.55479 12.7182 7.62479 12.4695 7.50854C12.2158 7.3898 12.2607 7.10357 12.312 6.97982C12.3632 6.85608 12.487 6.70984 12.8132 6.79983C13.1582 6.89483 13.4107 7.27231 13.3819 7.3848Z" fill="#F5E5C7"/>
<path d="M12.5143 9.30959C12.3756 9.27084 12.2943 9.46083 12.2043 9.54583C12.0918 9.65332 11.6194 10.1995 12.2218 10.3395C12.8256 10.4808 12.6606 9.81581 12.6381 9.68082C12.6231 9.58708 12.6156 9.33834 12.5143 9.30959Z" fill="#F5E5C7"/>
<path d="M13.1619 8.95576C13.1119 9.0995 13.3931 9.38324 13.5281 9.54698C13.6631 9.71072 13.9618 9.86196 14.1356 9.53573C14.3106 9.2095 14.0393 9.00701 13.7981 8.95076C13.5569 8.89326 13.2069 8.82577 13.1619 8.95576Z" fill="#F5E5C7"/>
<path d="M15.3192 7.79581C15.3192 7.89206 14.9817 8.16204 14.6042 8.17329C14.323 8.18204 14.1143 8.07955 14.1143 7.80206C14.1143 7.52083 14.368 7.36333 14.683 7.43083C14.8642 7.46833 15.3192 7.64332 15.3192 7.79581Z" fill="#F5E5C7"/>
<path d="M14.6654 6.97962C14.6942 7.09211 15.0317 7.12586 15.2729 7.13211C15.5154 7.13836 15.8129 7.05336 15.7004 6.72088C15.5879 6.3884 15.2554 6.47339 15.1316 6.53464C15.0092 6.59589 14.6279 6.82712 14.6654 6.97962Z" fill="#F5E5C7"/>
<path d="M13.6131 4.7547C13.5119 4.90094 13.7031 5.20468 13.8781 5.33967C14.0531 5.47466 14.2943 5.55966 14.4356 5.32217C14.5768 5.08593 14.3281 4.87219 14.1768 4.77595C14.0243 4.6822 13.7119 4.61221 13.6131 4.7547Z" fill="#F5E5C7"/>
<path d="M13.0444 5.03096C12.9781 4.98971 12.6281 5.07845 12.4257 5.26219C12.2157 5.45218 12.1382 5.72967 12.3919 5.92716C12.6369 6.11715 12.9031 5.93216 13.0106 5.67967C13.1119 5.44218 13.1194 5.0772 13.0444 5.03096Z" fill="#F5E5C7"/>
<path d="M10.3083 4.87363C10.3758 4.77989 10.2521 4.27742 10.0946 4.08493C9.93711 3.89369 9.73962 3.81994 9.52088 3.98368C9.34839 4.11242 9.28465 4.46241 9.53213 4.6424C9.80087 4.83989 10.2346 4.97488 10.3083 4.87363Z" fill="#F5E5C7"/>
<path d="M10.5044 4.09607C10.5882 4.15732 10.8932 4.11357 11.1006 3.97733C11.3081 3.84109 11.3394 3.62485 11.2194 3.47611C11.0731 3.29611 10.8369 3.36361 10.6682 3.5836C10.5207 3.77484 10.3982 4.01733 10.5044 4.09607Z" fill="#F5E5C7"/>
<path d="M11.0059 4.82243C11.0734 5.00242 11.7496 4.96368 11.9458 4.87868C12.1433 4.79369 12.2808 4.61495 12.1708 4.35496C12.0471 4.06247 11.8521 4.06122 11.7096 4.10122C11.4283 4.18122 10.9396 4.64619 11.0059 4.82243Z" fill="#F5E5C7"/>
<defs>
<radialGradient id="paint0_radial_721_145" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(9.02655 6.51166) rotate(-0.374469) scale(8.19673 5.34427)">
<stop offset="0.1616" stop-color="#F1B14A"/>
<stop offset="0.1688" stop-color="#F1B049"/>
<stop offset="0.3704" stop-color="#E89825"/>
<stop offset="0.5354" stop-color="#E28810"/>
<stop offset="0.6425" stop-color="#E08308"/>
<stop offset="0.7125" stop-color="#E1860D"/>
<stop offset="0.7924" stop-color="#E5901B"/>
<stop offset="0.877" stop-color="#EBA031"/>
<stop offset="0.947" stop-color="#F1B14A"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -0,0 +1,8 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.78857 14.527C3.78857 14.527 6.19095 17.0543 10.312 16.4131C14.433 15.7731 16.0854 12.1596 16.4254 10.0997C16.8579 7.47608 15.6592 5.67242 15.5955 5.57368C15.548 5.50118 3.78857 14.527 3.78857 14.527Z" fill="#DC5819"/>
<path d="M3.60104 3.68857C1.13242 6.28719 0.713693 10.0832 2.56984 13.0168C3.43355 14.3818 3.96227 14.6592 3.96227 14.6592C3.96227 14.6592 8.00081 17.2491 12.5393 13.3231C16.4266 9.96074 15.6029 5.58222 15.6029 5.58222C15.6029 5.58222 15.1179 4.52228 14.098 3.51608C11.8344 1.2812 6.77462 0.346251 3.60104 3.68857Z" fill="#EF6C32"/>
<path d="M13.4956 3.00498C13.4594 3.00748 9.84832 3.26746 6.02852 8.40219C3.61615 11.6445 3.73489 13.9332 3.82864 14.5606C3.82864 14.5606 3.6449 14.4094 3.5149 14.2694C3.38491 14.1306 3.28366 14.0069 3.28366 14.0069C3.27366 12.9082 3.6024 10.8258 5.62854 8.10346C8.71213 3.95493 11.6607 2.87374 12.8969 2.59375C12.8969 2.59375 13.0481 2.6825 13.1944 2.77374C13.3019 2.84124 13.4956 3.00498 13.4956 3.00498Z" fill="#2D3130"/>
<path d="M2.94616 4.47241C3.34738 5.13113 4.31608 6.5548 5.99349 8.04097C8.10463 9.91337 10.2995 11.4358 12.5144 12.5645C13.5606 13.0982 14.3643 13.4107 14.933 13.5944C14.933 13.5944 14.8505 13.7207 14.773 13.8244C14.6955 13.9282 14.6105 14.0157 14.6105 14.0157C14.0318 13.8194 13.2594 13.5069 12.2857 13.0095C10.0333 11.8608 7.8034 10.3146 5.66101 8.4147C4.0736 7.00728 3.11115 5.67485 2.63867 4.92864C2.63867 4.92864 2.70492 4.79739 2.79241 4.68115C2.87991 4.56491 2.94616 4.47241 2.94616 4.47241Z" fill="#2D3130"/>
<path d="M7.64719 1.59619C7.46845 1.79993 7.21346 2.13116 6.91598 2.63239C6.64349 3.08986 6.5035 3.54484 6.34101 4.07231C6.07477 4.93727 5.77353 5.91721 4.78734 7.16965C3.41741 8.90956 2.07123 9.49078 1.44376 9.67702C1.44376 9.67702 1.42626 9.51452 1.42002 9.40078C1.41377 9.28704 1.41627 9.15829 1.41627 9.15829C2.01248 8.95455 3.19242 8.38458 4.39361 6.86091C5.32231 5.68098 5.60854 4.74853 5.86228 3.92607C6.02727 3.3911 6.18226 2.88612 6.486 2.3774C6.62974 2.13616 6.76473 1.92992 6.88848 1.75743C6.88848 1.75743 7.02722 1.71119 7.21721 1.66869C7.3647 1.63369 7.64719 1.59619 7.64719 1.59619Z" fill="#2D3130"/>
<path d="M9.58958 16.5004C9.17085 16.1217 8.30589 15.2568 7.67468 14.0531C6.97596 12.7219 6.64848 10.7195 7.58093 9.46831C8.64588 8.04088 10.6345 7.84714 12.4419 7.84714C12.4444 7.84714 12.4469 7.84714 12.4494 7.84714C14.7506 7.84839 16.4817 8.32962 16.5017 8.33587C16.5017 8.33587 16.488 8.16712 16.4692 8.02338C16.453 7.89964 16.4355 7.78464 16.4355 7.78464C16.348 7.76215 14.8181 7.34842 12.4494 7.34717C12.4469 7.34717 12.4444 7.34717 12.4419 7.34717C10.5108 7.34717 8.37589 7.56466 7.1797 9.16957C6.40974 10.202 6.12726 12.1844 7.2322 14.2856C7.75217 15.2755 8.40714 16.0392 8.88586 16.5204C8.88586 16.5204 9.02586 16.5254 9.2046 16.5229C9.38084 16.5204 9.58958 16.5004 9.58958 16.5004Z" fill="#2D3130"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,11 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.53866 2.43392C1.32492 2.80765 1.6599 3.60261 3.29731 3.92759C5.33596 4.33257 7.53209 3.51261 9.18825 3.80135C10.1307 3.96634 10.7282 4.18008 11.0707 4.33507C11.3369 4.45631 11.3369 4.46631 11.3369 4.46631L11.7594 3.12638C11.7594 3.12638 10.5832 2.48892 9.00576 2.41892C7.42835 2.34893 6.25591 2.86515 4.69224 2.8239C2.33487 2.7614 1.87364 1.8477 1.53866 2.43392Z" fill="#966738"/>
<path d="M13.7792 6.71613L15.0767 6.18616C15.0767 6.18616 15.7491 7.12486 15.9866 8.96726C16.2704 11.1684 15.5816 12.6246 15.8054 13.867C16.0291 15.1094 16.5178 15.7794 16.4753 16.0731C16.4328 16.3669 16.2241 16.6869 15.9454 16.5344C15.6666 16.3806 14.7967 15.2632 14.7592 13.7145C14.7167 11.9971 15.1929 10.5647 14.8017 9.03851C14.3667 7.34735 13.7792 6.71613 13.7792 6.71613Z" fill="#966738"/>
<path d="M11.6594 3.07629L11.0981 4.34873C11.0981 4.34873 11.8494 4.67996 12.6781 5.43492C13.4018 6.09488 13.9205 6.90109 13.9205 6.90109L15.0792 6.18863C15.0792 6.18863 14.478 5.12369 13.5993 4.35998C12.5531 3.45002 11.6594 3.07629 11.6594 3.07629Z" fill="#D29F6C"/>
<path d="M13.8368 3.62756L5.21472 12.4546L5.25222 12.8646L5.6247 12.9208L14.3405 4.11254L13.8368 3.62756Z" fill="#F3C976"/>
<path d="M14.2167 3.2026L13.768 3.70382L14.2605 4.1963L14.7805 3.72132C14.7805 3.72132 15.6079 3.88006 15.6967 3.82757C15.8179 3.75507 16.5441 1.50894 16.5066 1.45019C16.4454 1.35395 15.6867 1.65768 15.2042 1.82892C14.6317 2.03141 14.148 2.2264 14.1042 2.2864C14.0055 2.41889 14.2167 3.2026 14.2167 3.2026Z" fill="#858585"/>
<path d="M15.4192 16.2531C15.3979 16.2531 15.3754 16.2506 15.3542 16.2444L4.93848 13.422C4.85349 13.3983 4.78599 13.3321 4.76349 13.2471L2.10863 3.61756C2.09613 3.57132 2.09613 3.52382 2.11113 3.47757L2.43361 2.41138L2.91109 2.56637L2.6636 3.73006L5.18722 12.9146L15.3192 15.7457L16.0741 14.8745L16.2641 15.3657L15.6129 16.1644C15.5629 16.2219 15.4929 16.2531 15.4192 16.2531Z" fill="#B8CED4"/>
<path d="M5.22598 12.5245L5.6247 12.9208C5.6247 12.9208 5.20348 13.432 5.13598 13.4382C5.09598 13.442 4.74975 13.1583 4.77725 13.0958C4.80475 13.0345 5.22598 12.5245 5.22598 12.5245Z" fill="#5D6265"/>
<path d="M7.46208 10.221C7.45833 10.231 6.79712 10.9272 6.18715 11.5709C5.71468 12.0697 5.2672 12.5534 5.23345 12.5534C5.15721 12.5534 4.46849 11.9034 4.45724 11.7522C4.44599 11.6009 5.49969 10.6922 5.99966 10.3385C7.03211 9.60728 7.46208 10.221 7.46208 10.221Z" fill="#DB0D2A"/>
<path d="M7.85453 10.6246C7.82829 10.6484 7.07083 11.4208 6.44211 12.0621C6.01088 12.502 5.6284 12.8783 5.6234 12.9145C5.61715 12.9658 6.20087 13.6407 6.26212 13.6407C6.37461 13.6407 7.4783 12.4633 7.82079 11.8696C8.32576 10.9946 7.85453 10.6246 7.85453 10.6246Z" fill="#DB0D2A"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,39 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.97592 1.44005C6.96717 1.62005 6.80718 16.5155 6.82593 16.5918C6.84468 16.668 7.93962 16.6468 7.93962 16.603C7.93962 16.5068 8.13336 1.5488 8.12961 1.4513C8.12586 1.39131 6.97842 1.37256 6.97592 1.44005Z" fill="#FFD51D"/>
<path d="M6.97341 1.55371C6.97341 1.55371 7.35339 2.06868 7.59088 2.37742C7.80337 2.65365 8.1121 3.00363 8.1121 3.00363L8.1021 3.75359C8.1021 3.75359 7.62213 3.21612 7.37964 2.92739C7.11715 2.61615 6.96466 2.36867 6.96466 2.36867L6.97341 1.55371Z" fill="#FDA726"/>
<path d="M6.93969 4.33862C6.93969 4.33862 7.18218 4.76485 7.49716 5.16858C7.7534 5.49731 8.07338 5.79355 8.07338 5.79355L8.05713 7.27597L6.90344 7.05973L6.93969 4.33862Z" fill="#FDA726"/>
<path d="M6.86471 11.6094L6.86096 12.1306C6.86096 12.1306 7.09095 12.5144 7.38343 12.8468C7.62717 13.1231 7.9784 13.4968 7.9784 13.4968L8.00215 11.6094L7.52968 11.3782L6.86471 11.6094Z" fill="#FDA726"/>
<path d="M6.84093 14.1719C6.84093 14.1719 7.20967 14.6531 7.4659 14.9406C7.66964 15.1693 7.95463 15.4693 7.95463 15.4693L7.94463 16.1855C7.94463 16.1855 7.4659 15.7018 7.24966 15.4693C7.03342 15.2368 6.83344 15.0043 6.83344 15.0043L6.84093 14.1719Z" fill="#FDA726"/>
<path d="M2.10621 13.4207C2.10621 13.4207 1.91872 13.1407 2.03371 12.782C2.15496 12.402 2.42994 12.262 2.55869 11.9583C2.73618 11.5383 2.69493 11.1983 3.02741 10.8833C3.32615 10.6008 3.73862 10.7058 4.10236 10.6008C4.46609 10.4958 4.61983 10.2771 4.61983 10.2771L5.46853 10.5358C5.46853 10.5358 5.35604 11.4808 5.01606 11.4645C4.67608 11.4483 3.50489 11.667 3.48114 11.7145C3.45739 11.7633 2.41494 13.4032 2.41494 13.4032L2.10621 13.4207Z" fill="#8A867E"/>
<path d="M9.99951 11.3682C10.0158 11.4407 10.2907 11.8369 10.727 12.1919C11.1632 12.5469 11.8094 12.8056 12.0282 12.9681C12.2469 13.1294 12.4081 13.2019 12.6744 13.5581C12.9406 13.9143 13.0294 14.1318 13.2243 14.4068C13.4181 14.6818 13.6206 14.8755 13.7981 15.013C13.9818 15.1543 14.3555 15.3767 14.4205 15.368C14.4855 15.3605 14.768 14.7705 14.768 14.7705C14.768 14.7705 13.9143 13.7981 13.7231 13.4681C13.5318 13.1394 13.1493 12.6181 12.7244 12.3744C12.5119 12.2519 11.1232 10.5208 11.1232 10.5208L9.99951 11.3682Z" fill="#8A867E"/>
<path d="M12.6732 7.68727C12.6732 7.68727 12.8119 7.52478 12.9019 7.46228C13.0156 7.38228 13.1081 7.33604 13.2881 7.33229C13.4506 7.32979 13.9506 7.76476 13.9506 7.76476C13.9506 7.76476 15.133 9.9559 15.1368 9.95215C15.1405 9.9484 15.9055 10.8246 15.8917 10.8071C15.878 10.7884 16.3617 11.0371 16.3617 11.0371C16.3617 11.0371 16.2792 11.5821 15.3143 11.5533C14.3706 11.5258 13.7743 10.9896 13.4394 10.2771C13.2781 9.93465 13.2531 9.31343 13.2531 9.31343L12.6732 7.68727Z" fill="#7A57BD"/>
<path d="M13.0581 7.3709C13.0581 7.3709 13.1256 7.67214 13.3706 7.88963C13.6156 8.10587 13.8306 8.29586 14.0006 8.56834C14.1705 8.84083 14.5543 9.83452 15.0468 10.4145C15.5392 10.9945 15.7255 11.032 15.9167 11.102C16.0779 11.1607 16.2342 11.1545 16.2917 11.132C16.3804 11.0995 16.4642 10.927 16.0592 10.5532C15.6542 10.1795 15.4005 9.73703 15.178 9.29205C14.9555 8.84708 14.8443 7.85338 14.233 7.4409C13.6218 7.02967 13.0581 7.3709 13.0581 7.3709Z" fill="#A47FCC"/>
<path d="M13.0418 7.92847C13.0418 7.92847 13.4293 8.43219 13.4131 9.12215C13.3968 9.81212 13.0606 10.3008 13.0606 10.7096C13.0606 11.1183 13.1493 11.3995 13.5493 11.5195C13.9505 11.6395 14.1668 11.712 14.2393 11.8808C14.3118 12.0495 14.2955 12.4745 14.4793 12.827C14.663 13.1794 14.803 13.2607 14.888 13.4444C14.9842 13.6494 15.0005 13.9894 15.0005 13.9894C15.0005 13.9894 14.8005 14.9593 14.568 14.5506C14.3355 14.1419 13.5331 12.482 13.3093 12.2895C13.0843 12.097 11.8819 10.7733 11.8819 10.7021C11.8819 10.6308 13.0418 7.92847 13.0418 7.92847Z" fill="#ACA399"/>
<path d="M2.77615 13.5869L3.23113 13.6507L3.25238 13.322C3.25238 13.322 3.40112 13.0145 3.54861 12.8132C3.69735 12.612 3.83485 12.3895 4.11983 12.347C4.40607 12.3045 6.21597 12.347 6.36471 12.262C6.51346 12.177 6.81969 11.7958 6.81969 11.7958C6.81969 11.7958 7.91338 11.8908 9.04332 11.6583C9.84828 11.492 10.377 11.4358 10.377 11.4358L11.1707 11.3621C11.1707 11.3621 12.0069 10.9921 11.9857 10.9171C11.9644 10.8433 11.3395 9.70964 11.2757 9.74214C11.212 9.77339 6.43721 10.2184 6.36346 10.3034C6.28972 10.3884 5.42101 11.2771 5.42101 11.2771C5.42101 11.2771 3.51486 11.7745 3.48362 11.7858C3.45237 11.7958 2.65741 13.4482 2.65741 13.4482L2.77615 13.5869Z" fill="#ACA399"/>
<path d="M4.02607 9.23706C4.02607 9.23706 4.02107 8.84833 4.08232 8.36336C4.14106 7.89338 4.24981 7.37216 4.26106 6.96968C4.27356 6.52095 4.22231 6.25472 4.17481 6.23972C4.12857 6.22472 3.90233 6.75969 3.90233 6.75969C3.90233 6.75969 3.91608 7.00218 3.81233 7.24092C3.68984 7.52215 3.49485 7.42216 3.49485 7.42216L3.26361 6.6372L3.71484 5.39101L3.95607 3.6936L4.75853 4.26232L5.296 6.40471L5.10851 8.4371L4.56979 9.44955L4.02607 9.23706Z" fill="#ACA399"/>
<path d="M11.2245 7.05347L10.6982 8.3184L11.287 11.0195L10.7832 11.412C10.7832 11.412 11.1532 12.1219 11.7294 12.4019C12.3069 12.6819 12.8206 12.6369 12.9919 12.6269C13.1631 12.6182 13.2781 12.6919 13.3794 12.8344C13.8593 13.5031 13.8306 14.1093 13.9643 14.3581C14.1981 14.7906 14.488 14.8268 14.488 14.8268C14.488 14.8268 15.0105 14.6368 14.993 14.6018C14.9755 14.5656 14.7493 14.1593 14.7493 14.1593C14.7493 14.1593 14.6593 13.7631 14.4518 13.4019C14.2443 13.0407 14.1268 12.8607 14.0731 12.4819C14.0193 12.1032 14.0006 11.9232 13.5956 11.8332C13.1894 11.7432 12.6669 11.4907 12.7031 10.977C12.7394 10.4633 13.3844 9.37084 13.2894 8.57839C13.2081 7.90217 12.6219 7.26221 11.7019 7.11721C11.4482 7.07722 11.2245 7.05347 11.2245 7.05347Z" fill="#F1EDEC"/>
<path d="M15.0118 13.7907C14.9555 13.8007 14.813 14.1419 14.7055 14.3231C14.6206 14.4656 14.3993 14.7919 14.4168 14.8369C14.4343 14.8819 15.1393 15.5631 15.183 15.5943C15.2468 15.6393 15.373 15.5943 15.4268 15.3956C15.4805 15.1968 15.743 14.0607 15.743 14.0607C15.743 14.0607 15.1205 13.7719 15.0118 13.7907Z" fill="#5E6367"/>
<path d="M11.8107 7.56722C11.7644 7.70471 11.8919 7.80221 12.1169 8.01845C12.3419 8.23468 12.4782 8.49592 12.5044 8.8034C12.5319 9.10964 12.5831 9.33713 12.7481 9.34463C12.9469 9.35337 13.1669 8.8934 12.8294 8.20843C12.4694 7.47722 11.8382 7.48597 11.8107 7.56722Z" fill="white"/>
<path d="M11.2957 7.2373L11.2782 10.9621C11.2782 10.9621 10.967 11.0834 10.577 10.9621C10.187 10.8409 10.057 10.5984 10.057 10.5984C10.057 10.5984 9.77955 11.0659 9.02584 11.0571C8.27213 11.0484 8.08214 10.6934 8.08214 10.6934C8.08214 10.6934 7.95089 10.9859 7.39842 11.0571C7.08594 11.0971 6.8197 11.0971 6.7672 10.9759C6.71471 10.8546 5.841 8.14476 5.841 8.14476L8.80085 7.37605L11.2957 7.2373Z" fill="#B0E4FE"/>
<path d="M7.16468 8.23345L7.13843 10.6058C7.13843 10.6058 7.39716 10.6083 7.63215 10.4496C7.87714 10.2833 7.98713 10.1171 8.03088 10.1209C8.09088 10.1259 8.40336 10.6071 9.00083 10.6233C9.66829 10.6408 9.96203 10.0696 10.0145 10.0696C10.0833 10.0696 10.2295 10.3171 10.3783 10.3983C10.647 10.5458 10.8457 10.4758 10.8457 10.4758L10.8545 7.78223L7.16468 8.23345Z" fill="#34B6E2"/>
<path d="M2.01994 4.46994C2.01994 4.46994 1.9012 3.72873 2.17744 3.29375C2.45367 2.85877 2.87865 2.74003 2.87865 2.74003L3.65986 2.54254C3.65986 2.54254 3.96234 2.41629 4.53981 2.43379C5.10978 2.45129 5.59851 2.74378 5.86349 2.89877C6.16973 3.07626 6.45596 3.46249 6.35722 3.62998C6.25847 3.79747 5.99223 3.67998 5.99223 3.67998C5.99223 3.67998 6.71345 4.1837 7.05968 5.0924C7.34716 5.84861 7.38591 6.61357 7.38591 6.61357L8.31461 8.066L11.1207 6.68232C11.1207 6.68232 11.3095 6.67107 11.4269 6.76107C11.5944 6.88981 11.6257 7.0948 11.6257 7.0948C11.6257 7.0948 11.4444 7.14479 11.2482 7.42353C10.7045 8.19474 10.3883 10.0521 8.88708 9.9234C7.58215 9.81215 7.35591 8.50097 7.26592 8.16474C7.17717 7.82851 6.00848 6.75732 6.00848 6.75732L4.89479 4.21245L3.7186 3.13501L2.44367 4.38994L2.01994 4.46994Z" fill="#7A57BD"/>
<path d="M4.64982 10.9221C4.64982 10.9221 4.39733 11.0658 4.07235 11.1121C3.74736 11.1571 3.40488 11.1033 3.14365 11.3821C2.88241 11.6608 2.9899 12.0583 2.78241 12.3645C2.57493 12.6708 2.40368 12.8607 2.23244 13.0495C2.0612 13.2382 2.02495 13.4557 2.0337 13.6357C2.04245 13.8157 2.1962 14.1319 2.1962 14.1319L3.0624 14.0682L3.05365 13.5095C3.05365 13.5095 2.93741 13.3082 3.09865 13.032C3.20239 12.8545 3.48613 12.3833 3.57612 12.2658C3.66612 12.1483 3.72612 12.0108 3.93735 11.977C4.16609 11.9395 5.9935 11.9233 6.16474 11.8421C6.33598 11.7608 6.70596 11.3646 6.73346 11.0671C6.76096 10.7696 6.77845 8.35224 6.77845 8.35224L6.9472 7.02106C6.9472 7.02106 6.84845 6.75107 6.74721 6.47983C6.40472 5.55988 5.98475 4.59743 5.18979 3.68248C4.40858 2.78378 3.62237 2.87877 3.62237 2.87877C3.62237 2.87877 2.40493 1.77008 2.34119 1.67884C2.27744 1.58759 2.1887 1.49885 2.16995 1.57009C2.1512 1.64134 2.2062 2.41755 2.2062 2.41755L2.65742 3.19251C2.65742 3.19251 2.56743 3.56249 2.41368 3.93247C2.25994 4.30245 2.01996 4.46869 2.01996 4.46869L1.92871 6.1086C1.92871 6.1086 1.73747 6.40734 1.73122 6.79607C1.72497 7.17855 3.24989 6.75607 3.25614 6.72357C3.26239 6.69107 3.77736 5.28865 3.77111 5.2174C3.76486 5.14616 4.15484 4.02371 4.15484 4.02371C4.15484 4.02371 4.84731 5.01866 4.73731 6.17485C4.69981 6.57483 4.63357 6.99856 4.57732 7.33979C4.39358 8.46223 4.25859 9.21844 4.25859 9.21844L4.64982 10.6221V10.9221Z" fill="#F1EDEC"/>
<path d="M3.60862 13.8983C3.60862 13.952 3.44488 14.232 3.1024 14.692C2.75992 15.1519 2.63742 15.3019 2.50868 15.1194C2.37994 14.9369 2.33244 14.627 2.28369 14.4632C2.23994 14.3157 2.1662 14.0782 2.1662 14.0782L3.25239 13.332C3.25364 13.332 3.60862 13.7808 3.60862 13.8983Z" fill="#5E6367"/>
<path d="M2.79364 5.25611C2.73739 5.2811 2.6874 5.52984 2.88239 5.77483C3.07738 6.01981 3.30111 6.13731 3.30111 6.13731L3.47361 5.75858C3.47361 5.75858 3.23487 5.60859 3.09987 5.48484C2.96113 5.3561 2.84364 5.23361 2.79364 5.25611Z" fill="white"/>
<path d="M2.16995 1.56992C2.13496 1.58617 1.92622 1.85616 2.06996 2.40238C2.15495 2.72611 2.76242 3.32608 2.76242 3.32608L3.1249 3.57982C3.1249 3.57982 3.07116 3.20234 2.85492 2.9486C2.63868 2.69486 2.38494 2.40988 2.3162 2.17114C2.24745 1.93241 2.2862 1.51618 2.16995 1.56992Z" fill="#ACA399"/>
<path d="M1.92498 6.10737C1.92498 6.10737 1.71374 6.4811 1.72249 6.84483C1.72999 7.17731 1.83624 7.35605 2.04123 7.53729C2.24497 7.71728 2.59745 7.84853 2.81744 7.75978C2.84743 7.74728 2.85993 7.53729 2.93118 7.38105C2.98618 7.26231 3.17742 7.07107 3.17742 7.07107C3.17742 7.07107 3.26991 7.27106 3.28491 7.37105C3.29991 7.47105 3.30991 7.66979 3.34616 7.67854C3.4849 7.71353 3.67614 7.57479 3.79738 7.27481C3.92738 6.95357 3.90113 6.76608 3.90113 6.76608L4.10862 6.29111C4.10862 6.29111 4.00112 5.4599 3.29241 5.26741C2.5837 5.07492 1.96373 5.4299 1.96373 5.4299L1.92498 6.10737Z" fill="url(#paint0_radial_715_295)"/>
<path d="M2.22246 6.3922C2.02497 6.36971 1.94623 6.59094 1.94623 6.83718C1.94623 7.08342 2.07872 7.18591 2.20496 7.18591C2.33121 7.18591 2.4462 7.02342 2.4462 6.79468C2.44495 6.5672 2.37871 6.41095 2.22246 6.3922Z" fill="#845B51"/>
<path d="M2.35742 4.74364C2.37992 4.79989 2.44742 4.88989 2.54366 4.92363C2.63991 4.95738 2.7524 4.94113 2.7524 4.94113C2.7524 4.94113 2.78115 5.18362 2.95489 5.18987C3.13738 5.19612 3.15238 4.87364 3.12363 4.71615C3.09488 4.55865 2.99989 4.45116 2.99989 4.45116C2.99989 4.45116 2.7124 4.41116 2.53116 4.53616C2.39867 4.62615 2.35742 4.74364 2.35742 4.74364Z" fill="#464D50"/>
<path d="M3.26488 2.55373C3.26488 2.55373 3.56736 2.79622 3.60486 3.21495C3.64611 3.68367 3.64486 3.78992 3.64486 3.78992L2.54367 3.09745C2.54367 3.09745 2.49117 3.2187 2.48867 3.35369C2.48617 3.49993 2.50742 3.65118 2.50742 3.65118L3.65111 4.35364C3.65111 4.35364 3.60736 4.97986 3.42237 5.63482C3.25863 6.21229 3.03364 6.64227 2.99614 6.72226C2.95864 6.80226 2.8274 7.14474 3.13863 7.22224C3.41112 7.28973 3.46612 6.901 3.54611 6.72726C3.62611 6.55352 3.81985 6.0648 3.93484 5.63357C4.24983 4.45363 4.18858 3.74867 4.11983 3.2612C4.04734 2.73997 3.88734 2.63998 3.69611 2.55873C3.55861 2.50249 3.35612 2.46499 3.26488 2.55373Z" fill="#34B6E2"/>
<path d="M4.01483 8.99843C4.01483 8.99843 3.98609 9.34841 4.06233 9.87089C4.12358 10.2846 4.44356 10.9633 4.6548 10.9121C4.70605 10.8996 4.57356 10.2534 4.77729 9.99588C4.91604 9.81964 5.16852 9.78839 5.81724 9.42091C6.35971 9.11343 6.84094 8.51221 6.84094 8.51221L6.73969 7.62476L6.05598 7.79225C6.05598 7.79225 5.556 8.27597 4.96978 8.5922C4.44856 8.87469 4.01483 8.99843 4.01483 8.99843Z" fill="#D853C9"/>
<path d="M6.45474 7.58598C6.45099 7.58723 6.211 7.48974 6.05601 7.74973C5.86477 8.07096 5.86352 8.65218 5.84477 8.69592C5.82602 8.73967 5.70978 8.87842 5.82602 8.90216C5.94101 8.92591 6.38849 8.88716 6.67598 8.68093C6.96471 8.47469 7.01721 8.19095 7.01721 8.19095L6.45474 7.58598Z" fill="#34B6E2"/>
<path d="M11.2569 6.6974C11.2569 6.6974 11.0832 6.68865 10.6982 7.17862C10.1758 7.84233 9.90827 9.20726 8.97582 9.20726C8.04337 9.20726 7.89463 8.3948 7.64839 7.76734C7.40215 7.13987 6.94718 6.79739 6.94718 6.79739C6.94718 6.79739 6.70844 6.64865 6.83593 6.45491C6.93343 6.30617 7.25091 6.43991 7.42965 6.5224C7.60839 6.6049 7.93088 6.81364 8.13336 6.89363C8.69209 7.11487 9.20831 6.97738 9.49954 6.91113C9.79078 6.84364 10.0608 6.71864 10.4995 6.6724C10.9895 6.61865 11.2569 6.6974 11.2569 6.6974Z" fill="#A47FCC"/>
<path d="M6.66972 6.9986C6.42599 7.16734 6.1635 7.55357 6.46848 8.04729C6.74722 8.49727 7.33969 8.35353 7.50968 8.18353C7.72342 7.9698 7.80841 7.55607 7.58593 7.19859C7.36344 6.84111 6.89721 6.83986 6.66972 6.9986Z" fill="#FFA6A3"/>
<path d="M6.94471 7.32231C6.81596 7.35481 6.69347 7.53855 6.75846 7.73604C6.81471 7.90978 7.01845 7.98103 7.14844 7.91853C7.28594 7.85228 7.37343 7.66729 7.29594 7.5023C7.23219 7.36356 7.09345 7.28481 6.94471 7.32231Z" fill="#F182A7"/>
<defs>
<radialGradient id="paint0_radial_715_295" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(2.61485 7.97649) scale(2.18616)">
<stop offset="0.6623" stop-color="#FFA8A6"/>
<stop offset="0.9142" stop-color="#FFA8A6" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,17 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.02217 9.55581L1.83744 9.7433C1.83744 9.7433 1.50871 9.41456 1.55621 9.29707C1.6037 9.17958 5.89723 5.82475 6.06097 5.8485C6.22471 5.87225 6.60094 6.27098 6.90592 6.12974C7.21091 5.98849 6.90592 5.16729 6.99967 5.05104C7.09341 4.93355 10.4482 3.12739 10.6595 3.03365C10.8707 2.9399 11.0582 2.72867 11.4807 2.82241C11.9032 2.91616 12.9594 3.10365 14.108 3.8786C15.2567 4.65356 16.4542 5.54477 16.3604 6.03724C16.2667 6.52972 7.02217 9.55581 7.02217 9.55581Z" fill="#FFE265"/>
<path d="M3.43233 15.4455C4.58227 15.3042 15.3042 13.0056 15.7504 12.9356C16.1967 12.8656 16.4779 12.4431 16.4779 11.9732C16.4779 11.5032 16.3604 6.03723 16.3604 6.03723C16.3604 6.03723 15.8442 6.20097 15.2342 6.34222C14.6242 6.48221 9.62576 7.70214 7.65586 8.14837C5.68596 8.5946 2.54113 9.4158 2.18865 9.4158C1.83617 9.4158 1.55493 9.29831 1.55493 9.29831C1.55493 9.29831 1.64868 14.1781 1.67243 14.5305C1.69617 14.883 1.85992 15.3042 2.25864 15.4217C2.65737 15.5392 2.91736 15.508 3.43233 15.4455Z" fill="#FEB502"/>
<path d="M10.3708 4.19483C10.3708 4.49482 10.6758 4.69356 11.1808 4.67856C11.732 4.66231 11.8845 4.33483 11.867 4.12484C11.8495 3.9136 11.5958 3.72861 11.0933 3.73736C10.6271 3.74611 10.3708 3.9311 10.3708 4.19483Z" fill="#FF8B0D"/>
<path d="M4.73988 7.93458C3.92742 8.20832 4.01867 8.91953 4.25616 9.07827C4.49364 9.23701 4.80988 9.22826 5.17986 9.09577C5.54984 8.96328 5.78483 8.81203 5.82232 8.56455C5.85732 8.32956 5.54984 7.66084 4.73988 7.93458Z" fill="#ED6B31"/>
<path d="M12.122 5.90225C11.5771 5.92725 10.9746 6.31848 10.7496 6.69471C10.5121 7.09094 10.4946 7.55716 11.0396 7.84715C11.5846 8.13713 13.512 7.39842 13.4157 6.72096C13.3195 6.04224 12.6945 5.876 12.122 5.90225Z" fill="#ED6B31"/>
<path d="M10.2307 11.3832C10.2307 11.3832 11.0045 10.8107 11.0045 10.407C11.0045 10.002 10.6707 9.04583 9.25329 8.97334C7.92461 8.90459 7.10591 10.7157 7.97711 11.6482C8.84832 12.5806 9.37579 12.1582 9.70202 12.0707C10.0283 11.9819 10.2307 11.3832 10.2307 11.3832Z" fill="#ED6B31"/>
<path d="M6.40339 13.0106C6.40339 13.0106 7.17209 12.5294 6.58213 12.0019C6.16715 11.6307 5.61218 11.7982 5.32969 12.0869C4.96846 12.4569 5.06596 13.0456 5.39969 13.3093C5.73467 13.5743 6.40339 13.0106 6.40339 13.0106Z" fill="#ED6B31"/>
<path d="M3.79857 10.8469C3.79857 10.8469 3.71108 10.5707 3.39734 10.4594C3.04486 10.3345 2.51864 10.7732 2.74363 11.2419C2.94612 11.6644 3.39484 11.5406 3.39484 11.5406L3.79857 10.8469Z" fill="#ED6B31"/>
<path d="M15.0518 11.2507C15.1281 11.1682 15.6268 10.8083 15.6268 10.5345C15.6268 10.262 14.8756 9.41958 13.9169 10.0533C12.9582 10.687 13.4594 11.6457 13.7232 11.8045C13.9869 11.9632 14.7181 11.612 15.0518 11.2507Z" fill="#ED6B31"/>
<path d="M4.85735 8.36585C4.32363 8.55084 4.12239 8.92832 4.25613 9.07831C4.39737 9.23705 4.70486 9.34205 5.24108 9.14831C5.65606 8.99831 5.86605 8.74708 5.83105 8.52334C5.80605 8.36085 5.34108 8.19836 4.85735 8.36585Z" fill="#FF8B0D"/>
<path d="M3.07736 10.8282C2.84362 11.1207 3.03361 11.4532 3.2536 11.5319C3.47359 11.6107 3.86106 11.3732 3.83482 10.9782C3.80732 10.582 3.2886 10.5645 3.07736 10.8282Z" fill="#FF8B0D"/>
<path d="M5.79599 12.4219C5.29352 12.8294 5.27977 13.2281 5.47351 13.3631C5.6085 13.4581 6.10597 13.6556 6.56095 13.3194C6.89843 13.0706 6.90468 12.5482 6.79844 12.2982C6.70969 12.0857 6.21847 12.0782 5.79599 12.4219Z" fill="#FF8B0D"/>
<path d="M8.84581 10.2007C8.17334 10.8544 8.01335 11.9156 8.67332 12.1656C9.25454 12.3856 10.0895 12.2531 10.582 11.6469C11.0994 11.0094 11.1544 10.3057 10.822 9.89072C10.4245 9.392 9.36578 9.69698 8.84581 10.2007Z" fill="#FF8B0D"/>
<path d="M11.9019 6.55347C11.022 6.85221 10.5033 7.48592 10.952 7.80341C11.4007 8.11964 11.8669 8.07339 12.3769 7.89965C12.9556 7.70341 13.3981 7.24844 13.4156 6.81721C13.4294 6.46348 12.6769 6.28974 11.9019 6.55347Z" fill="#FF8B0D"/>
<path d="M14.2518 10.5995C13.6531 11.087 13.4781 11.5757 13.7243 11.8044C13.9706 12.0332 14.5606 12.1382 15.1055 11.7607C15.6717 11.3682 15.713 10.9232 15.6505 10.5645C15.5805 10.1608 14.8668 10.0983 14.2518 10.5995Z" fill="#FF8B0D"/>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,42 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.99861 5.46985L2.46614 5.59234C2.46614 5.59234 1.72742 6.73228 1.56618 7.94472C1.46494 8.70593 1.46119 9.69962 1.53119 10.1946C1.60243 10.6896 1.85742 11.7433 2.25865 12.4857C2.64488 13.2007 3.02111 13.4819 3.43233 13.7307C3.60858 13.8369 3.76607 14.2006 4.04105 14.3769C4.29854 14.5419 4.63852 14.5744 4.63852 14.5744C4.63852 14.5744 5.57847 16.0505 7.61211 16.3068C9.99824 16.6068 11.1657 15.5293 11.1657 15.5293C11.1657 15.5293 12.1206 15.5268 13.0218 15.1406C14.033 14.7069 14.573 13.8044 14.573 13.8044C14.573 13.8044 15.8392 12.7145 16.2404 11.4095C16.5629 10.3621 16.5716 8.37094 16.3242 7.43349C16.0767 6.49604 15.5979 5.70233 15.218 5.19361C14.9155 4.78863 11.5382 2.99373 11.1307 2.83373C10.7245 2.67499 6.9409 2.42751 6.79966 2.53375C6.65841 2.64125 2.99861 5.46985 2.99861 5.46985Z" fill="#AC5811"/>
<path d="M4.31614 3.46869C3.43494 4.18615 2.97871 4.56113 2.52124 5.45858C2.06376 6.35603 2.21125 7.54722 2.29375 7.9872C2.375 8.42718 2.52249 9.27588 2.60373 9.69961C2.68498 10.1246 2.90122 11.132 3.49994 11.7558C4.33864 12.6295 5.0986 12.9307 5.0986 12.9307C5.0986 12.9307 5.71482 13.8856 7.20349 14.4319C9.16089 15.1493 10.0421 14.1056 10.7921 13.8281C11.542 13.5507 12.4395 13.5344 13.2069 12.9957C13.9731 12.457 14.8218 11.8045 15.3768 10.4346C15.9318 9.06464 15.8168 8.46092 15.833 8.11844C15.8493 7.77596 15.8005 6.12855 15.1643 5.11735C14.5281 4.10615 13.7631 3.49868 13.4682 3.30619C12.8157 2.88247 10.4858 1.86877 9.8996 1.75503C9.31213 1.64128 7.21724 1.81877 6.38603 2.16125C5.55483 2.50374 4.31614 3.46869 4.31614 3.46869Z" fill="url(#paint0_radial_718_89)"/>
<path d="M8.03476 9.93212C7.89601 9.88837 7.74977 9.68213 7.71477 9.2409C7.66602 8.61219 8.12975 8.43845 8.12975 8.43845C8.12975 8.43845 8.68222 8.2922 9.07095 8.15346C9.43218 8.02472 9.69217 7.85973 9.69217 7.85973C9.69217 7.85973 9.72717 7.18601 9.9509 6.98727C10.1759 6.78853 10.5721 6.80603 11.0133 6.85728C11.4533 6.90853 12.162 7.20226 12.0583 7.46975C11.9545 7.73723 11.8333 8.00472 11.4108 8.24721C10.9883 8.48844 10.6859 8.71343 10.4696 8.90342C10.2534 9.09466 9.85716 9.33465 9.20094 9.57714C8.54473 9.81962 8.23474 9.99461 8.03476 9.93212Z" fill="#FFC86E"/>
<path d="M4.31864 4.30862C3.90366 4.22363 3.31744 5.12233 3.2087 5.41981C3.0987 5.7173 2.98621 6.44226 2.97371 6.77349C2.95746 7.18472 2.94996 7.57095 3.12245 7.88468C3.25495 8.12717 3.57618 8.26341 3.86491 8.01343C4.15365 7.76344 4.65487 7.07848 4.78736 6.98223C4.91986 6.88599 5.30734 6.75849 5.15485 6.12853C5.09235 5.87104 4.45863 5.33857 4.42738 5.26607C4.39739 5.19483 4.55363 4.35737 4.31864 4.30862Z" fill="#FFC86E"/>
<path d="M7.25225 3.17991C7.2685 3.42115 7.46599 3.80738 7.67973 4.01611C7.99971 4.32735 9.46713 5.37979 9.84712 5.3198C10.2271 5.2598 10.6408 4.57733 10.7621 4.38734C10.8833 4.19736 11.1245 3.92987 11.2195 3.80863C11.3145 3.68738 11.5645 3.41115 11.3583 3.13491C11.1508 2.85868 10.6333 2.67744 10.2883 2.65119C9.94336 2.62494 9.40714 2.4012 8.94966 2.4187C8.49219 2.4362 7.20975 2.55744 7.25225 3.17991Z" fill="#FFC86E"/>
<path d="M13.292 6.39853C13.3208 6.66102 13.6895 6.71851 13.9832 6.9085C14.277 7.09849 14.4757 7.29723 14.6482 7.24474C14.8207 7.19349 15.0557 6.91725 15.0019 6.43353C14.9332 5.81231 14.5444 5.01735 14.0007 5.0436C13.672 5.05985 13.717 5.63107 13.717 5.63107C13.717 5.63107 13.2745 6.24354 13.292 6.39853Z" fill="#FFC86E"/>
<path d="M6.1836 4.77845C6.1836 4.77845 5.00491 4.77095 5.02616 4.88844C5.04866 5.00594 5.15116 5.39342 5.45864 5.40842C5.76612 5.42341 6.16235 5.45216 6.35234 5.39342C6.54233 5.33467 7.06981 4.90969 7.06981 4.7197C7.06981 4.52971 6.85732 4.25098 6.85732 4.25098L6.1836 4.77845Z" fill="#CF701E"/>
<path d="M7.86715 6.93968C7.86715 6.93968 7.8534 6.94842 7.8334 6.96467C7.77466 7.01342 7.66216 7.12717 7.70591 7.25466C7.76466 7.4234 8.10839 7.73838 8.44587 7.73838C8.78335 7.73838 8.96584 7.4234 9.02459 7.21841C9.08334 7.01342 9.05334 6.88843 9.05334 6.88843L7.86715 6.93968Z" fill="#CF701E"/>
<path d="M5.23871 7.89969C5.23871 7.89969 5.03372 7.97344 5.04122 8.10468C5.04872 8.23593 5.09996 8.47841 5.3637 8.73465C5.62994 8.99339 6.01616 9.06463 6.3674 8.96964C6.71863 8.87464 6.97486 8.61091 6.94612 8.25967C6.91737 7.90844 6.57989 7.63721 6.57989 7.63721L5.23871 7.89969Z" fill="#CF701E"/>
<path d="M3.72254 9.8409C3.72254 9.8409 3.50255 9.89215 3.52505 10.0384C3.54755 10.1846 3.79629 10.6459 4.19127 10.5946C4.58625 10.5434 5.20996 10.0896 5.20996 9.9134C5.20996 9.73716 4.89498 9.50342 4.89498 9.50342L3.72254 9.8409Z" fill="#CF701E"/>
<path d="M6.41717 11.6719C6.41717 11.6719 6.33592 12.1444 6.97339 12.3168C8.03584 12.6031 8.14583 11.9581 8.14583 11.9581L6.41717 11.6719Z" fill="#CF701E"/>
<path d="M10.5908 11.8906C10.5908 11.8906 10.3058 12.1756 10.6295 12.3993C10.9532 12.6231 11.322 12.6993 11.6695 12.6606C12.0169 12.6218 12.2544 12.3444 12.2544 12.3444L10.5908 11.8906Z" fill="#CF701E"/>
<path d="M13.2507 8.72583C13.2507 8.72583 12.7133 8.99707 13.1295 9.46579C13.4457 9.82327 14.1694 9.75078 14.4657 9.54954C14.6594 9.41829 14.7169 9.18581 14.6444 9.06831C14.5707 8.95082 13.2507 8.72583 13.2507 8.72583Z" fill="#CF701E"/>
<path d="M11.1708 5.62103C11.1708 5.62103 11.142 6.23474 11.9695 6.21475C13.1269 6.186 12.9807 5.46729 12.9807 5.46729L11.1708 5.62103Z" fill="#CF701E"/>
<path d="M11.7058 4.85228C11.7058 4.85228 11.1633 5.18976 11.1046 5.32851C11.0459 5.46725 10.9209 5.90722 11.5508 5.87848C12.1808 5.84973 12.8108 5.82723 13.0232 5.57099C13.2357 5.31476 13.1845 5.07977 13.097 4.88978C13.0095 4.69979 12.517 3.98108 12.1258 3.98983C11.8696 3.99483 11.7058 4.85228 11.7058 4.85228Z" fill="#593329"/>
<path d="M12.1233 3.98744C12.1233 3.98744 11.7795 3.94369 11.4783 4.47866C11.1783 5.01363 11.1008 5.33237 11.1008 5.33237C11.1008 5.33237 11.712 5.33487 12.0783 5.29112C12.4445 5.24737 12.6932 5.01988 12.6932 5.01988C12.6932 5.01988 12.3945 4.05368 12.1233 3.98744Z" fill="#925849"/>
<path d="M7.96335 6.48593C7.96335 6.48593 7.66336 6.46593 7.64087 6.58217C7.61837 6.69967 7.68461 6.9984 7.78711 7.12965C7.8896 7.26089 8.19709 7.55462 8.50457 7.49588C8.81205 7.43713 8.99454 7.1359 9.04579 7.03215C9.09204 6.93466 9.14204 6.82966 9.05454 6.69842C8.9658 6.56593 7.96335 6.48593 7.96335 6.48593Z" fill="#593329"/>
<path d="M8.40971 6.01723C8.15473 5.98973 7.53976 6.47845 7.64975 6.64595C7.77225 6.82969 7.7935 6.71344 8.13098 6.88218C8.40971 7.02093 8.53096 7.14967 8.70595 7.12717C8.88094 7.10467 9.08968 6.81594 9.05343 6.69844C9.01718 6.58095 8.7547 6.05348 8.40971 6.01723Z" fill="#925849"/>
<path d="M5.52366 4.03867L4.9237 4.45364C4.9237 4.45364 4.9012 4.73488 5.02619 4.88862C5.15118 5.04236 5.18743 5.13736 5.77365 5.13736C6.35987 5.13736 6.90859 5.05736 7.01109 4.63988C7.11358 4.22241 6.93359 3.95492 6.8236 3.80118C6.7136 3.64744 5.52366 4.03867 5.52366 4.03867Z" fill="#593329"/>
<path d="M5.53104 3.38745C5.39604 3.44494 5.07731 3.87867 5.00357 4.06866C4.92982 4.25865 4.91357 4.46114 4.92357 4.46364C5.04481 4.49364 5.75853 4.55989 6.07351 4.50864C6.38849 4.45739 6.64473 4.31865 6.74722 4.13491C6.82847 3.98867 6.82222 3.79493 6.82222 3.79493C6.82222 3.79493 6.60848 3.40995 6.31475 3.29245C6.02226 3.17496 5.58228 3.36495 5.53104 3.38745Z" fill="#925949"/>
<path d="M5.02853 7.92721C5.02853 7.92721 4.97853 8.00346 5.07353 8.1497C5.16852 8.29594 5.35476 8.47093 5.6335 8.55218C5.91223 8.63218 6.10972 8.74217 6.29221 8.72092C6.47595 8.69842 6.83468 8.55218 6.84843 8.04721C6.86343 7.54223 6.76094 7.30725 6.54095 7.08726C6.32096 6.86727 6.16222 6.90477 6.16222 6.90477L5.02853 7.92721Z" fill="#593329"/>
<path d="M5.09246 7.45956C5.01496 7.58955 4.95746 8.01203 5.05246 8.10702C5.14745 8.20202 5.71867 8.20827 5.86866 8.19202C6.01991 8.17577 6.51363 7.94328 6.52113 7.76704C6.52863 7.5908 6.43363 7.35706 6.43363 7.26957C6.43363 7.18207 6.54363 6.97708 6.24364 6.90334C5.94366 6.82959 5.25245 7.18832 5.09246 7.45956Z" fill="#925849"/>
<path d="M4.1024 9.357C4.1024 9.357 3.63117 9.47324 3.60118 9.63449C3.57243 9.79573 3.63992 9.95822 3.80116 10.112C3.96241 10.2657 4.0799 10.4269 4.20364 10.4119C4.32864 10.3969 4.65737 10.1045 4.75986 10.0095C4.86236 9.91447 5.0086 9.74573 4.99485 9.60699C4.9811 9.46824 4.92111 9.32825 4.78986 9.21826C4.65987 9.10826 4.1024 9.357 4.1024 9.357Z" fill="#593329"/>
<path d="M4.2199 8.9396C3.99991 8.9446 3.83117 9.21083 3.75867 9.30583C3.68492 9.40082 3.56118 9.62831 3.61243 9.66456C3.66368 9.70081 3.87616 9.72331 3.97866 9.78955C4.08115 9.8558 4.2349 9.95829 4.35239 9.8633C4.46988 9.7683 4.57238 9.50457 4.63112 9.41707C4.68987 9.32958 4.79237 9.21958 4.79237 9.21958C4.79237 9.21958 4.57113 8.9321 4.2199 8.9396Z" fill="#925849"/>
<path d="M6.9084 10.7044C6.86465 10.7556 6.24219 11.2606 6.24219 11.2606C6.24219 11.2606 6.26469 11.7006 6.40343 11.8031C6.54217 11.9056 6.97465 12.0518 7.25338 12.1106C7.53212 12.1693 8.02959 12.2356 8.15459 12.1843C8.27958 12.1331 8.41082 11.8618 8.41832 11.5106C8.42582 11.1594 8.30083 10.8969 8.16209 10.7356C8.02334 10.5744 7.9121 10.5144 7.9121 10.5144L6.9084 10.7044Z" fill="#593329"/>
<path d="M7.73588 11.3633C7.81712 11.272 7.91212 10.5133 7.91212 10.5133C7.91212 10.5133 7.3484 10.0808 7.11341 10.0521C6.87842 10.0233 6.60094 10.0446 6.51969 10.1321C6.43969 10.2196 6.12471 10.5346 6.15346 10.7395C6.18221 10.9445 6.24095 11.2595 6.24095 11.2595C6.24095 11.2595 7.56713 11.5545 7.73588 11.3633Z" fill="#925849"/>
<path d="M10.4684 11.7244C10.4684 11.7244 10.3971 12.1994 10.9196 12.2894C11.4421 12.3781 11.8758 12.4969 12.102 12.4494C12.3283 12.4019 12.5532 12.0994 12.577 11.8794C12.6007 11.6594 12.6132 11.3419 12.577 11.2557C12.482 11.027 12.042 10.4007 11.8283 10.3345C11.7083 10.2982 11.5946 10.3182 11.5946 10.3182L10.4684 11.7244Z" fill="#593329"/>
<path d="M10.807 10.892C10.5732 11.1507 10.4532 11.5245 10.4657 11.722C10.4782 11.9082 11.3644 11.827 11.4694 11.7632C11.5744 11.6995 11.8032 11.4145 11.8107 11.3195C11.8182 11.2245 11.7457 10.927 11.7332 10.7433C11.7207 10.5595 11.7157 10.3095 11.6144 10.3158C11.5132 10.322 11.1394 10.5245 10.807 10.892Z" fill="#925849"/>
<path d="M13.8043 7.66724C13.8043 7.66724 14.0105 7.69723 14.2643 7.84973C14.5605 8.02722 14.8705 8.47969 14.8467 8.84842C14.823 9.21715 14.5017 9.3834 14.1105 9.38965C13.7193 9.3959 13.3368 9.37465 13.2656 9.17216C13.1943 8.96967 13.8043 7.66724 13.8043 7.66724Z" fill="#593329"/>
<path d="M13.4919 7.79085C13.4306 7.87584 13.2719 8.13583 13.2181 8.46831C13.1644 8.80079 13.2006 9.18077 13.3194 9.24702C13.4381 9.31202 14.0918 8.7483 14.1268 8.6883C14.1631 8.6283 13.9181 7.7196 13.8406 7.67835C13.7631 7.63711 13.5694 7.6846 13.4919 7.79085Z" fill="#925849"/>
<defs>
<radialGradient id="paint0_radial_718_89" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(9.02019 8.19924) scale(6.6499 6.6499)">
<stop offset="0.507" stop-color="#F29F58"/>
<stop offset="0.7151" stop-color="#F09D56"/>
<stop offset="0.8262" stop-color="#EB9550"/>
<stop offset="0.9141" stop-color="#E18745"/>
<stop offset="0.9898" stop-color="#D47436"/>
<stop offset="1" stop-color="#D27133"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,14 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.8981 13.9368H10.3171V14.803H15.8981V13.9368Z" fill="#AF0C1A"/>
<path d="M9.64575 14.5493V15.213L16.4791 15.2205C16.4791 15.2205 16.4941 14.5781 16.4716 14.5781C16.4504 14.5793 9.64575 14.5493 9.64575 14.5493Z" fill="#DC0D2A"/>
<path d="M9.64575 13.3882L9.66075 14.0594C9.66075 14.0594 16.4579 14.0956 16.4654 14.0744C16.4729 14.0531 16.4804 13.4394 16.4504 13.4244C16.4216 13.4094 9.64575 13.3882 9.64575 13.3882Z" fill="#DC0D2A"/>
<path d="M2.10986 6.98474C2.10986 6.98474 2.10986 9.24213 2.10986 9.31837C2.10986 9.39462 2.16236 9.46211 2.26111 9.46961C2.35985 9.47711 3.65853 9.45461 3.73353 9.46211C3.80852 9.46961 3.89227 9.40212 3.89227 9.26587C3.89227 9.12963 3.89227 6.99349 3.89227 6.99349C3.89227 6.99349 3.43979 6.65351 3.38605 6.66101C3.3323 6.66851 2.10986 6.98474 2.10986 6.98474Z" fill="#858585"/>
<path d="M2.66233 7.52465C2.55734 7.53715 2.51109 7.59964 2.51109 7.72089C2.51109 7.84213 2.49609 8.65709 2.49609 8.73958C2.49609 8.82208 2.50359 8.90582 2.62484 8.91332C2.74608 8.92082 3.2898 8.92832 3.3573 8.92082C3.42479 8.91332 3.47854 8.83833 3.48604 8.73958C3.49354 8.64084 3.48604 7.75838 3.48604 7.68964C3.48604 7.56839 3.40355 7.5309 3.2973 7.5309C3.19106 7.5309 2.78233 7.50965 2.66233 7.52465Z" fill="#B0B0B0"/>
<path d="M13.7444 7.6571L13.7594 6.87964L11.932 6.88714L11.917 7.6196L12.3695 8.11082H12.6382C12.6282 8.83704 12.5957 11.1157 12.5957 11.1157C12.5957 11.1157 12.4932 11.2619 12.377 11.3719C12.2257 11.5156 11.977 11.7044 11.9995 12.1719C12.0207 12.6018 12.4445 12.9268 12.8607 12.9268C13.3057 12.9268 13.6082 12.6018 13.6232 12.2931C13.6382 11.9831 13.5169 11.9706 13.4494 11.9606C13.3432 11.9456 13.2607 12.0206 13.2382 12.1569C13.2157 12.2931 13.0794 12.4668 12.8457 12.4131C12.612 12.3594 12.4607 12.1031 12.6495 11.8544C12.7844 11.6756 13.0419 11.6206 13.0644 11.4694C13.0844 11.3394 13.0694 8.79954 13.0657 8.10957H13.3444L13.7444 7.6571Z" fill="#858587"/>
<path d="M11.4569 13.2582C11.3131 13.3245 11.3331 13.4382 11.3344 13.5545C11.3394 14.0619 11.3631 15.2144 11.3631 15.2144C11.3631 15.2144 11.3969 15.3806 11.6156 15.3806C11.8769 15.3806 11.8969 15.2069 11.8969 15.2069V13.482L12.8831 12.6645L14.1917 13.6257C14.1917 13.6257 14.228 15.1194 14.228 15.2056C14.228 15.2919 14.3142 15.4081 14.5167 15.4006C14.748 15.3919 14.7767 15.2056 14.7767 15.2056C14.7767 15.2056 14.768 13.6057 14.7605 13.4732C14.748 13.2757 14.4517 13.2795 14.4517 13.2795C14.4517 13.2795 13.008 12.1308 12.8781 12.1245C12.7343 12.1183 11.5569 13.212 11.4569 13.2582Z" fill="#FF510F"/>
<path d="M12.3945 9.602C12.3008 9.617 12.2795 9.68199 12.2795 9.76824C12.2795 9.85448 12.2808 10.7119 12.2808 10.7694C12.2808 10.8269 12.2808 10.8844 12.3608 10.8919C12.4408 10.8994 13.2632 10.8994 13.3057 10.8994C13.3707 10.8994 13.407 10.8194 13.407 10.7194C13.407 10.6182 13.412 9.77449 13.407 9.71699C13.3995 9.62325 13.3132 9.602 13.2407 9.5945C13.1682 9.58575 12.4595 9.592 12.3945 9.602Z" fill="#F1901B"/>
<path d="M1.77368 4.73616L6.02971 2.70752C6.02971 2.70752 7.45838 3.025 10.242 3.70872C13.4831 4.50492 14.1368 4.79491 14.1368 4.79491L13.1606 5.06365L12.3669 4.77116L6.04221 3.25749L2.64739 4.83241L1.90242 4.9424L1.77368 4.73616Z" fill="#858585"/>
<path d="M16.5492 6.64477C16.4817 6.57352 14.5593 4.68862 14.4668 4.68862C14.3743 4.68862 7.51213 4.68862 7.51213 4.68862C7.51213 4.68862 6.39844 3.01871 6.25595 2.81247C6.09596 2.57998 5.95222 2.56749 5.80722 2.80622C5.74848 2.90372 4.67728 4.67612 4.67728 4.67612C4.67728 4.67612 1.85243 4.71237 1.68994 4.69987C1.52745 4.68862 1.4812 4.76987 1.4812 4.83862C1.4812 4.90861 1.4812 7.1185 1.5162 7.141C1.5512 7.16349 4.38605 7.15224 4.38605 7.15224L4.3623 15.5768C4.3623 15.5768 4.10981 15.5818 3.93607 15.5818C3.76233 15.5818 3.66609 15.5843 3.66734 15.7168C3.66734 15.7868 3.66484 16.0868 3.66734 16.1918C3.67234 16.3918 3.65109 16.4943 3.78358 16.493C3.89982 16.4918 7.84587 16.493 8.06585 16.493C8.28584 16.493 8.28584 16.3543 8.28584 16.2843C8.28584 16.2143 8.28584 15.8443 8.28584 15.7293C8.28584 15.6143 8.1821 15.5668 8.10085 15.5668C8.01961 15.5668 7.60338 15.5668 7.60338 15.5668V9.16714C7.60338 9.16714 8.0321 9.19089 8.49458 9.12089C8.95706 9.05089 9.10705 8.53842 9.18829 8.16844C9.26954 7.79846 9.24704 7.1235 9.24704 7.1235C9.24704 7.1235 16.4092 7.14224 16.5604 7.08475C16.7104 7.026 16.7554 6.86601 16.5492 6.64477ZM2.16616 5.27609H3.00362L2.16616 6.21104C2.16616 5.92481 2.16616 5.26859 2.16616 5.27609ZM2.62364 6.34229L3.26861 5.61108L4.04107 6.34229H2.62364ZM4.42355 6.13605L3.55484 5.27609H4.42355V6.13605ZM6.87842 13.0944L5.99846 13.9844L5.11351 13.0944H6.87842ZM5.33225 12.6782L6.02471 11.9632L6.73592 12.6695L5.33225 12.6782ZM5.97221 8.59342L4.97477 7.65472H6.89342L5.97221 8.59342ZM7.10215 8.10844V9.65836L6.30595 8.9084L7.10215 8.10844ZM6.79092 10.0646H5.15726L5.96596 9.25088L6.79092 10.0646ZM7.10215 10.7933V12.3657L6.33095 11.6145L7.10215 10.7933ZM6.74842 10.4733L5.98721 11.3083L5.17601 10.4883L6.74842 10.4733ZM7.1659 6.60477H5.40224L7.16715 5.58233V6.60477H7.1659ZM6.01346 3.46369C6.01346 3.46369 6.80842 4.61738 6.78842 4.65613C6.77842 4.67612 6.37344 4.66113 5.98846 4.65863C5.62598 4.65738 5.281 4.66988 5.27725 4.64613C5.26725 4.59863 6.01346 3.46369 6.01346 3.46369ZM4.96602 5.26734H6.75842L4.95852 6.35354L4.96602 5.26734ZM4.88977 8.18844L5.64098 8.9309L4.88977 9.69586V8.18844ZM4.88977 10.842L5.66223 11.6208L4.89477 12.4307L4.88977 10.842ZM4.88977 13.5457L5.66598 14.3219L4.88977 15.1068V13.5457ZM5.11351 15.5856L6.00346 14.6593L6.91216 15.5681C6.50594 15.5668 5.54974 15.5793 5.11351 15.5856ZM7.10215 15.0956L6.3297 14.3181L7.10215 13.5144V15.0956ZM7.63213 6.60477L7.63963 5.49358L8.69707 6.60727C8.05085 6.60477 7.63213 6.60477 7.63213 6.60477ZM9.25829 6.51478L8.00586 5.2361L10.527 5.2286L9.25829 6.51478ZM9.82701 6.60977L11.0482 5.41484L12.3231 6.61602C11.4832 6.61352 10.5982 6.61102 9.82701 6.60977ZM11.4807 5.2261L14.1893 5.2186L12.8519 6.52728L11.4807 5.2261ZM15.598 6.62352C15.5855 6.62352 14.6768 6.62102 13.4781 6.61852C13.7706 6.32354 14.368 5.71982 14.6143 5.47108C14.9455 5.80732 15.5005 6.37229 15.588 6.45978C15.7142 6.58477 15.6367 6.62352 15.598 6.62352Z" fill="#F3911C"/>
<path d="M8.13965 7.33466L8.14465 8.23837L8.93961 8.83084C8.93961 8.83084 9.1296 8.6096 9.21584 8.11712C9.28834 7.70589 9.24834 7.32341 9.24834 7.32341C9.24834 7.32341 8.15215 7.31966 8.13965 7.33466Z" fill="#A7D0D6"/>
<path d="M7.59208 7.09356C7.59208 7.09356 7.60208 7.92976 7.58208 7.93976C7.56208 7.94976 4.366 7.93226 4.30601 7.91226C4.24601 7.89226 4.24726 7.10356 4.27601 7.09356C4.30476 7.08356 7.59208 7.09356 7.59208 7.09356Z" fill="#D15116"/>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -0,0 +1,15 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.87954 1.94633C5.58971 1.87134 1.96115 4.59994 2.03115 9.01971C2.0924 12.9058 5.01224 15.8468 8.79579 15.9081C12.5793 15.9693 15.9317 13.4682 15.9517 8.99971C15.9729 4.4962 12.4981 2.02883 8.87954 1.94633Z" fill="#FF2A23"/>
<path d="M8.8895 3.52361C6.27964 3.61236 3.64603 5.55975 3.58478 8.99332C3.52354 12.4269 6.52463 14.3393 8.8895 14.3393C11.8918 14.3393 14.4005 11.9744 14.338 8.82833C14.2767 5.6835 11.9118 3.42112 8.8895 3.52361Z" fill="#FFFDFE"/>
<path d="M8.95078 4.86112C6.5859 4.80738 4.8385 6.89727 4.81725 8.89091C4.79725 10.9271 6.07093 13.0032 8.90953 13.1057C11.7481 13.2082 13.0831 10.9471 13.1243 9.03465C13.1668 7.12225 11.6444 4.92237 8.95078 4.86112Z" fill="#FF2A23"/>
<path d="M6.24718 8.9658C6.22718 10.7545 7.50086 11.7832 9.04328 11.7832C10.4445 11.7832 11.8194 10.8995 11.8194 9.0283C11.8194 7.34214 10.5445 6.33469 9.12578 6.27345C7.7071 6.21095 6.26718 7.23964 6.24718 8.9658Z" fill="white"/>
<path d="M7.51208 8.95208C7.43583 10.0183 8.30328 10.5283 9.08449 10.5283C9.82445 10.5283 10.5544 10.042 10.5757 9.05458C10.5944 8.12713 9.86695 7.56091 9.08449 7.53341C8.27704 7.50591 7.57332 8.08588 7.51208 8.95208Z" fill="#FB2B22"/>
<path d="M9.34201 9.03589L8.92328 9.26838L9.72199 10.4046C9.72199 10.4046 10.3395 10.4521 10.4819 10.2908C10.6244 10.1296 10.4632 9.65461 10.4632 9.65461L9.34201 9.03589Z" fill="#CC1935"/>
<path d="M11.9268 7.80095C11.8143 7.56846 11.6993 7.38222 11.3944 7.43972C11.0906 7.49722 10.5294 7.8197 10.4244 7.97219C10.3282 8.11219 10.2344 8.28593 10.2344 8.28593C10.2344 8.28593 9.15947 8.8734 8.98948 8.96964C8.72325 9.12213 8.90699 9.43212 9.09448 9.39712C9.35071 9.34962 10.4957 8.8834 10.4957 8.8834C10.4957 8.8834 10.7381 8.94089 10.8806 8.94089C11.0231 8.94089 11.8356 8.53966 11.9456 8.46592C12.1268 8.34217 12.0406 8.03844 11.9268 7.80095Z" fill="#ACB1B5"/>
<path d="M10.4594 9.6521C10.4594 9.6521 10.3994 9.86459 10.1807 10.0846C9.95322 10.3121 9.71948 10.4021 9.71948 10.4021L10.4407 11.4595C10.4407 11.4595 10.9769 11.4983 11.3669 11.1183C11.7569 10.7383 11.5869 10.2408 11.5869 10.2408L10.4594 9.6521Z" fill="#C8C8C8"/>
<path d="M11.1744 12.5706L10.4395 11.4582C10.4395 11.4582 10.5619 11.3932 10.7232 11.2832C11.0194 11.0807 11.1844 10.9195 11.3957 10.5895C11.5106 10.412 11.5844 10.2395 11.5844 10.2395L12.7206 10.8007C12.7206 10.8007 12.6956 11.6782 12.3244 12.1057C11.9531 12.5331 11.1744 12.5706 11.1744 12.5706Z" fill="#CC1935"/>
<path d="M11.1744 12.5707L11.8056 13.5169C11.8056 13.5169 12.6293 13.5982 13.3043 12.7895C13.9792 11.9807 13.7992 11.3158 13.7992 11.3158L12.7205 10.7996C12.7205 10.7996 12.5106 11.3045 12.0931 11.7895C11.6756 12.2745 11.1744 12.5707 11.1744 12.5707Z" fill="#C8C8C8"/>
<path d="M12.7405 14.9157L11.8068 13.5182C11.8068 13.5182 12.5105 13.107 13.0105 12.5158C13.5292 11.9021 13.8005 11.3184 13.8005 11.3184L15.3116 12.0696C15.3116 12.0696 14.9717 12.9483 14.2179 13.742C13.4805 14.5157 12.7405 14.9157 12.7405 14.9157Z" fill="#CC1935"/>
<path d="M11.2006 8.17093C11.3419 8.43466 11.5756 8.43716 11.8231 8.33217C12.0706 8.22717 13.2393 7.40097 13.2393 7.40097C13.2393 7.40097 14.8267 8.19968 15.8529 7.7997C16.8791 7.40097 16.8503 6.31728 16.8503 6.31728L16.2429 5.9323C16.2429 5.9323 16.6854 5.70231 16.8278 5.61731C16.8978 5.57481 17.0016 5.41732 16.9441 5.27608C16.8991 5.16609 16.7266 5.12859 16.5841 5.18609C16.4416 5.24358 16.0304 5.37233 16.0304 5.37233C16.0304 5.37233 16.1941 4.69111 16.1654 4.65361C16.1366 4.61612 15.8416 4.46362 15.8416 4.46362C15.8416 4.46362 14.8817 4.46362 14.758 4.51112C14.6342 4.55862 13.6843 5.49982 13.6843 5.49982L13.228 6.43102L13.033 6.846C13.033 6.846 11.7019 7.44222 11.5019 7.52721C11.3031 7.61346 11.0269 7.84969 11.2006 8.17093Z" fill="#006CA9"/>
<path d="M13.3168 6.73108C13.3168 6.73108 13.8768 6.3261 14.2755 6.12611C14.6742 5.92612 15.4142 5.68239 15.6329 5.65614C15.8517 5.62989 15.9417 5.68864 15.9804 5.79113C16.0192 5.89363 15.8904 6.02237 15.7804 6.08112C15.6717 6.13861 15.0917 6.40235 14.4492 6.74358C13.8068 7.08481 13.5018 7.3348 13.2418 7.39355C13.1106 7.42355 12.9418 7.30605 12.9318 7.10981C12.9243 6.95732 12.9543 6.82983 13.0543 6.53734C13.1768 6.17736 13.4455 5.63614 13.6718 5.28366C14.0068 4.76244 14.4892 4.40871 15.023 4.31246C15.5567 4.21622 15.9942 4.35121 16.1042 4.4412C16.2129 4.5312 16.2066 4.62369 16.1679 4.65494C16.1292 4.68619 16.0717 4.71119 15.7817 4.60869C15.5804 4.53745 14.9392 4.53745 14.4892 4.87243C14.0343 5.20991 13.923 5.41865 13.7243 5.78613C13.5255 6.15361 13.3168 6.73108 13.3168 6.73108Z" fill="#1F87FD"/>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -0,0 +1,21 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.76871 13.4269C1.76871 12.2208 5.00604 11.5608 8.99958 11.5608C12.9931 11.5608 16.2304 12.2208 16.2304 13.4269C16.2304 15.1244 12.9931 16.4993 8.99958 16.4993C5.00604 16.4993 1.76871 15.1244 1.76871 13.4269Z" fill="#94D1E0"/>
<path d="M2.39368 7.33472V13.0282H2.39743C2.48117 14.5093 5.38727 15.6993 8.96208 15.6993C12.5369 15.6993 15.443 14.5093 15.5267 13.0282H15.5305V7.33472H2.39368Z" fill="#E63F33"/>
<path d="M3.92859 10.4544C5.28977 11.0219 7.07718 11.3344 8.96208 11.3344C10.847 11.3344 12.6394 11.0331 13.9956 10.4544C15.2142 9.93445 15.338 9.58947 15.5305 9.39073V8.63452C15.1743 9.107 14.573 9.53072 13.7518 9.87196C12.4656 10.4082 10.7645 10.7032 8.96083 10.7032C7.15717 10.7032 5.45601 10.4082 4.16983 9.87196C3.34987 9.52947 2.74866 9.107 2.39117 8.63452V9.55572L3.02239 10.0007C3.29488 10.1644 3.59611 10.3157 3.92859 10.4544Z" fill="#BDBDBD"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.39345 10.3438C6.53472 10.3438 6.64924 10.4583 6.64924 10.5995V15.6605C6.64924 15.8018 6.53472 15.9163 6.39345 15.9163C6.25218 15.9163 6.13766 15.8018 6.13766 15.6605V10.5995C6.13766 10.4583 6.25218 10.3438 6.39345 10.3438Z" fill="#94D1E0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5307 10.3438C11.672 10.3438 11.7865 10.4583 11.7865 10.5995V15.6605C11.7865 15.8018 11.672 15.9163 11.5307 15.9163C11.3894 15.9163 11.2749 15.8018 11.2749 15.6605V10.5995C11.2749 10.4583 11.3894 10.3438 11.5307 10.3438Z" fill="#94D1E0"/>
<path d="M6.25098 14.2556C6.13474 14.2131 6.03099 14.1244 5.971 14.0169C5.86475 13.8256 5.8735 13.5806 5.86975 13.3356C5.86225 12.8019 6.00974 12.6694 6.18348 12.6694H6.60221C6.91595 12.7982 6.91595 13.0569 6.91595 13.2307C6.91595 13.4119 6.91595 13.3469 6.91595 13.5281C6.91595 13.7906 6.92969 14.0531 6.67846 14.2194C6.65471 14.2356 6.45972 14.3318 6.25098 14.2556Z" fill="#94D1E0"/>
<path d="M11.6694 14.2556C11.7819 14.2044 11.8807 14.1181 11.9494 14.0169C12.0981 13.7981 12.0469 13.5806 12.0507 13.3356C12.0582 12.8307 11.9107 12.6694 11.7369 12.6694H11.3182C11.0045 12.7982 11.0045 13.0569 11.0045 13.2307C11.0045 13.4119 11.0045 13.3469 11.0045 13.5281C11.0045 13.7906 11.0095 14.0269 11.2419 14.2194C11.3494 14.3081 11.5557 14.3081 11.6694 14.2556Z" fill="#94D1E0"/>
<path d="M16.0405 11.3856C16.028 10.7969 15.9205 10.6957 15.6405 10.7194C15.6405 9.63947 15.6405 8.66577 15.6405 8.66577L15.228 8.92076L15.258 13.8255C15.258 13.8255 15.6392 13.943 15.6392 13.5318C15.6392 13.4105 15.6392 12.938 15.6392 12.3318C16.0467 12.1893 16.068 11.7556 16.0405 11.3856Z" fill="#94D1E0"/>
<path d="M2.68615 8.85704L2.27367 8.60205C2.27367 8.60205 2.27367 9.6145 2.27367 10.7194C1.99369 10.6957 1.88619 10.7969 1.87369 11.3857C1.87369 11.6306 1.83244 11.8481 1.96119 12.0669C2.03493 12.1894 2.14118 12.2931 2.27367 12.3319C2.27367 12.9143 2.27367 13.3655 2.27367 13.483C2.27367 13.8943 2.6549 13.7768 2.6549 13.7768L2.68615 8.85704Z" fill="#94D1E0"/>
<path d="M15.8942 7.56211C15.6905 5.15599 11.3494 4.47727 8.96207 4.48477C6.59595 4.46477 2.05119 5.19598 2.03244 7.6671C0.728759 9.99448 4.86104 11.5519 8.99957 11.5619C13.1519 11.5719 17.4404 9.91699 15.8942 7.56211Z" fill="#94D1E0"/>
<path d="M8.96208 10.3231C12.5897 10.3231 15.5305 9.09754 15.5305 7.58574C15.5305 6.07394 12.5897 4.84839 8.96208 4.84839C5.33445 4.84839 2.39368 6.07394 2.39368 7.58574C2.39368 9.09754 5.33445 10.3231 8.96208 10.3231Z" fill="#FFCC80"/>
<path d="M8.96205 10.3657C12.3108 10.3657 15.0255 9.23473 15.0255 7.83959C15.0255 6.44446 12.3108 5.31348 8.96205 5.31348C5.61331 5.31348 2.89862 6.44446 2.89862 7.83959C2.89862 9.23473 5.61331 10.3657 8.96205 10.3657Z" fill="#FEF6E0"/>
<path d="M7.73839 9.38452L3.36987 6.48217L4.05984 6.14844L7.94213 9.09828L7.73839 9.38452Z" fill="#FFCC80"/>
<path d="M10.2932 9.38452L14.6605 6.48217L13.9718 6.14844L10.0883 9.09828L10.2932 9.38452Z" fill="#FFCC80"/>
<path d="M8.20462 8.83084C8.02838 8.5921 7.74214 8.53836 7.70589 8.47211L7.5634 8.21712C7.51465 8.12963 7.4784 8.07463 7.43841 8.02213L2.48367 1.64247C2.36617 1.48998 2.14994 1.45498 1.98869 1.56248C1.80995 1.68372 1.77371 1.93246 1.9112 2.0987L6.91343 8.39961C6.97968 8.48211 7.05468 8.55711 7.13842 8.62085L7.34841 8.78209C7.40466 8.82459 7.39466 9.09583 7.5934 9.27832C7.75839 9.42956 8.01588 9.48456 8.17962 9.31207C8.30586 9.18082 8.30836 8.97083 8.20462 8.83084Z" fill="#BA793E"/>
<path d="M9.85326 8.83084C10.0295 8.5921 10.3157 8.53836 10.352 8.47211L10.4945 8.21712C10.5432 8.12963 10.5795 8.07463 10.6195 8.02213L15.5742 1.64247C15.693 1.48998 15.9092 1.45498 16.0692 1.56248C16.2479 1.68372 16.2842 1.93246 16.1467 2.0987L11.1444 8.40086C11.0782 8.48336 11.0032 8.55836 10.9195 8.6221L10.7095 8.78334C10.6532 8.82584 10.6632 9.09708 10.4645 9.27957C10.2995 9.43081 10.042 9.48581 9.87826 9.31332C9.75202 9.18082 9.74952 8.97083 9.85326 8.83084Z" fill="#BA793E"/>
<path d="M3.38108 10.0295C2.65487 9.57824 2.32114 9.12202 2.37989 8.97078C2.41738 8.87453 2.55613 8.87453 2.62237 8.95328C3.27859 9.72574 4.62352 10.2695 5.59472 10.4494C5.69596 10.4682 5.77346 10.6569 5.50097 10.6832C5.26473 10.7044 4.33978 10.6257 3.38108 10.0295Z" fill="white"/>
<path d="M11.4494 14.0019C11.3669 14.0356 11.2744 13.9356 11.2357 13.8044C11.1969 13.6731 11.1744 13.2156 11.1969 13.0819C11.2182 12.9494 11.2457 12.8682 11.3494 12.8682C11.4532 12.8682 11.5369 12.9519 11.5369 13.0557C11.5369 13.0557 11.5244 13.5556 11.5307 13.6769C11.5369 13.8006 11.5319 13.9681 11.4494 14.0019Z" fill="white"/>
<path d="M6.30596 14.0019C6.22347 14.0356 6.13097 13.9356 6.09222 13.8044C6.05348 13.6731 6.03098 13.2156 6.05348 13.0819C6.07597 12.9482 6.10222 12.8682 6.20597 12.8682C6.30971 12.8682 6.39346 12.9519 6.39346 13.0557C6.39346 13.0557 6.38096 13.5556 6.38721 13.6769C6.39346 13.8006 6.38846 13.9681 6.30596 14.0019Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -0,0 +1,28 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.68228 8.48206L4.3548 7.77085L3.55359 4.65601L3.34985 4.17104L3.64359 3.44858C3.64359 3.44858 4.05982 3.50732 4.3773 3.86606C4.76353 4.30353 5.3935 5.61221 5.3935 5.61221L5.261 4.57977L5.97971 2.71612C5.97971 2.71612 6.2947 2.68237 6.37844 2.76861C6.52719 2.92111 6.78092 3.99105 6.87092 4.32978C6.96091 4.66851 7.08591 5.13099 7.2434 5.33348C7.40089 5.53597 7.59338 5.58222 7.59338 5.58222L8.1571 3.40483L8.29209 1.42993C8.29209 1.42993 8.48958 1.42868 8.76582 1.48618C8.97456 1.52993 9.11455 1.60117 9.18205 1.68492C9.24954 1.76867 9.25204 3.09985 9.25204 3.09985C9.25204 3.09985 10.4432 2.0849 10.482 2.09615C10.5645 2.11865 10.8207 2.17489 10.9557 2.32239C11.0907 2.46863 10.9782 3.0111 10.877 3.37233C10.7757 3.73356 10.4257 4.90725 10.4257 4.90725L10.742 4.78726L10.812 4.85475L11.4007 3.95105C11.4007 3.95105 11.6132 3.9498 11.7857 4.0348C11.9731 4.12729 12.0856 4.35478 12.0856 4.35478L13.5981 2.16615C13.5981 2.16615 13.7131 2.26864 13.8143 2.36988C13.9155 2.47113 13.9868 2.54238 13.9918 2.64487C13.9968 2.74611 13.6318 3.41983 13.4631 3.75856C13.2943 4.09729 13.0118 4.77476 13.0118 4.77476L14.2418 3.83606C14.2418 3.83606 14.6493 3.89355 14.6618 4.02855C14.673 4.16354 14.4568 4.41353 14.2418 4.78601C14.0268 5.15849 13.6668 5.88095 13.5418 6.12844C13.4168 6.37592 13.5868 6.38842 13.5868 6.38842L15.0605 5.45722C15.0605 5.45722 15.378 5.52222 15.408 5.66971C15.4417 5.83845 15.0767 6.17468 14.8392 6.44467C14.6018 6.71591 14.0943 7.36962 13.8355 7.87834C13.5768 8.38707 13.4031 8.70455 13.4031 8.70455L11.1594 10.9307L5.18976 10.5695L4.68228 8.48206Z" fill="url(#paint0_radial_718_88)"/>
<path d="M12.1458 6.02463C11.9958 6.05963 11.5221 6.87958 11.0796 8.12702C10.8134 8.87698 10.6371 9.69693 10.6471 10.0894C10.6571 10.4819 11.2496 10.3619 11.2496 10.3619L11.5008 9.54694C11.5008 9.54694 11.7821 8.59074 12.1146 7.87703C12.5033 7.03957 13.007 6.5071 12.9695 6.37836C12.9195 6.20587 12.357 5.97463 12.1458 6.02463Z" fill="#FBDE93"/>
<path d="M13.6343 3.89228C13.4243 4.01478 12.9506 4.80724 12.8094 5.10972C12.6681 5.4122 12.6631 5.98092 12.8181 6.05467C13.0093 6.14467 13.2218 5.5022 13.5443 4.96848C13.8668 4.43476 14.248 3.83104 14.248 3.83104C14.248 3.83104 13.8755 3.75104 13.6343 3.89228Z" fill="#FBDE93"/>
<path d="M14.4293 5.4622C14.1081 5.59845 13.5094 6.23716 13.3019 6.5584C12.9194 7.15212 12.6381 7.81583 12.5369 8.08707C12.4369 8.3583 12.3557 8.73078 12.6681 8.55954C12.9806 8.3883 13.3819 7.58334 13.8956 6.85963C14.4905 6.01968 15.063 5.45096 15.063 5.45096C15.063 5.45096 14.6905 5.35096 14.4293 5.4622Z" fill="#FBDE93"/>
<path d="M13.0807 1.87995C12.9569 1.86495 12.6182 2.7649 12.427 3.19738C12.2357 3.62985 11.9807 4.20232 11.9807 4.20232C11.9807 4.20232 11.8333 5.34101 12.0445 5.36101C12.2557 5.38101 12.6695 3.99233 12.9782 3.35612C13.2794 2.73615 13.5969 2.16243 13.5969 2.16243C13.5969 2.16243 13.3219 1.90994 13.0807 1.87995Z" fill="#FBDE93"/>
<path d="M10.8558 4.13481C10.7445 4.25231 10.807 4.85728 10.807 4.85728C10.807 4.85728 11.2395 5.17976 11.3208 5.10851C11.402 5.03727 11.4058 3.94482 11.4058 3.94482C11.4058 3.94482 11.037 3.94357 10.8558 4.13481Z" fill="#FBDE93"/>
<path d="M9.96212 4.95848C9.80587 5.08973 9.91212 5.70469 9.87212 6.25592C9.81212 7.08087 9.57089 8.77078 9.49964 9.26451C9.42839 9.75823 9.3584 10.3007 9.3584 10.3007L10.1334 10.1795C10.1334 10.1795 10.4346 7.87583 10.5558 6.98963C10.6771 6.10342 10.8571 4.84724 10.7371 4.77599C10.6158 4.70725 10.1534 4.79724 9.96212 4.95848Z" fill="#FBDE93"/>
<path d="M9.86827 2.01991C9.67078 2.05741 9.2533 3.09985 9.2533 3.09985C9.2533 3.09985 9.2283 3.85231 9.2283 4.21479C9.2283 4.57727 9.15831 5.00975 9.2783 5.00975C9.39829 5.00975 9.63078 4.31604 9.87202 3.71232C10.1133 3.1086 10.4832 2.09615 10.4832 2.09615C10.4832 2.09615 10.1483 1.96741 9.86827 2.01991Z" fill="#FBDE93"/>
<path d="M7.59727 1.63861C7.46103 1.78235 7.67726 2.15233 7.69726 3.16728C7.71726 4.18347 7.59227 5.5809 7.59227 5.5809C7.59227 5.5809 7.69976 5.5734 7.76726 5.78214C7.82476 5.95838 7.9185 7.02957 8.03849 6.98958C8.15974 6.94958 8.30973 6.23462 8.43097 5.02718C8.55222 3.81974 8.50097 2.88479 8.46097 2.50231C8.42097 2.11983 8.29098 1.42737 8.29098 1.42737C8.29098 1.42737 7.79851 1.42737 7.59727 1.63861Z" fill="#FBDE93"/>
<path d="M5.97981 2.71606C5.97981 2.71606 5.39859 2.75731 5.2386 3.0698C5.07735 3.38228 5.1286 3.53352 5.1586 3.83476C5.1886 4.13599 5.28359 4.73721 5.28359 4.73721C5.28359 4.73721 5.81732 5.40092 6.10855 5.23968C6.39978 5.07969 5.97981 2.71606 5.97981 2.71606Z" fill="#FBDE93"/>
<path d="M3.64363 3.44854C3.64363 3.44854 3.32114 3.38855 3.1299 3.42854C2.93866 3.46854 2.94866 3.64978 3.03991 3.77103C3.1299 3.89227 3.54613 4.64473 3.54613 4.64473C3.54613 4.64473 3.66988 4.64723 3.83112 4.70723C3.99236 4.76722 4.26859 4.91722 4.25734 4.67598C4.23735 4.25475 3.64363 3.44854 3.64363 3.44854Z" fill="#FBDE93"/>
<path d="M4.55231 8.19957C4.55231 8.19957 3.94484 6.97088 3.49236 6.29592C3.03989 5.62095 2.56616 5.09848 2.56616 4.98849C2.56616 4.87724 2.87865 4.76725 3.05864 4.71725C3.23863 4.66726 3.55236 4.65601 3.55236 4.65601C3.55236 4.65601 4.29607 5.91344 4.59855 6.5984C4.90104 7.28337 5.18227 8.08083 5.11228 8.17832C5.04853 8.26582 4.5973 8.20707 4.55231 8.19957Z" fill="#FBDE93"/>
<path d="M4.96121 6.00465C4.91372 6.18089 5.14245 6.98085 5.2837 7.55457C5.42494 8.12829 5.59618 8.732 5.59618 8.732L6.07865 9.7182L6.61238 9.52696C6.61238 9.52696 6.33114 8.98324 6.1699 8.42077C6.00866 7.8558 5.79992 6.10339 5.58493 5.87465C5.43494 5.71341 4.99246 5.89465 4.96121 6.00465Z" fill="#FBDE93"/>
<path d="M6.63232 5.80348C6.50607 5.88598 6.70231 7.22216 6.80356 7.77588C6.9048 8.3296 7.11604 8.91332 7.22604 9.18456C7.33728 9.45579 7.65851 10.1408 7.65851 10.1408C7.65851 10.1408 7.991 10.5332 8.001 10.4932C8.01099 10.4532 8.15224 9.95951 8.15224 9.95951C8.15224 9.95951 7.70851 8.65083 7.63726 8.08711C7.56727 7.52339 7.45602 5.81348 7.34603 5.75349C7.23604 5.69349 6.83356 5.67349 6.63232 5.80348Z" fill="#FBDE93"/>
<path d="M8.87582 5.83344C8.72458 5.97343 8.57459 7.82584 8.51334 8.50955C8.4521 9.19326 8.4021 10.1995 8.4021 10.1995L9.07581 10.1495C9.07581 10.1495 9.32705 7.66459 9.36705 7.34211C9.40705 7.01963 9.58829 6.28592 9.49829 5.99343C9.4083 5.70345 9.00582 5.71345 8.87582 5.83344Z" fill="#FBDE93"/>
<path d="M4.35233 8.42834C4.30983 8.52459 5.62351 15.713 5.64476 15.8205C5.66601 15.9279 5.62351 16.0254 5.82725 16.1217C6.03224 16.2179 6.35222 16.2692 6.55721 16.2917C6.7622 16.3129 8.17338 16.3154 8.18463 16.1442C8.19587 15.9717 12.4144 9.82702 12.4144 9.82702C12.4144 9.82702 13.1919 8.52334 13.1381 8.41584C13.0844 8.30835 12.8744 8.35835 12.6456 8.51459C12.4169 8.67208 12.2057 9.06081 11.5632 9.49204C10.867 9.95951 9.94453 10.0583 9.01708 10.047C8.16213 10.0358 7.14593 9.88077 6.41847 9.34204C5.7785 8.86707 5.58976 8.52459 5.01979 8.37335C4.51732 8.2396 4.35233 8.42834 4.35233 8.42834Z" fill="#FD703F"/>
<path d="M5.34619 8.89825L6.53863 16.2941C6.53863 16.2941 7.9598 16.4991 9.06725 16.5203C10.1759 16.5416 11.4971 16.3104 11.4971 16.3104L12.4571 13.0655L13.272 8.7845C13.272 8.7845 13.0708 8.58326 12.8445 8.7495C12.6708 8.877 12.1346 9.70695 11.1871 10.0732C10.1184 10.4857 8.19229 10.5157 6.98985 10.0082C5.88741 9.54446 5.55118 8.94074 5.34619 8.89825Z" fill="#FF2A23"/>
<path d="M12.1883 16.0829C11.9458 16.2279 11.3809 16.3253 11.3809 16.3253C11.3809 16.3253 11.9408 13.603 12.2521 12.1818C12.5633 10.7606 13.012 8.35701 13.012 8.35701C13.012 8.35701 13.192 8.32827 13.377 8.35076C13.587 8.37701 13.712 8.43826 13.7545 8.50451C13.8232 8.612 13.4095 10.6194 13.0045 12.5793C12.6345 14.3792 12.3171 16.0066 12.1883 16.0829Z" fill="#DC0D25"/>
<defs>
<radialGradient id="paint0_radial_718_88" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(9.0729 0.0159706) scale(10.3905 10.3905)">
<stop offset="0.447" stop-color="#FCC216"/>
<stop offset="0.5897" stop-color="#FABB16"/>
<stop offset="0.7939" stop-color="#F3A814"/>
<stop offset="0.9212" stop-color="#ED9913"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -0,0 +1,33 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.73857 12.2331C2.73857 12.2331 2.61858 13.0181 2.89732 13.7105C3.1298 14.2905 3.73227 15.0542 4.88471 15.6367C6.03715 16.2191 7.36083 16.4241 9.31448 16.4641C11.2681 16.5054 13.2755 16.0492 14.2392 15.203C15.2042 14.3555 15.5416 13.643 15.5142 12.8693C15.4867 12.0956 15.0754 11.8019 15.0754 11.8019L2.73857 12.2331Z" fill="#FCB745"/>
<path d="M3.2974 11.4443L2.72743 12.0143C2.72743 12.0143 2.57244 12.7768 3.4324 13.6017C4.08361 14.2254 5.23605 15.3091 9.06335 15.3379C12.6457 15.3654 13.7719 14.5779 14.4631 14.0217C15.1555 13.4655 15.4155 12.4993 15.3468 12.228C15.2793 11.9568 15.0668 11.8006 15.0668 11.8006L3.2974 11.4443Z" fill="#E78B20"/>
<path d="M3.54113 12.2581C3.54113 12.2581 3.37989 12.5319 3.43238 12.7056C3.47363 12.8419 3.55488 13.3981 5.16854 14.103C6.02225 14.4768 7.47467 14.7818 9.36082 14.7268C11.5982 14.6618 13.3906 14.2118 14.2581 13.3968C15.1468 12.5631 14.9768 12.0669 14.9768 12.0669L3.54113 12.2581Z" fill="#FFD290"/>
<path d="M3.07979 10.3582C3.07979 10.3582 2.54482 10.9031 2.45607 11.1456C2.37233 11.3744 2.38358 11.7068 2.53107 11.9756C2.72981 12.3355 2.9773 12.373 3.28353 12.5568C3.61476 12.7543 4.13849 13.3705 5.42717 13.6417C6.71585 13.913 8.41201 14.0355 9.32071 14.0355C10.2294 14.0355 11.4369 14.0217 12.563 13.6417C13.6892 13.2618 14.9779 12.5155 15.2766 11.8506C15.5754 11.1856 15.2904 10.7919 15.2904 10.7919L14.6392 10.3582H3.07979Z" fill="#6D544D"/>
<path d="M2.76732 9.66693C2.76732 9.66693 2.35235 9.64818 2.25235 10.2769C2.17986 10.7344 2.50984 11.0231 2.50984 11.0231C2.50984 11.0231 2.33485 11.2743 2.53734 11.5931C2.72733 11.8918 2.93107 11.9593 3.35104 12.0268C3.65353 12.0756 3.81227 11.9856 4.206 12.203C4.69972 12.4755 4.91096 12.963 5.58968 13.1117C6.26839 13.2605 7.02835 13.1255 7.0546 13.0168C7.08085 12.908 6.10465 12.5018 4.34099 11.5106C3.74602 11.1769 3.20105 10.7231 3.05231 10.4931C2.90357 10.2631 2.76732 9.66693 2.76732 9.66693Z" fill="#9B7169"/>
<path d="M11.302 12.9768C11.302 13.1531 11.8857 13.0718 12.157 12.9231C12.4282 12.7743 12.8619 12.3943 13.2556 12.2306C13.6494 12.0681 13.7981 12.2169 14.3681 11.9456C14.9381 11.6744 15.2505 11.0369 15.2505 11.0369C15.2505 11.0369 15.528 10.6944 15.5505 10.4544C15.5855 10.0882 15.5493 9.91072 15.2918 9.82948C15.0343 9.74823 14.4731 10.5919 14.3918 10.6869C14.3106 10.7819 13.1082 11.6606 12.6332 11.9994C12.1582 12.3381 11.302 12.8818 11.302 12.9768Z" fill="#9B7169"/>
<path d="M3.29733 9.20577C3.29733 9.20577 2.61862 9.28702 2.52362 9.572C2.42863 9.85699 2.72736 10.277 3.51357 10.8069C4.15853 11.2407 6.36842 12.2844 7.24462 12.8281C8.24832 13.4518 9.09827 14.323 9.23952 14.3343C9.41576 14.348 10.6394 13.0481 12.3331 11.9056C13.943 10.8182 15.0467 10.5494 15.4404 9.74824C15.6517 9.31826 15.1417 9.06953 15.1417 9.06953L8.60205 6.58716L3.29733 9.20577Z" fill="#FFE869"/>
<path d="M9.32088 13.6018C9.52337 13.5793 10.5821 12.5568 11.8307 11.7706C13.0794 10.9844 14.9243 10.142 15.1956 9.66823C15.4668 9.1945 14.8156 8.88077 14.8156 8.88077L7.21724 7.60583L2.76747 9.35574C2.76747 9.35574 2.52374 9.43699 2.68623 9.64073C2.84872 9.84447 3.10621 10.1832 3.67618 10.5357C4.36864 10.9644 6.14604 11.6756 7.27099 12.2856C8.39843 12.8956 9.07714 13.628 9.32088 13.6018Z" fill="#FFBE01"/>
<path d="M3.37988 9.55827L3.61612 10.1907C3.61612 10.1907 5.31603 11.9044 8.6971 11.9719C12.0619 12.0394 13.9481 10.4382 13.9481 10.4382L14.3781 9.35828L3.37988 9.55827Z" fill="#F4482B"/>
<path d="M7.81222 10.6031L7.54598 11.0294C7.54598 11.0294 8.1147 11.3706 8.27844 11.4294C8.51343 11.5131 8.74467 11.4831 9.0909 11.3756C9.43713 11.2694 10.8746 10.6969 10.8746 10.6969C10.8746 10.6969 12.0632 10.7144 12.8095 10.7281C13.4707 10.7406 14.1331 10.5032 14.4794 10.1032C14.8256 9.7032 15.3631 8.84574 15.3631 8.84574C15.3631 8.84574 15.6431 8.7795 15.8293 8.56576C16.0155 8.35202 16.0293 8.16578 16.0293 8.16578C16.0293 8.16578 15.9755 7.8058 15.7768 7.7133C15.5768 7.61956 6.66978 7.07458 6.66978 7.07458L3.10122 7.34082L2.5 8.57951L4.48365 10.2707L7.27975 10.4307L7.81222 10.6031Z" fill="#A6B732"/>
<path d="M2.68614 7.48837C2.4849 7.39713 2.27366 7.40838 2.17992 7.67461C2.08617 7.94085 2.11492 8.05834 2.11492 8.05834C2.11492 8.05834 1.84118 8.11584 1.80744 8.45957C1.76744 8.87205 2.21991 9.01829 2.21991 9.01829C2.21991 9.01829 2.24116 9.17828 2.4199 9.33828C2.62364 9.52077 2.82738 9.50202 2.82738 9.50202C2.82738 9.50202 3.36485 10.0307 3.72483 10.2432C4.08482 10.4557 4.60354 10.6032 5.12351 10.5895C5.64348 10.5757 5.81597 10.337 6.2022 10.4695C6.58843 10.602 7.61213 11.1407 8.02461 11.1494C8.67707 11.1632 10.0882 10.1507 10.4082 10.1645C10.7282 10.1782 12.1794 10.4045 12.9518 10.4045C13.7243 10.4045 14.0043 10.1782 14.1768 9.95199C14.3493 9.72576 14.643 9.15329 14.5493 9.13954C14.4555 9.12579 13.843 9.63201 13.4043 9.59201C12.9643 9.55201 12.5794 9.29953 12.0331 9.29953C11.4869 9.29953 8.82456 9.64576 8.65082 9.72576C8.47708 9.80575 7.73212 10.3782 7.58588 10.352C7.43964 10.3257 6.93341 9.8995 6.76092 9.80575C6.58843 9.71201 4.77228 9.65201 4.51104 9.69951C4.32355 9.73326 4.05857 9.7795 4.05857 9.7795C4.05857 9.7795 4.08482 9.36702 3.85858 9.12704C3.63234 8.88705 2.88613 8.47457 2.88613 8.47457C2.88613 8.47457 2.97862 7.62087 2.68614 7.48837Z" fill="#C2DD1F"/>
<path d="M14.4956 8.20707C14.4956 8.20707 15.3081 8.20707 15.5343 8.19332C15.7605 8.17957 16.0268 8.16707 16.0268 8.16707C16.0268 8.16707 15.9493 7.79834 15.8318 7.6021C15.5918 7.19962 15.0143 7.08838 15.0143 7.08838L14.4956 8.20707Z" fill="#C2DD1F"/>
<path d="M2.49988 6.99588L2.41113 7.38836C2.41113 7.38836 3.63982 9.56449 9.16203 9.64699C12.3944 9.69574 14.2518 8.71454 15.0717 7.91208C15.5629 7.43085 15.5792 6.96338 15.5792 6.96338L2.49988 6.99588Z" fill="#DE8010"/>
<path d="M2.22002 5.91342C2.15003 7.00086 2.41501 7.39084 2.41501 7.39084C2.41501 7.39084 4.24242 9.04575 8.90967 9.122C13.6932 9.1995 15.2481 7.61333 15.5206 7.15085C15.6943 6.85712 15.8331 6.2809 15.7231 5.44094C15.5406 4.05352 13.9432 1.42115 8.88717 1.44115C3.83119 1.46115 2.32252 4.33475 2.22002 5.91342Z" fill="url(#paint0_radial_721_184)"/>
<path d="M7.0235 2.79484C7.06225 3.00733 6.10355 3.7148 5.96481 3.03608C5.83482 2.39736 6.98725 2.5986 7.0235 2.79484Z" fill="#FDEAC8"/>
<path d="M7.36586 4.12605C7.41586 4.25729 8.39831 4.44603 8.29331 3.89231C8.18332 3.31609 7.26337 3.85606 7.36586 4.12605Z" fill="#FDEAC8"/>
<path d="M8.22709 2.60604C8.25209 2.79478 9.39703 3.01477 9.37328 2.4823C9.34454 1.82483 8.19835 2.3873 8.22709 2.60604Z" fill="#FDEAC8"/>
<path d="M6.33846 4.56352C6.34471 4.60602 6.06348 4.82725 5.77599 4.9135C5.5235 4.98975 5.26477 4.95725 5.23602 4.64351C5.20852 4.34103 5.42351 4.23104 5.681 4.25728C5.97723 4.28603 6.32596 4.48227 6.33846 4.56352Z" fill="#FDEAC8"/>
<path d="M8.71705 5.44724C8.66955 5.2485 7.59336 4.97226 7.66586 5.60098C7.7396 6.2272 8.7683 5.65848 8.71705 5.44724Z" fill="#FDEAC8"/>
<path d="M9.72448 4.016C9.67198 3.95475 9.42574 3.966 9.12576 4.04475C8.90952 4.10224 8.74953 4.34598 8.89952 4.57722C9.07451 4.84721 9.36824 4.73346 9.55573 4.50472C9.70823 4.31973 9.78322 4.08475 9.72448 4.016Z" fill="#FDEAC8"/>
<path d="M10.8771 2.55482C10.8434 2.51858 10.5971 2.49358 10.3959 2.59107C10.1934 2.68857 10.0184 2.92106 10.2059 3.13854C10.3959 3.35728 10.6784 3.23104 10.8121 2.9998C10.9184 2.81356 10.9284 2.60982 10.8771 2.55482Z" fill="#FDEAC8"/>
<path d="M11.6495 4.45727C11.617 4.25353 10.712 3.89105 10.7183 4.44602C10.7245 5.01474 11.6782 4.64101 11.6495 4.45727Z" fill="#FDEAC8"/>
<path d="M12.9005 3.45228C12.8705 3.29604 11.7193 3.16979 11.9156 3.75101C12.1193 4.35723 12.9368 3.64227 12.9005 3.45228Z" fill="#FDEAC8"/>
<path d="M11.3157 5.49714C11.3232 5.32215 10.447 4.92092 10.382 5.44589C10.317 5.97087 11.3057 5.72463 11.3157 5.49714Z" fill="#FDEAC8"/>
<defs>
<radialGradient id="paint0_radial_721_184" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(8.9328 2.94018) rotate(89.3583) scale(5.50481 9.59198)">
<stop offset="0.517" stop-color="#DF8016"/>
<stop offset="0.6426" stop-color="#E58C21"/>
<stop offset="1" stop-color="#F5AC3C"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -0,0 +1,27 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.53148 15.0048C4.03297 14.9075 4.32384 14.2326 4.18115 13.4973C4.03847 12.762 3.51626 12.2448 3.01478 12.3421C2.51329 12.4394 2.22242 13.1144 2.3651 13.8497C2.50779 14.585 3.02999 15.1021 3.53148 15.0048Z" fill="#616161"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.80501 13.7743C2.59761 12.0082 2.47024 10.1592 2.55166 8.48633L3.30073 8.52278C3.22216 10.1373 3.34478 11.9405 3.54985 13.6868L2.80501 13.7743Z" fill="#616161"/>
<path d="M15.6327 13.8482C15.7754 13.1129 15.4845 12.4379 14.983 12.3406C14.4815 12.2433 13.9593 12.7605 13.8166 13.4958C13.674 14.2311 13.9648 14.906 14.4663 15.0034C14.9678 15.1007 15.49 14.5835 15.6327 13.8482Z" fill="#616161"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4494 13.6868C14.6545 11.9405 14.7771 10.1373 14.6985 8.52278L15.4476 8.48633C15.529 10.1592 15.4016 12.0082 15.1942 13.7743L14.4494 13.6868Z" fill="#616161"/>
<path d="M15.2405 9.32571C14.5943 9.29572 14.0581 8.81074 13.9743 8.16953C13.8806 7.44331 13.6394 6.60461 13.0344 5.79965C12.0832 4.53597 10.6908 3.86725 9.00586 3.86725C7.82467 3.86725 6.16476 4.20224 4.96357 5.79965C4.3511 6.61461 4.11237 7.47081 4.02112 8.20827C3.94488 8.81949 3.4499 9.29322 2.83368 9.32197L2.75869 9.32571C2.32621 9.34571 1.96873 8.98823 1.99373 8.557C2.05373 7.50331 2.31871 5.63216 3.37491 4.22723C4.69734 2.46733 6.69723 1.49988 9.00586 1.49988C11.3057 1.49988 13.3006 2.46733 14.6231 4.22473C15.6805 5.63091 15.9442 7.50331 16.0055 8.55576C16.0305 8.98823 15.673 9.34571 15.2405 9.32571Z" fill="#616161"/>
<path d="M5.84104 16.4103C4.6261 16.5791 3.23243 15.6829 2.95119 13.6092C2.67121 11.5443 3.65241 10.2894 4.64735 10.0969L5.84104 16.4103Z" fill="#424242"/>
<path d="M5.91105 9.66946L4.8536 9.8507C4.42613 9.93319 4.09114 10.2644 4.00365 10.6919C3.88616 11.2681 3.81116 12.1893 4.05365 13.4455C4.29738 14.7004 4.71111 15.5266 5.03734 16.0179C5.27733 16.3804 5.71231 16.5628 6.13979 16.4791L7.19723 16.2979C7.70845 16.1991 7.8347 14.6342 7.47847 12.8043C7.12348 10.9744 6.42227 9.57071 5.91105 9.66946Z" fill="#757575"/>
<path opacity="0.5" d="M7.30094 16.0162C7.72721 15.9335 7.80678 14.4958 7.47867 12.8049C7.15057 11.1141 6.53903 9.81048 6.11276 9.8932C5.6865 9.97591 5.60693 11.4137 5.93503 13.1045C6.26314 14.7953 6.87468 16.0989 7.30094 16.0162Z" fill="#424242"/>
<path d="M7.23096 12.853C7.45345 13.9979 7.5022 14.9516 7.34095 14.9829C7.17971 15.0141 6.86723 14.1117 6.64599 12.9667C6.4235 11.8218 6.37476 10.8681 6.536 10.8368C6.69724 10.8056 7.00847 11.7081 7.23096 12.853Z" fill="url(#paint0_linear_715_302)"/>
<path d="M12.1582 16.4103C13.3731 16.5791 14.7668 15.6829 15.0481 13.6092C15.328 11.5443 14.3468 10.2894 13.3519 10.0969L12.1582 16.4103Z" fill="#424242"/>
<path d="M12.0882 9.66946L13.1456 9.8507C13.5731 9.93319 13.9081 10.2644 13.9956 10.6919C14.1131 11.2681 14.1881 12.1893 13.9456 13.4455C13.7018 14.7004 13.2881 15.5266 12.9619 16.0179C12.7219 16.3804 12.2869 16.5628 11.8594 16.4791L10.802 16.2979C10.2908 16.1991 10.1645 14.6342 10.5208 12.8043C10.8757 10.9744 11.577 9.57071 12.0882 9.66946Z" fill="#757575"/>
<path opacity="0.5" d="M12.0637 13.1035C12.3918 11.4127 12.3122 9.97494 11.886 9.89222C11.4597 9.8095 10.8482 11.1131 10.5201 12.804C10.1919 14.4948 10.2715 15.9325 10.6978 16.0153C11.1241 16.098 11.7356 14.7943 12.0637 13.1035Z" fill="#424242"/>
<path d="M10.7683 12.853C10.5458 13.9979 10.4971 14.9516 10.6583 14.9829C10.8196 15.0141 11.132 14.1117 11.3533 12.9667C11.5758 11.8218 11.6245 10.8681 11.4633 10.8368C11.302 10.8056 10.9908 11.7081 10.7683 12.853Z" fill="url(#paint1_linear_715_302)"/>
<path d="M15.8855 8.76198C15.5318 9.00197 15.0955 8.73198 14.9618 8.367C14.8068 7.45705 14.5006 6.40086 13.9006 5.52965C12.7769 3.80599 10.8882 2.94229 8.9996 2.94104C7.11095 2.94229 5.2223 3.80599 4.09861 5.52965C3.49864 6.40086 3.19365 7.4583 3.03741 8.367C2.90367 8.73198 2.46744 9.00197 2.11371 8.76198C2.08496 8.74198 2.04121 8.69199 1.99622 8.66449C2.03121 9.04572 2.3637 9.34195 2.75868 9.32445L2.86742 9.31945C3.63988 9.3257 3.94611 8.83448 4.01736 8.24076C4.1061 7.49705 4.34234 6.62585 4.96481 5.79964C6.1635 4.20597 7.81841 3.86849 8.9996 3.86724C10.1808 3.86849 11.8357 4.20597 13.0344 5.79964C13.6469 6.6146 13.8856 7.4708 13.9768 8.20826C14.0543 8.82698 14.3543 9.3257 15.198 9.3232L15.2393 9.3257C15.6342 9.34445 15.9667 9.04697 16.0017 8.66574C15.958 8.69199 15.9142 8.74198 15.8855 8.76198Z" fill="#424242"/>
<path d="M2.83121 6.20091C2.71497 6.11841 2.62372 6.02717 2.78996 5.3997C2.9912 4.64349 3.9249 3.17982 5.80605 2.20987C6.15478 2.02988 7.00224 1.76114 7.26098 2.22737C7.50596 2.66984 6.32228 2.99233 5.9048 3.21607C4.81236 3.80353 4.02365 4.63224 3.59992 5.28721C3.46493 5.49595 3.1087 6.3959 2.83121 6.20091Z" fill="#757575"/>
<defs>
<linearGradient id="paint0_linear_715_302" x1="6.67969" y1="12.9608" x2="7.03923" y2="12.891" gradientUnits="userSpaceOnUse">
<stop offset="0.2016" stop-color="#212121"/>
<stop offset="1" stop-color="#212121" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint1_linear_715_302" x1="11.3196" y1="12.9609" x2="10.96" y2="12.8911" gradientUnits="userSpaceOnUse">
<stop offset="0.2016" stop-color="#212121"/>
<stop offset="1" stop-color="#212121" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -0,0 +1,25 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.9153 14.4092L15.1966 14.0943C15.1916 14.0555 15.1854 14.0168 15.1754 13.9793C15.1216 13.7705 14.9241 13.5168 14.5979 13.443L14.5316 12.5068L13.8317 12.5468L13.9192 13.543C13.6592 13.6855 13.5392 13.9368 13.5479 14.2255L12.813 14.8217L13.2505 15.3704L13.8417 14.873C13.9892 14.9867 14.1829 15.0817 14.4229 15.068C14.7116 15.0517 14.9091 14.9017 15.0316 14.7492L15.6678 15.0905L15.9153 14.4092Z" fill="#878787"/>
<path d="M6.0135 14.1379C5.88226 12.4643 4.50233 11.9118 3.49989 11.9743C2.35495 12.0468 1.29375 12.8843 1.3125 14.3129C1.32875 15.5691 2.11621 16.5553 3.72737 16.5553C5.01731 16.5566 6.1085 15.3554 6.0135 14.1379ZM3.67113 15.7279C2.69993 15.7454 2.13996 15.1791 2.13121 14.3292C2.12371 13.533 2.77492 12.8418 3.59488 12.8655C4.38234 12.888 4.96231 13.3342 5.05855 14.1617C5.1548 14.9891 4.52983 15.7129 3.67113 15.7279Z" fill="#4D453C"/>
<path d="M3.58723 13.1605C2.95101 13.1268 2.39354 13.5655 2.38604 14.3692C2.37854 15.2204 3.14975 15.5141 3.59473 15.5229C4.0397 15.5316 4.74092 15.2054 4.76342 14.4179C4.78841 13.5817 4.33469 13.1993 3.58723 13.1605Z" fill="#858585"/>
<path d="M14.1731 11.6731C12.9419 11.7556 11.867 12.8155 11.882 14.1467C11.8982 15.5466 12.9719 16.5403 14.3319 16.5403C15.6605 16.5403 16.7642 15.5616 16.7417 14.0742C16.718 12.4606 15.4693 11.5856 14.1731 11.6731ZM14.3644 15.5779C13.6094 15.5704 13.0357 14.9729 12.9644 14.3217C12.8694 13.4505 13.4157 12.8168 14.1894 12.7543C14.9931 12.6905 15.6605 13.168 15.748 14.0267C15.8355 14.8854 15.1593 15.5866 14.3644 15.5779Z" fill="#4D453C"/>
<path d="M1.89367 12.2531C1.93992 12.3343 2.45864 12.2931 2.85612 12.4681C3.11486 12.5818 3.39734 12.8818 3.35735 13.2955C3.31735 13.7092 3.2386 14.0905 3.2386 14.0905L3.96981 14.3292C3.96981 14.3292 4.25605 13.6855 4.41479 13.518C4.57353 13.3505 4.93226 13.0168 5.45723 13.088C5.86096 13.143 6.31594 13.5093 6.31594 13.5093C6.31594 13.5093 6.0297 11.7756 5.99721 11.7594C5.96596 11.7431 4.39104 11.8231 4.39104 11.8231C4.39104 11.8231 3.67483 11.5294 2.94362 11.7119C2.21116 11.8956 1.86117 12.1981 1.89367 12.2531Z" fill="#DB0D2A"/>
<path d="M4.2399 11.498L3.48494 13.7892C3.48494 13.7892 3.1962 13.7904 3.08746 14.1867C2.99996 14.5054 3.21495 14.7116 3.41369 14.7666C3.64868 14.8329 3.95241 14.7279 4.02616 14.4566C4.12115 14.1067 3.93116 13.9479 3.93116 13.9154C3.93116 13.8829 4.77362 11.5618 4.77362 11.5618L4.2399 11.498Z" fill="#E1D9DC"/>
<path d="M6.84961 13.3155L8.20829 14.8479L10.8519 14.8354L12.4706 14.3529L12.2493 13.488L10.9394 12.8705C10.9394 12.8705 11.5944 11.0169 11.4581 10.9556C11.3219 10.8944 9.7907 9.86816 9.7907 9.86816L6.88711 10.8444L6.84961 13.3155Z" fill="#2F2F2F"/>
<path d="M12.3868 11.2294C12.3868 11.2294 11.9393 11.6369 11.6793 11.9981C11.4969 12.2531 11.4456 12.5481 11.4456 12.5481L11.8281 12.7206C11.8281 12.7206 12.0593 12.1906 12.6005 11.8756C13.2218 11.5131 13.6942 11.5594 13.768 11.7194C13.8417 11.8794 13.5367 11.9331 13.1005 12.3868C12.7793 12.7206 12.6068 13.0418 12.6068 13.0418C12.6068 13.0418 14.053 13.6343 14.053 13.7093C14.053 13.783 13.793 14.5242 13.793 14.5242L12.1493 14.1292C12.1493 14.1292 11.0869 13.808 10.9756 14.1292C10.8644 14.4505 11.3581 14.6355 11.3581 14.6355L10.9006 14.9317L9.47946 14.3142C9.47946 14.3142 9.39571 13.9218 9.83819 13.0543C10.5069 11.7406 11.5056 11.0769 11.5056 11.0769L12.3868 11.2294Z" fill="#5E6268"/>
<path d="M10.9369 14.7805C10.9406 14.833 10.9806 14.8955 11.1481 14.8867C11.3156 14.878 12.993 14.9042 13.1955 14.8605C13.398 14.8167 16.1091 13.6405 16.1628 13.5343C16.2016 13.4568 16.2078 13.1955 16.0816 12.9268C15.9404 12.6268 15.7991 12.4768 15.6141 12.5118C15.4291 12.5468 11.6518 14.348 11.3693 14.4617C11.0869 14.5767 10.9281 14.6567 10.9369 14.7805Z" fill="#E0E0E0"/>
<path d="M12.9218 14.2767C12.9518 14.348 13.2455 14.338 13.5868 14.1817C13.928 14.0255 16.0991 12.963 16.0991 12.963C16.0991 12.963 16.0629 12.8543 15.9991 12.753C15.9491 12.6743 15.8654 12.5918 15.8654 12.5918C15.8654 12.5918 13.3968 13.8492 13.258 13.918C13.078 14.008 12.8656 14.143 12.9218 14.2767Z" fill="#FEFEFE"/>
<path d="M15.063 9.95188L15.9642 10.7481C15.9642 10.7481 16.1654 10.4256 16.1654 10.3944C16.1654 10.3619 15.5617 9.59814 15.5617 9.59814L15.063 9.95188Z" fill="#484D51"/>
<path d="M12.5669 11.2956C12.5669 11.2956 13.793 10.7694 14.308 10.5394C15.1142 10.1807 15.9392 9.71821 16.0867 9.63072C16.3529 9.47198 16.3504 9.32323 16.2767 9.26199C16.2029 9.20074 16.0054 9.28698 15.9054 9.29948C15.8054 9.31198 15.5105 9.32448 15.5105 9.32448L13.2993 9.92945C13.2993 9.92945 11.8294 10.8557 11.8656 10.8557C11.9019 10.8557 12.5669 11.2956 12.5669 11.2956Z" fill="#DC0D2A"/>
<path d="M9.77075 10.1719C9.77075 10.1719 10.737 11.1119 10.8757 11.1819C11.0157 11.2519 11.2819 11.2831 11.5044 11.2394C11.7269 11.1956 12.9531 10.2669 13.4806 10.1019C14.008 9.93693 14.9167 9.91193 15.2155 9.74569C15.5142 9.57945 15.5104 9.32446 15.5104 9.32446L12.9156 9.50445C12.9156 9.50445 12.3656 10.3882 11.3332 10.3307C10.5395 10.2857 9.94199 9.61945 9.94199 9.61945L9.77075 10.1719Z" fill="#464C4F"/>
<path d="M12.7831 11.9943C13.0119 12.1156 13.1206 11.8518 13.0631 11.7343C12.9419 11.4869 12.4919 10.7831 12.3769 10.7369C12.2819 10.6994 11.767 11.0731 11.7157 11.1119C11.6645 11.1494 11.6832 11.2306 11.792 11.3094C11.9319 11.4106 12.6057 11.9006 12.7831 11.9943Z" fill="#C8C8C8"/>
<path d="M5.00966 11.0169L3.4585 10.7057L4.22596 9.22699L5.04966 8.40329L5.53714 8.22705C5.53714 8.22705 5.12591 8.77327 4.90467 9.117C4.76718 9.33074 4.62218 9.68197 4.62218 9.68197C4.62218 9.68197 4.79343 9.64447 4.92967 9.62822C5.06591 9.61197 5.21715 9.60072 5.21715 9.60072C5.21715 9.60072 5.17216 9.30824 5.2359 9.157C5.29965 9.0045 5.43214 8.90576 5.52839 8.89701C5.62463 8.88951 5.68838 8.93701 5.68838 8.93701C5.68838 8.93701 5.67463 8.70577 5.69588 8.64077C5.73588 8.52078 6.07211 8.37954 6.48834 8.22705C6.90706 8.0733 7.37704 7.85956 7.50328 7.88956C7.58203 7.90831 7.76702 8.26579 7.67077 8.36954C7.57453 8.47328 6.08836 9.1695 6.08836 9.1695L6.33584 9.84946L5.00966 11.0169Z" fill="#464C4F"/>
<path d="M7.70974 10.7943L9.73088 10.7206L10.1246 11.1318L8.80218 11.953L7.521 11.6405L7.40601 10.8443L7.70974 10.7943Z" fill="#464C4F"/>
<path d="M6.84709 12.0594L5.78339 13.2205C5.78339 13.2205 6.17337 13.913 6.28087 14.2105C6.39336 14.5205 6.5521 15.0805 6.5521 15.0805C6.5521 15.0805 10.5844 15.1954 10.7731 15.1704C10.9619 15.1454 11.1756 14.9242 11.0769 14.8342C10.9781 14.7442 9.44195 14.1118 9.44195 14.1118C9.44195 14.1118 8.23451 14.1942 8.03702 14.1205C7.83953 14.0468 7.19582 13.1593 7.23082 13.0918C7.31581 12.9243 8.88323 11.9031 8.88323 11.9031L8.75199 11.7631L7.54455 11.4919L7.6183 10.9994C7.6183 10.9994 8.16077 10.5644 8.22576 10.5644C8.29076 10.5644 8.94073 10.4582 9.30196 10.3757C9.66319 10.2932 10.1069 10.1945 10.2132 10.137C10.3194 10.0795 10.3981 9.98072 10.3694 9.82448C10.3469 9.70698 10.0407 9.60324 10.0407 9.60324L6.87084 9.422L6.21962 9.49074C6.21962 9.49074 6.13213 9.78323 5.6634 10.0707C5.19467 10.3582 4.39722 10.5444 4.39722 10.5444L6.84709 12.0594Z" fill="#DC0D2A"/>
<path d="M3.62231 9.04443C3.62231 9.04443 4.55851 8.03324 5.05474 7.58951C5.42222 7.26078 5.7047 7.04829 5.8097 7.00829C5.86595 6.98704 6.40967 6.96829 6.46466 6.98454C6.52091 7.00079 6.57716 7.08829 6.48841 7.17703C6.40092 7.26453 6.12343 7.58201 5.87345 7.8445C5.75845 7.96449 5.53721 8.22323 5.53721 8.22323L5.12973 8.37447L4.10604 8.88569L3.62231 9.04443Z" fill="#AEE3FD"/>
<path d="M5.52591 8.22571C5.52591 8.22571 4.46222 8.40445 3.73976 8.94692C3.0173 9.48939 2.73731 10.5818 2.72106 10.6881C2.70481 10.7943 2.56732 11.1481 2.83856 11.318C2.9698 11.4005 3.23104 11.3618 3.50977 11.428C3.74101 11.4818 4.26473 11.658 4.66721 12.0193C5.06969 12.3805 5.78465 13.2267 5.78465 13.2267L8.75199 11.7643C8.75199 11.7643 7.89579 10.7631 5.91589 10.6318C3.936 10.5006 3.73101 10.5493 3.73101 10.5493C3.73101 10.5493 3.97975 9.83437 4.33098 9.3669C4.7997 8.74193 5.52591 8.22571 5.52591 8.22571Z" fill="#FE2A22"/>
<path d="M8.37714 13.2118C8.37714 13.2118 8.1859 12.6768 8.63587 12.3681C8.97585 12.1343 9.40708 12.1656 9.69957 12.5543C9.99205 12.943 9.73081 13.3993 9.57582 13.528C9.0696 13.9505 8.60837 13.5505 8.60837 13.5505C8.60837 13.5505 8.63837 13.7705 8.43963 13.9217C8.29839 14.0292 7.94466 14.0605 7.79842 13.7917C7.64093 13.5018 7.80217 13.2868 7.96716 13.1955C8.17965 13.0768 8.37714 13.2118 8.37714 13.2118Z" fill="#464C4F"/>
<path d="M3.04728 9.81194C3.04728 9.81194 3.721 9.80819 3.726 9.83569C3.731 9.86319 3.47851 10.5019 3.47851 10.5019C3.47851 10.5019 2.7073 10.9019 2.68355 10.8969C2.65981 10.8919 2.67855 10.5532 2.77855 10.2782C2.87854 10.0032 3.04728 9.81194 3.04728 9.81194Z" fill="#D9E3DF"/>
<path d="M6.21851 9.49316C6.21851 9.49316 6.80597 9.99064 7.86592 10.0806C8.92586 10.1706 10.0421 9.60441 10.0421 9.60441C10.0421 9.60441 8.25465 8.77695 8.03841 8.79195C7.80592 8.80695 6.22726 9.44316 6.21851 9.49316Z" fill="#FE2A22"/>
<path d="M3.32099 8.642C3.35223 8.75449 3.81971 9.10697 4.12344 9.07947C4.40218 9.05448 4.35468 8.53575 4.35218 8.40826C4.34843 8.15077 4.32593 7.93328 4.31093 7.79829C4.29468 7.6508 4.17219 7.63455 4.08595 7.70455C3.9997 7.77329 3.28724 8.52325 3.32099 8.642Z" fill="#464C4F"/>
</svg>

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -0,0 +1,100 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.10094 15.5491L5.56352 16.2291L2.34369 8.94571L3.88111 8.26575L7.10094 15.5491Z" fill="url(#paint0_linear_746_959)"/>
<path d="M2.87744 9.64192C2.93369 9.52192 3.00868 9.43943 3.08743 9.36943C3.16743 9.30068 3.25367 9.24694 3.34492 9.20569C3.43741 9.16444 3.53491 9.13694 3.6399 9.12444C3.7449 9.11319 3.85614 9.11444 3.98238 9.15319C3.92614 9.27444 3.85239 9.35568 3.77239 9.42693C3.6924 9.49567 3.60615 9.54942 3.51366 9.59067C3.42116 9.63067 3.32367 9.65817 3.21867 9.67067C3.11618 9.68191 3.00493 9.68191 2.87744 9.64192Z" fill="#FFC107"/>
<path d="M3.73865 11.5881C3.79489 11.4681 3.86989 11.3856 3.94864 11.3156C4.02863 11.2469 4.11488 11.1931 4.20612 11.1519C4.29862 11.1106 4.39611 11.0831 4.50111 11.0706C4.6061 11.0594 4.71735 11.0606 4.84359 11.0994C4.78734 11.2206 4.7136 11.3018 4.6336 11.3731C4.5536 11.4418 4.46736 11.4956 4.37486 11.5368C4.28237 11.5768 4.18487 11.6043 4.07988 11.6168C3.97613 11.6281 3.86614 11.6268 3.73865 11.5881Z" fill="#FFC107"/>
<path d="M3.16492 10.2906C3.22116 10.1706 3.29616 10.0881 3.37491 10.0181C3.4549 9.94937 3.54115 9.89562 3.63239 9.85437C3.72489 9.81312 3.82238 9.78563 3.92738 9.77313C4.03237 9.76188 4.14362 9.76313 4.26986 9.80187C4.21361 9.92312 4.13987 10.0044 4.05987 10.0756C3.97987 10.1444 3.89363 10.1981 3.80113 10.2394C3.70864 10.2793 3.61114 10.3068 3.50615 10.3193C3.4024 10.3306 3.29241 10.3293 3.16492 10.2906Z" fill="#FFC107"/>
<path d="M4.31238 12.8854C4.36862 12.7655 4.44362 12.683 4.52237 12.613C4.60236 12.5442 4.68861 12.4905 4.77985 12.4492C4.87235 12.408 4.96984 12.3805 5.07484 12.368C5.17983 12.3567 5.28983 12.358 5.41732 12.3967C5.36107 12.518 5.28733 12.5992 5.20733 12.6705C5.12733 12.7392 5.04109 12.793 4.94859 12.8342C4.8561 12.8742 4.7586 12.9017 4.65361 12.9142C4.54987 12.9254 4.43987 12.9242 4.31238 12.8854Z" fill="#FFC107"/>
<path d="M4.0249 12.2368C4.08115 12.1168 4.15615 12.0343 4.23489 11.9643C4.31489 11.8955 4.40113 11.8418 4.49238 11.8005C4.58487 11.7593 4.68237 11.7318 4.78736 11.7193C4.89236 11.708 5.00235 11.7093 5.12984 11.748C5.0736 11.8693 4.99985 11.9505 4.91986 12.0218C4.83986 12.0905 4.75361 12.1443 4.66112 12.1855C4.56862 12.2255 4.47113 12.253 4.36613 12.2655C4.26239 12.2768 4.1524 12.2755 4.0249 12.2368Z" fill="#FFC107"/>
<path d="M5.1723 14.8316C5.2298 14.7116 5.30355 14.6291 5.38229 14.5591C5.46229 14.4904 5.54853 14.4366 5.63978 14.3954C5.73227 14.3541 5.82977 14.3266 5.93476 14.3141C6.03976 14.3029 6.14975 14.3041 6.27724 14.3429C6.221 14.4641 6.14725 14.5454 6.06726 14.6166C5.98726 14.6854 5.90101 14.7391 5.80977 14.7804C5.71727 14.8204 5.61978 14.8479 5.51478 14.8604C5.40979 14.8716 5.2998 14.8704 5.1723 14.8316Z" fill="#FFC107"/>
<path d="M3.45117 10.9394C3.50742 10.8194 3.58241 10.7369 3.66116 10.6669C3.74116 10.5982 3.8274 10.5444 3.91865 10.5032C4.01114 10.4619 4.10864 10.4344 4.21363 10.4219C4.31863 10.4107 4.42987 10.4119 4.55611 10.4507C4.49987 10.5719 4.42612 10.6532 4.34612 10.7244C4.26613 10.7932 4.17988 10.8469 4.08739 10.8882C3.99489 10.9282 3.8974 10.9557 3.7924 10.9682C3.68866 10.9794 3.57867 10.9781 3.45117 10.9394Z" fill="#FFC107"/>
<path d="M4.88611 14.1829C4.94236 14.0629 5.01735 13.9804 5.0961 13.9104C5.17609 13.8417 5.26234 13.788 5.35358 13.7467C5.44608 13.7055 5.54357 13.678 5.64857 13.6655C5.75356 13.6542 5.86356 13.6555 5.99105 13.6942C5.9348 13.8155 5.86106 13.8967 5.78106 13.9679C5.70107 14.0367 5.61482 14.0904 5.52357 14.1317C5.43108 14.1717 5.33358 14.1992 5.22859 14.2117C5.1236 14.2229 5.01235 14.2217 4.88611 14.1829Z" fill="#FFC107"/>
<path d="M4.59863 13.5343C4.65488 13.4143 4.72988 13.3318 4.80862 13.2618C4.88862 13.193 4.97486 13.1393 5.06611 13.098C5.1586 13.0568 5.2561 13.0293 5.36109 13.0168C5.46609 13.0055 5.57608 13.0068 5.70357 13.0455C5.64733 13.1668 5.57358 13.248 5.49359 13.3193C5.41359 13.388 5.32734 13.4418 5.2361 13.483C5.1436 13.523 5.04611 13.5505 4.94111 13.563C4.83612 13.5742 4.72613 13.573 4.59863 13.5343Z" fill="#FFC107"/>
<path d="M3.91738 8.48077L2.48995 9.11199C2.31746 9.18824 2.11747 9.11074 2.04123 8.93825C1.96498 8.76576 2.04248 8.56577 2.21497 8.48952L3.64239 7.85831C3.81488 7.78206 4.01487 7.85956 4.09112 8.03205C4.16737 8.20329 4.08987 8.40453 3.91738 8.48077Z" fill="#FFA000"/>
<g opacity="0.81">
<path opacity="0.81" d="M4.57732 13.9392L3.70612 11.993L5.09104 11.0693L6.16349 13.2905L4.57732 13.9392Z" fill="url(#paint1_linear_746_959)"/>
</g>
<path d="M2.11749 9.04568C2.23124 9.17068 2.42748 9.13568 2.42748 9.13568L2.48247 9.25942L4.02239 8.58571L3.96865 8.45821C3.96865 8.45821 4.13114 8.34697 4.11739 8.12573L2.11749 9.04568Z" fill="url(#paint2_linear_746_959)"/>
<path d="M2.81244 16.1078V16.4803H15.9867V16.1078C15.9867 13.9817 12.6482 12.9292 9.39959 12.933C6.17101 12.9367 2.81244 13.8554 2.81244 16.1078Z" fill="url(#paint3_linear_746_959)"/>
<path d="M3.73236 13.6642C3.73236 14.8329 5.57851 16.4803 5.57851 16.4803H13.0106C13.0106 16.4803 14.7205 14.7879 14.7205 13.408C14.7205 13.408 12.8244 12.8643 9.39956 12.933C5.57851 13.0093 3.73236 13.6642 3.73236 13.6642Z" fill="#212121"/>
<path d="M9.99328 13.3067C12.2157 13.3067 13.7056 13.5692 14.3106 13.7005C14.1418 14.5792 13.2969 15.6266 12.8494 16.1003H5.72476C5.14104 15.5554 4.33983 14.6229 4.15234 13.9355C4.8023 13.7592 6.53846 13.3705 9.40706 13.313C9.6033 13.3092 9.79954 13.3067 9.99328 13.3067ZM9.99328 12.9268C9.80079 12.9268 9.6033 12.928 9.39956 12.933C5.57976 13.0093 3.73236 13.6642 3.73236 13.6642C3.73236 14.8329 5.57851 16.4803 5.57851 16.4803H13.0106C13.0106 16.4803 14.7205 14.7879 14.7205 13.408C14.7205 13.408 13.0419 12.9268 9.99328 12.9268Z" fill="url(#paint4_linear_746_959)"/>
<path d="M7.55591 13.1867C7.55591 12.8142 7.58091 12.1593 8.02713 11.9505C8.33587 11.8068 10.4095 11.8805 10.7295 12.0455C11.1345 12.2543 11.2432 12.8342 11.2432 13.1855C11.2432 13.8154 10.4183 15.7028 9.40081 15.7028C8.38336 15.7028 7.55591 13.8167 7.55591 13.1867Z" fill="url(#paint5_linear_746_959)"/>
<path opacity="0.61" d="M7.67841 12.2467L7.59216 12.6592C7.59216 12.6592 8.23838 13.4404 9.39957 13.4404C10.7407 13.4404 11.1945 12.7604 11.1945 12.7604L11.0782 12.3742L7.67841 12.2467Z" fill="black"/>
<path d="M10.6057 16.4803C11.5294 15.4716 12.3882 14.2704 12.6444 13.0917L11.6744 12.8355C11.5519 12.803 11.427 12.873 11.3895 12.993C11.0282 14.1579 9.80079 15.4753 8.68335 16.4803H10.6057Z" fill="black"/>
<path d="M10.3483 16.4828C11.3132 15.4504 12.2307 14.2004 12.4969 12.9755L11.527 12.7193C11.4045 12.6868 11.2795 12.7568 11.242 12.8767C10.8657 14.0879 9.55332 15.4654 8.40088 16.4828H10.3483Z" fill="url(#paint6_linear_746_959)"/>
<g opacity="0.31">
<path opacity="0.31" d="M7.57971 12.9655C7.57971 12.9655 9.06213 15.1716 10.857 16.4278C10.857 16.4278 9.84834 16.544 9.81959 16.4803C9.79085 16.4165 7.65846 14.0841 7.65846 14.0841L7.57971 12.9655Z" fill="black"/>
</g>
<path d="M10.3333 16.4802C9.22584 15.4891 7.98215 14.1554 7.57968 12.9654C7.52718 12.8092 7.36219 12.7229 7.20595 12.7742L6.23975 13.1004C6.50473 14.2941 7.38344 15.4891 8.31964 16.4802H10.3333Z" fill="url(#paint7_linear_746_959)"/>
<path d="M9.39959 2.17358C7.19471 2.17358 5.15356 4.53096 5.15356 7.92453C5.15356 11.2994 7.2572 12.828 9.39959 12.828C11.542 12.828 13.6456 11.2994 13.6456 7.92453C13.6456 4.53096 11.6045 2.17358 9.39959 2.17358Z" fill="#F9DDBD"/>
<path d="M7.3172 8.86824C7.66167 8.86824 7.94092 8.57892 7.94092 8.22202C7.94092 7.86513 7.66167 7.57581 7.3172 7.57581C6.97273 7.57581 6.69348 7.86513 6.69348 8.22202C6.69348 8.57892 6.97273 8.86824 7.3172 8.86824Z" fill="#312D2D"/>
<path d="M11.482 8.86824C11.8265 8.86824 12.1057 8.57892 12.1057 8.22202C12.1057 7.86513 11.8265 7.57581 11.482 7.57581C11.1375 7.57581 10.8583 7.86513 10.8583 8.22202C10.8583 8.57892 11.1375 8.86824 11.482 8.86824Z" fill="#312D2D"/>
<path d="M8.25714 7.08204C8.13965 6.92704 7.86841 6.70081 7.34094 6.70081C6.81347 6.70081 6.54223 6.92704 6.42474 7.08204C6.37224 7.15078 6.38599 7.23078 6.42224 7.27828C6.45599 7.32327 6.55473 7.36452 6.66347 7.32702C6.77222 7.28952 6.98471 7.17953 7.34219 7.17703C7.69842 7.17953 7.91091 7.28952 8.0209 7.32702C8.12965 7.36452 8.22839 7.32327 8.26214 7.27828C8.29589 7.23078 8.30964 7.15078 8.25714 7.08204Z" fill="#454140"/>
<path d="M12.3744 7.08204C12.2569 6.92704 11.9857 6.70081 11.4582 6.70081C10.9307 6.70081 10.6595 6.92704 10.542 7.08204C10.4895 7.15078 10.5032 7.23078 10.5395 7.27828C10.5732 7.32327 10.672 7.36452 10.7807 7.32702C10.8895 7.28952 11.102 7.17953 11.4594 7.17703C11.8157 7.17953 12.0282 7.28952 12.1382 7.32702C12.2469 7.36452 12.3456 7.32327 12.3794 7.27828C12.4131 7.23078 12.4256 7.15078 12.3744 7.08204Z" fill="#454140"/>
<g opacity="0.17">
<path opacity="0.17" d="M9.39959 2.0061C7.05846 2.0061 5.15356 3.57852 5.15356 7.15208C5.15356 10.7456 6.99097 12.7105 9.39959 12.7105C11.8082 12.7105 13.8494 10.6994 13.8494 7.10458C13.8481 3.53102 11.7407 2.0061 9.39959 2.0061ZM9.39959 12.4106C7.82217 12.4106 5.73603 11.5981 5.73603 8.657C5.73603 5.73216 7.48969 5.81215 9.39959 5.81215C11.3095 5.81215 13.0631 5.73216 13.0631 8.657C13.0631 11.5981 10.977 12.4106 9.39959 12.4106Z" fill="black"/>
</g>
<path d="M9.44585 8.60449C10.8395 8.60449 13.3319 9.94067 13.3319 9.94067C13.2557 11.2181 11.592 12.688 9.5346 12.658C6.08228 12.608 5.46606 10.0432 5.46606 10.0432C5.46606 10.0432 8.05218 8.60449 9.44585 8.60449Z" fill="url(#paint8_linear_746_959)"/>
<path opacity="0.4" d="M13.3281 9.99189C13.3281 9.98814 13.3294 9.98439 13.3294 9.98189C13.1969 9.90314 13.0632 9.82065 12.9282 9.73565C12.1645 9.36067 10.4945 8.60571 9.4471 8.60571C8.48215 8.60571 6.85473 9.31943 5.85479 9.84315C5.6548 9.97689 5.76979 9.98939 5.57605 10.1119C6.23102 9.8819 8.11842 9.02944 9.41835 9.02944C10.6945 9.02819 12.6294 9.7669 13.3281 9.99189Z" fill="#BDBDBD"/>
<path d="M9.39959 1.49988C6.95596 1.49988 4.55609 3.26728 4.55609 6.8421C4.55609 10.4357 6.88472 12.933 9.39834 12.933C11.912 12.933 14.2406 10.4357 14.2406 6.8421C14.2418 3.26728 11.8432 1.49988 9.39959 1.49988ZM9.39959 12.6555C7.75217 12.6555 5.57604 11.0919 5.57604 8.15078C5.57604 5.22593 7.40594 5.30593 9.39959 5.30593C11.3932 5.30593 13.2231 5.22593 13.2231 8.15078C13.2231 11.0906 11.0457 12.6555 9.39959 12.6555Z" fill="url(#paint9_linear_746_959)"/>
<path d="M9.53456 2.71479C9.53456 2.71479 7.72966 2.15357 6.08225 3.92723C4.50233 5.62839 4.9948 8.87947 4.9948 8.87947C4.9948 8.87947 4.82731 6.04587 6.69221 4.27221C8.32338 2.71979 9.53456 2.71479 9.53456 2.71479Z" fill="url(#paint10_linear_746_959)"/>
<path d="M12.5669 5.14215C13.6194 6.1646 13.6344 8.88946 13.6344 8.88946C13.6344 8.88946 14.4231 6.14585 12.9781 4.66718C11.7832 3.44349 10.1283 3.93847 10.1283 3.93847C10.1283 3.93847 11.4582 4.06471 12.5669 5.14215Z" fill="url(#paint11_linear_746_959)"/>
<g opacity="0.38">
<path opacity="0.38" d="M9.39959 1.74986C10.612 1.74986 11.7357 2.19734 12.5619 3.00855C13.4981 3.92725 13.9918 5.25218 13.9918 6.8421C13.9918 8.38826 13.5394 9.81819 12.7144 10.8969C13.1744 10.1707 13.4731 9.25197 13.4731 8.15203C13.4731 6.8021 13.0756 5.92589 12.2569 5.47092C11.5682 5.08844 10.6895 5.05719 9.69582 5.05719H9.54833H9.40084H9.25334H9.10585C7.42219 5.05719 5.3273 5.05719 5.3273 8.15203C5.3273 9.25322 5.62478 10.1707 6.08601 10.8969C5.26105 9.81819 4.80858 8.38826 4.80858 6.8421C4.80858 5.25343 5.30355 3.92725 6.2385 3.00855C7.06346 2.19734 8.18715 1.74986 9.39959 1.74986ZM9.39959 1.49988C6.95596 1.49988 4.55609 3.26728 4.55609 6.8421C4.55609 10.4357 6.88472 12.933 9.39834 12.933C11.912 12.933 14.2406 10.4357 14.2406 6.8421C14.2418 3.26728 11.8432 1.49988 9.39959 1.49988ZM9.39959 5.30593C9.49833 5.30593 9.59707 5.30593 9.69457 5.30593C11.4982 5.30593 13.2231 5.37717 13.2231 8.15078C13.2231 11.0906 10.9733 12.6555 9.39959 12.6555C7.82592 12.6555 5.57604 11.0906 5.57604 8.15078C5.57604 5.37717 7.30095 5.30593 9.1046 5.30593C9.2021 5.30593 9.30084 5.30593 9.39959 5.30593Z" fill="url(#paint12_linear_746_959)"/>
</g>
<defs>
<linearGradient id="paint0_linear_746_959" x1="3.42906" y1="9.47632" x2="5.21624" y2="13.306" gradientUnits="userSpaceOnUse">
<stop stop-color="#646464"/>
<stop offset="1" stop-color="#444444"/>
</linearGradient>
<linearGradient id="paint1_linear_746_959" x1="5.47577" y1="13.9528" x2="4.50005" y2="11.5885" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint2_linear_746_959" x1="3.28571" y1="8.99385" x2="3.11769" y2="8.58672" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint3_linear_746_959" x1="9.39947" y1="16.6913" x2="9.39947" y2="15.0654" gradientUnits="userSpaceOnUse">
<stop offset="0.1696" stop-color="#646464"/>
<stop offset="1" stop-color="#757575"/>
</linearGradient>
<linearGradient id="paint4_linear_746_959" x1="9.22613" y1="12.927" x2="9.22613" y2="16.4801" gradientUnits="userSpaceOnUse">
<stop stop-color="#646464"/>
<stop offset="0.2649" stop-color="#575757"/>
<stop offset="0.7538" stop-color="#353535"/>
<stop offset="1" stop-color="#212121"/>
</linearGradient>
<linearGradient id="paint5_linear_746_959" x1="9.39944" y1="12.6796" x2="9.39944" y2="15.4456" gradientUnits="userSpaceOnUse">
<stop offset="0.0744" stop-color="#444444"/>
<stop offset="0.653" stop-color="#212121"/>
</linearGradient>
<linearGradient id="paint6_linear_746_959" x1="11.6532" y1="13.1431" x2="10.2698" y2="15.2902" gradientUnits="userSpaceOnUse">
<stop stop-color="#646464"/>
<stop offset="1" stop-color="#444444"/>
</linearGradient>
<linearGradient id="paint7_linear_746_959" x1="8.38795" y1="15.2569" x2="6.35194" y2="12.3334" gradientUnits="userSpaceOnUse">
<stop stop-color="#646464"/>
<stop offset="1" stop-color="#9E9E9E"/>
</linearGradient>
<linearGradient id="paint8_linear_746_959" x1="9.39942" y1="9.89264" x2="9.39942" y2="12.9208" gradientUnits="userSpaceOnUse">
<stop offset="0.0744" stop-color="#444444"/>
<stop offset="0.653" stop-color="#212121"/>
</linearGradient>
<linearGradient id="paint9_linear_746_959" x1="9.39934" y1="1.56391" x2="9.39934" y2="12.7995" gradientUnits="userSpaceOnUse">
<stop offset="0.0744" stop-color="#444444"/>
<stop offset="0.653" stop-color="#212121"/>
</linearGradient>
<linearGradient id="paint10_linear_746_959" x1="5.59864" y1="6.32337" x2="8.30122" y2="2.98737" gradientUnits="userSpaceOnUse">
<stop stop-color="#0D0C0C"/>
<stop offset="1" stop-color="#0D0C0C" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint11_linear_746_959" x1="12.9059" y1="6.30033" x2="11.0478" y2="4.10448" gradientUnits="userSpaceOnUse">
<stop stop-color="#0D0C0C"/>
<stop offset="1" stop-color="#0D0C0C" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint12_linear_746_959" x1="9.39934" y1="1.49988" x2="9.39934" y2="12.933" gradientUnits="userSpaceOnUse">
<stop stop-color="#BDBDBD"/>
<stop offset="1" stop-color="#646464"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,14 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.7946 16.0455C8.7946 16.0455 13.3519 15.573 14.2118 14.8205C14.6418 14.4443 15.1305 13.6406 15.4155 13.1656C15.7005 12.6894 15.888 12.3482 16.0605 11.8757C16.4042 10.9295 16.5767 8.09214 16.0605 6.67347C15.5442 5.25479 14.878 3.98611 13.9531 3.23365C13.0444 2.49119 11.4382 1.48999 8.98709 1.48999C7.8934 1.48999 7.06469 1.71998 6.36473 1.96247C5.31728 2.3262 4.72231 2.72368 4.32234 3.05866C3.84236 3.46239 2.89241 4.29609 2.23745 5.78976C1.98121 6.37223 1.69872 7.18844 1.61373 7.9184C1.31249 10.5195 1.78497 12.7132 3.28989 14.2606C4.79481 15.808 5.35353 15.593 5.35353 15.593L8.7946 16.0455Z" fill="#F47A00"/>
<path d="M4.90973 14.103C4.90973 14.103 4.6735 14.103 4.39351 13.9743C4.11353 13.8456 3.08233 12.4044 2.88859 11.8457C2.69485 11.2869 2.25363 9.31955 2.45861 8.3846C2.57611 7.85088 2.7161 7.4384 2.7161 7.4384C2.7161 7.4384 3.23607 6.03597 3.30107 5.58475C3.36607 5.13352 3.94229 4.6223 4.22102 4.29981C4.49976 3.97733 4.6935 3.65485 5.20972 3.35361C5.72594 3.05238 6.26341 3.05238 6.60715 2.88114C6.95088 2.70865 7.46085 2.39741 7.96207 2.25742C8.91327 1.99119 9.48824 2.27867 10.0045 2.25742C10.5207 2.23617 11.0094 2.39866 11.6431 2.73865C12.0944 2.98113 12.4431 3.25987 12.8506 3.58235C13.258 3.90483 13.8243 4.09857 14.2392 4.62105C14.6367 5.12102 14.7117 5.69599 14.9492 6.34096C15.1854 6.98592 15.4454 7.3259 15.4892 7.75588C15.5317 8.18586 15.4942 8.52334 15.5154 9.01707C15.5367 9.51079 15.6467 9.88077 15.6217 10.1683C15.5692 10.7882 15.3317 10.9245 15.2679 11.3544C15.2029 11.7844 14.928 12.2869 14.7892 12.5606C14.4142 13.3018 13.7255 13.7031 13.4355 13.8956C12.9318 14.2343 4.90973 14.103 4.90973 14.103Z" fill="#FFA726"/>
<path d="M3.13976 8.74452C2.69603 8.68577 2.29105 9.13949 2.3198 9.67322C2.3498 10.2069 2.70478 10.3844 3.08976 10.2857C3.57724 10.1607 3.75098 9.71321 3.74098 9.38698C3.73098 9.06075 3.58224 8.80326 3.13976 8.74452Z" fill="#F27B00"/>
<path d="M5.95848 5.41845C5.56975 5.10347 5.71225 4.67474 5.92224 4.40225C6.13222 4.12977 6.71094 4.00353 6.95218 4.4235C7.19342 4.84348 7.01343 5.13597 6.82219 5.33596C6.6347 5.53344 6.21722 5.62844 5.95848 5.41845Z" fill="#F27B00"/>
<path d="M7.60341 4.27854C7.7709 4.61477 8.24462 4.67352 8.56086 4.49603C8.87709 4.31854 8.98583 3.97231 8.79834 3.65732C8.61335 3.34609 8.13713 3.2036 7.7909 3.47983C7.44592 3.75482 7.50466 4.08105 7.60341 4.27854Z" fill="#F27B00"/>
<path d="M9.54186 5.50722C9.69561 5.8622 10.1331 5.9197 10.4718 5.75096C10.8105 5.58222 11.0493 5.13099 10.8468 4.78226C10.6431 4.43478 10.1868 4.40353 9.8831 4.61352C9.57936 4.82351 9.37062 5.11224 9.54186 5.50722Z" fill="#F27B00"/>
<path d="M11.397 4.59352C11.6807 4.82101 12.1757 4.68977 12.3569 4.35478C12.4956 4.09855 12.5719 3.80356 12.2857 3.51733C11.9994 3.23109 11.5319 3.35859 11.317 3.66107C11.1482 3.90106 11.1007 4.35728 11.397 4.59352Z" fill="#F27B00"/>
<path d="M4.47722 6.2734C4.26473 6.2884 4.29598 6.72463 4.28598 7.3471C4.27598 7.94207 4.15849 9.19075 4.22474 10.5307C4.25223 11.0944 4.19349 12.6843 4.24723 13.483C4.30223 14.283 4.26973 15.1767 4.26973 15.1767C4.26973 15.1767 5.55092 16.5041 8.99448 16.5541C12.6218 16.6066 14.1017 14.8717 14.1017 14.8717C14.1017 14.8717 14.0617 12.6831 14.0655 11.8219C14.0705 10.5482 13.9917 8.12581 13.9917 7.73458C13.9917 7.34335 14.0242 6.4209 13.883 6.2959C13.7417 6.17091 12.738 6.22716 11.8231 6.24966C11.0044 6.26965 8.14828 6.27715 7.09833 6.27715C6.04839 6.27715 4.91595 6.24216 4.47722 6.2734Z" fill="#3C3C3C"/>
<path d="M4.96974 7.12343C4.83599 7.20467 4.81349 8.28212 4.84474 9.28581C4.87599 10.2895 4.87599 11.2445 5.20472 11.307C5.53346 11.3695 5.86219 10.6332 6.20717 10.3508C6.55215 10.0683 6.95963 9.84953 7.22587 9.4583C7.4921 9.06707 7.78959 8.43961 7.93083 8.12712C8.07207 7.81339 8.5108 7.39091 8.4633 7.13968C8.4158 6.88844 6.88088 6.98343 6.41091 6.98343C5.94093 6.98343 5.25222 6.95219 4.96974 7.12343Z" fill="#858585"/>
<path d="M9.34083 6.96717C9.14209 7.10841 8.93335 7.65588 8.79211 7.90712C8.65087 8.15836 8.25964 8.70583 8.55712 8.89457C8.85461 9.08206 9.44957 8.64333 9.63831 8.51834C9.8258 8.39335 10.077 8.33085 10.1708 8.2521C10.2645 8.17336 10.7345 7.48464 10.8445 7.37465C10.9545 7.26466 11.1895 7.13966 11.1895 7.01467C11.1895 6.88967 10.6095 6.88968 10.0933 6.90467C9.57707 6.91967 9.45082 6.88843 9.34083 6.96717Z" fill="#858585"/>
<path d="M7.53947 10.147C7.39448 10.3445 7.14824 10.7733 7.42948 10.977C7.71196 11.1807 8.02445 10.9607 8.24444 10.7258C8.46442 10.4908 8.57317 10.177 8.30693 9.8958C8.0407 9.61456 7.71071 9.91205 7.53947 10.147Z" fill="#858585"/>
<path d="M6.5997 11.2757C6.44346 11.2632 6.11347 11.762 5.81599 11.887C5.51851 12.012 5.06353 11.9345 4.98603 12.2157C4.90729 12.4982 4.7348 13.6881 4.98603 13.8294C5.23727 13.9706 5.92598 13.7356 6.08223 13.6419C6.23847 13.5481 6.22347 13.3281 6.53595 13.1244C6.84969 12.9207 6.89593 12.3882 6.91218 12.012C6.92843 11.6357 6.80219 11.2907 6.5997 11.2757Z" fill="#858585"/>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,21 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.2395 13.9305C13.2395 13.9305 13.2283 15.2642 13.2283 15.5292C13.2283 15.7941 13.3783 15.9316 13.5508 15.9204C13.7232 15.9091 14.552 15.9204 14.7357 15.9204C14.9194 15.9204 15.1269 15.7479 15.1269 15.4954C15.1269 15.2429 15.1619 12.988 15.1619 12.988L13.2395 13.9305Z" fill="#4E433D"/>
<path d="M4.42997 13.1492L2.60132 12.8967C2.60132 12.8967 2.59007 15.2316 2.57882 15.4503C2.56757 15.6691 2.69381 15.8278 2.87756 15.8416C3.20004 15.8641 3.80876 15.8641 4.02749 15.8528C4.24623 15.8416 4.39497 15.6803 4.40747 15.4728C4.41997 15.2654 4.40747 14.0242 4.40747 14.0242L4.42997 13.1492Z" fill="#4E433D"/>
<path d="M14.0606 8.08963L15.2093 8.03838C15.2093 8.03838 15.638 8.71959 15.5843 11.0245C15.533 13.2131 14.9681 14.0681 14.4681 14.1893C13.9681 14.3105 12.0807 14.4968 9.09088 14.5143C6.10103 14.5318 3.8799 14.3605 3.45242 14.2581C3.02495 14.1556 2.56247 13.5231 2.51247 13.0619C2.43623 12.3544 2.43123 11.4032 2.46497 10.0533C2.49872 8.70334 2.79246 8.02588 2.79246 8.02588L14.0606 8.08963Z" fill="#474C4F"/>
<path d="M4.35115 11.8981C3.70369 11.7906 2.76374 11.3831 2.69249 11.6831C2.62125 11.9831 2.90748 12.3406 3.5937 12.5693C4.27991 12.7981 5.85233 12.9693 9.15465 12.8981C12.457 12.8268 13.5719 12.7406 14.2294 12.5556C14.8868 12.3693 15.6443 11.8694 15.4018 11.5831C15.1593 11.2969 14.5156 11.7119 13.9294 11.8256C13.3432 11.9394 11.1408 12.0006 9.11215 12.0406C6.91102 12.0831 5.03737 12.0118 4.35115 11.8981Z" fill="#858585"/>
<path d="M5.57114 10.0206C5.3574 9.96435 2.70504 9.9031 2.58879 10.0281C2.53755 10.0831 2.55005 10.8793 2.56004 11.0243C2.57004 11.1693 2.68129 11.2193 2.87378 11.2293C3.06752 11.2393 5.54239 11.258 5.70613 11.2393C5.86987 11.2205 5.90862 11.0555 5.87987 10.9105C5.85112 10.7643 5.75488 10.0693 5.57114 10.0206Z" fill="#FEA826"/>
<path d="M15.3544 9.89699C15.2331 9.82574 12.542 9.93324 12.4558 10.0107C12.3683 10.0882 11.902 11.0507 12.137 11.1419C12.3595 11.2294 15.2681 11.1832 15.3644 11.1344C15.5143 11.0594 15.4694 9.96449 15.3544 9.89699Z" fill="#FEA826"/>
<path d="M6.95345 11.4419C6.69221 11.4544 6.32473 11.3156 6.16974 10.9107C6.01475 10.5044 5.996 10.2632 6.0435 10.0507C6.09224 9.83822 6.23724 9.76072 6.57472 9.73198C6.91345 9.70323 7.57092 9.65448 8.98209 9.64448C10.3945 9.63448 11.3707 9.66323 11.622 9.69323C11.8732 9.72198 12.0182 9.80947 11.9794 10.0707C11.9407 10.3319 11.8057 10.8544 11.6994 11.1057C11.622 11.2894 11.3807 11.4244 11.0607 11.4344C10.7432 11.4419 7.15719 11.4319 6.95345 11.4419Z" fill="#2F2F2F"/>
<path d="M14.668 7.41965L15.0442 7.42965C15.0442 7.42965 15.0529 6.97717 15.1042 6.83718C15.1554 6.69719 15.2592 6.62219 15.4942 6.62219C15.7292 6.62219 16.1904 6.63969 16.3454 6.64719C16.5004 6.65469 16.8016 6.91968 16.8166 7.31716C16.8316 7.71464 16.5591 7.96462 16.2866 7.99462C16.0141 8.02462 14.9917 7.99462 14.9917 7.99462L14.668 7.41965Z" fill="#474C4F"/>
<path d="M3.45106 7.35714L3.03358 7.38339C3.03358 7.38339 3.04608 6.97466 3.01733 6.82717C2.98858 6.67968 2.81109 6.59219 2.6786 6.58469C2.54611 6.57719 1.92739 6.57719 1.75115 6.59968C1.57491 6.62218 1.27242 6.79842 1.27992 7.24715C1.28742 7.69588 1.57491 7.94586 1.81739 7.93961C2.05988 7.93336 2.64735 7.95211 2.82609 7.94711C3.05483 7.93961 3.5548 7.96211 3.5548 7.96211L3.45106 7.35714Z" fill="#474C4F"/>
<path d="M7.95972 3.09839L7.97847 3.68711H10.3571L10.3758 3.11714L7.95972 3.09839Z" fill="#474C4F"/>
<path d="M11.6233 3.07858C11.6233 3.22357 11.6233 3.84229 11.6233 3.84229L12.407 3.86104L12.3583 3.04858L11.6233 3.07858Z" fill="#474C4F"/>
<path d="M5.73486 3.83223C5.75361 3.71598 5.76361 2.89478 5.76361 2.89478L6.60482 3.01102L6.57607 3.76473L5.73486 3.83223Z" fill="#474C4F"/>
<path d="M2.7888 8.02582C2.81005 7.98582 3.06754 7.89458 3.22253 7.62209C3.33128 7.43085 3.64751 6.40341 4.06374 5.5822C4.47996 4.76099 4.65371 4.40226 4.76995 4.21852C4.88619 4.03478 5.05993 3.88979 5.35992 3.80229C5.6599 3.7148 6.70485 3.47856 9.21846 3.52731C11.7321 3.57606 12.3695 3.65105 12.6145 3.7298C12.812 3.79229 13.182 3.98603 13.4232 4.43101C13.6645 4.87599 14.6882 7.15962 14.7857 7.36211C14.8819 7.5646 15.2106 8.03582 15.2106 8.03582C15.2106 8.03582 14.3419 8.93952 12.3121 9.17451C10.4871 9.38575 7.83729 9.57449 5.10118 9.10202C2.9538 8.73453 2.76756 8.06582 2.7888 8.02582Z" fill="#E3E4DE"/>
<path d="M5.01991 4.55725C4.79743 4.77974 4.2962 5.87968 4.04747 6.51215C3.79873 7.14461 3.65249 7.66458 3.99122 7.67583C4.32995 7.68708 7.55103 7.64209 8.97596 7.63084C10.4009 7.61959 13.802 7.69833 14.0507 7.69833C14.2994 7.69833 14.4457 7.48334 14.3332 7.21211C14.2207 6.94087 13.3832 5.08722 13.192 4.78224C12.9995 4.47725 12.7283 4.37476 12.2533 4.35226C11.7783 4.32976 10.0034 4.22852 8.78347 4.22852C7.56353 4.22852 5.22365 4.35476 5.01991 4.55725Z" fill="#2D2E35"/>
<path d="M4.26346 7.15069C4.31221 7.22444 7.3708 7.0907 9.2132 7.12444C11.0556 7.15819 13.5192 7.21444 13.6442 7.20319C13.768 7.19194 13.7805 7.0107 13.768 6.95445C13.7567 6.89821 13.5417 6.45698 13.5417 6.45698C13.5417 6.45698 12.423 5.1908 12.3318 5.1908C12.2418 5.1908 9.07696 5.39454 9.07696 5.39454L5.51715 5.29329L4.44345 6.61822C4.44345 6.61822 4.24096 7.11694 4.26346 7.15069Z" fill="#96C8ED"/>
<path d="M4.4436 6.61844C4.4436 6.61844 4.94233 5.42475 5.14357 5.12227C5.22231 5.00478 5.27981 4.89603 5.55105 4.86228C5.82228 4.82853 7.22346 4.6248 9.09961 4.65854C10.9758 4.69229 12.0607 4.76729 12.2869 4.78229C12.5544 4.79979 12.7394 4.89478 12.8407 5.06477C12.9419 5.23476 13.6544 6.69219 13.6544 6.69219L12.5044 6.58719C12.5044 6.58719 12.4969 6.05722 12.4794 5.87473C12.4644 5.71724 12.3907 5.58724 12.1494 5.5735C11.9345 5.56225 11.4595 5.551 11.302 5.56225C11.1432 5.5735 11.0358 5.70474 11.0045 5.92973C10.9733 6.15596 10.9408 6.55969 10.9408 6.55969L7.3797 6.58094C7.3797 6.58094 7.3797 6.02347 7.3472 5.82223C7.32345 5.67724 7.23221 5.58224 7.04597 5.58349C6.82973 5.58474 6.41975 5.56225 6.12601 5.59599C5.98977 5.61224 5.88853 5.77723 5.87728 6.01472C5.86853 6.18596 5.85478 6.56844 5.85478 6.56844L4.4436 6.61844Z" fill="#AFE3FB"/>
<path d="M8.05607 1.98621H10.3285L10.7434 2.73117L10.3085 3.27239H8.05607L7.62109 2.59492L8.05607 1.98621Z" fill="#CECECF"/>
<path d="M10.3283 1.98621C10.3283 1.98621 12.2807 2.01495 12.4169 2.03495C12.5532 2.05495 12.6882 2.21869 12.7069 2.41243C12.7257 2.60617 12.6882 3.0799 12.6882 3.16614C12.6882 3.25239 12.5769 3.30489 12.3944 3.30489C12.2107 3.30489 10.3108 3.27239 10.3108 3.27239L10.3283 1.98621Z" fill="#DD0C26"/>
<path d="M5.42603 3.02115C5.42603 3.1474 5.43603 3.24364 5.56102 3.25364C5.68726 3.26364 8.05589 3.27239 8.05589 3.27239V1.98621C8.05589 1.98621 5.89975 1.98621 5.74476 1.98621C5.58977 1.98621 5.42603 2.16995 5.42603 2.31494C5.42603 2.51743 5.42603 3.02115 5.42603 3.02115Z" fill="#0F64C7"/>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -0,0 +1,15 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.7991 15.0355C15.7317 14.6755 15.4717 12.6843 15.4717 12.6843H2.52735C2.52735 12.6843 2.26736 14.6755 2.19987 15.0355C2.13237 15.3954 2.90358 16.4991 8.99951 16.4991C15.0954 16.4991 15.8666 15.3954 15.7991 15.0355Z" fill="#82AEC0"/>
<path d="M2.52368 12.7445C2.52368 12.0107 5.42353 11.4158 8.99959 11.4158C12.5757 11.4158 15.4755 12.0107 15.4755 12.7445C15.4755 13.4782 12.5757 14.1944 8.99959 14.1944C5.42353 14.1944 2.52368 13.4782 2.52368 12.7445Z" fill="#E0E0E0"/>
<path d="M13.2217 3.03992C12.8667 1.90873 11.6843 1.5 8.99816 1.5C6.31205 1.5 5.13087 1.90873 4.77589 3.03992C4.06092 5.31855 3.41846 12.5032 3.41846 12.5032C3.41846 12.5032 3.97718 13.4394 8.99941 13.4394C14.0216 13.4394 14.5816 12.5044 14.5816 12.5044C14.5816 12.5044 13.9367 5.31855 13.2217 3.03992Z" fill="#F44336"/>
<path opacity="0.9" d="M3.41724 12.517C3.41724 11.9421 5.91585 11.4758 8.99944 11.4758C12.083 11.4758 14.5816 11.9421 14.5816 12.517C14.5816 13.092 12.083 13.6532 8.99944 13.6532C5.91585 13.6532 3.41724 13.092 3.41724 12.517Z" fill="#C62828"/>
<path d="M10.0731 5.46596L12.8017 2.64986C12.8542 2.60361 12.9317 2.66236 12.9017 2.72486L10.8718 5.98218C10.7206 6.29717 10.8543 6.50091 11.2043 6.49216L13.7854 6.46591C13.8554 6.46466 13.8767 6.55965 13.8129 6.5884L11.4581 7.49585C11.1393 7.6396 11.0718 7.98083 11.3443 8.19832L14.2804 10.3157C14.3354 10.3595 14.2904 10.4457 14.2229 10.427L10.8818 9.04327C10.5456 8.94828 10.3919 9.13452 10.4606 9.477L11.5381 13.0881C11.5518 13.1568 11.4618 13.1943 11.4231 13.1368L9.58441 9.88073C9.38817 9.59199 9.15068 9.537 8.98444 9.84323L7.22079 13.1781C7.18704 13.2393 7.09454 13.2106 7.10079 13.1406L8.01199 9.542C8.04699 9.19452 7.887 9.12202 7.56202 9.24951L3.78097 11.1882C3.71597 11.2132 3.66347 11.1319 3.71347 11.0832L6.8858 8.44955C7.13579 8.20582 7.12454 7.93958 6.79331 7.82834L4.14595 6.92588C4.0797 6.90339 4.0922 6.80714 4.16095 6.80214L6.76581 6.85214C7.11454 6.82589 7.29078 6.47841 7.10829 6.18092L5.01465 3.16233C4.9784 3.10234 5.04965 3.03609 5.1059 3.07734L7.89825 5.50221C8.18199 5.70595 8.43072 5.6872 8.48197 5.34222L8.9357 2.09239C8.9457 2.02364 9.04319 2.01864 9.06069 2.08614L9.45567 5.25847C9.54316 5.5972 9.81065 5.6972 10.0731 5.46596Z" fill="url(#paint0_radial_716_535)"/>
<path d="M9.23063 5.25724L10.4006 3.1461C10.4356 3.08611 10.5281 3.11735 10.5193 3.1861L10.1543 5.49223C10.1118 5.83846 10.3043 5.9897 10.6318 5.86971L12.9229 5.09975C12.9879 5.076 13.0392 5.1585 12.9879 5.20599L11.193 6.73841C10.9368 6.9759 10.9818 7.32088 11.3105 7.43963L13.6004 8.21584C13.6654 8.23958 13.6516 8.33583 13.5816 8.33958L11.1418 8.38833C10.793 8.40583 10.7068 8.63206 10.8818 8.93455L12.1142 11.0394C12.1492 11.0994 12.0755 11.1644 12.0205 11.1219L10.1793 9.59701C9.90059 9.38702 9.65936 9.40952 9.59936 9.75325L9.16188 12.2456C9.14938 12.3144 9.05189 12.3169 9.03689 12.2494L8.58191 9.77825C8.50442 9.43827 8.32943 9.42077 8.06194 9.64451L6.12454 11.2732C6.0708 11.3182 5.99455 11.2569 6.02705 11.1957L7.16574 9.10329C7.32448 8.79206 7.22948 8.54457 6.8795 8.54457L4.42213 8.57582C4.35214 8.57582 4.33339 8.48082 4.39713 8.45332L6.54202 7.62712C6.86325 7.49087 6.9195 7.10589 6.65201 6.88091L4.85211 5.41473C4.79836 5.36974 4.84586 5.28474 4.91211 5.30474L7.18324 5.9847C7.51697 6.0872 7.74696 5.9897 7.68571 5.64597L7.21448 3.26985C7.20199 3.2011 7.29323 3.1661 7.33073 3.2236L8.58191 5.25599C8.77065 5.54973 9.05689 5.55973 9.23063 5.25724Z" fill="#FFD5CA"/>
<defs>
<radialGradient id="paint0_radial_716_535" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(8.99962 7.62467) scale(5.44854)">
<stop offset="0.5718" stop-color="#FF6E40"/>
<stop offset="0.7682" stop-color="#FF7046" stop-opacity="0.5414"/>
<stop offset="1" stop-color="#FF7555" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -12,5 +12,4 @@
import '@signozhq/design-tokens';
import '@signozhq/icons';
import '@signozhq/resizable';
import '@signozhq/ui';

View File

@@ -3,6 +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 { PANEL_TYPES } from 'constants/queryBuilder';
import { useExportRawData } from 'hooks/useExportData/useServerExport';
import { Download, LoaderCircle } from '@signozhq/icons';
import { DataSource } from 'types/common/queryBuilder';
@@ -18,11 +19,13 @@ import './DownloadOptionsMenu.styles.scss';
interface DownloadOptionsMenuProps {
dataSource: DataSource;
selectedColumns?: TelemetryFieldKey[];
panelType?: PANEL_TYPES;
}
export default function DownloadOptionsMenu({
dataSource,
selectedColumns,
panelType,
}: DownloadOptionsMenuProps): JSX.Element {
const [exportFormat, setExportFormat] = useState<string>(DownloadFormats.CSV);
const [rowLimit, setRowLimit] = useState<number>(DownloadRowCounts.TEN_K);
@@ -33,6 +36,7 @@ export default function DownloadOptionsMenu({
const { isDownloading, handleExportRawData } = useExportRawData({
dataSource,
panelType,
});
const handleExport = useCallback(async (): Promise<void> => {

View File

@@ -0,0 +1,33 @@
.export-menu-popover {
width: 240px;
padding: 0 12px 12px 12px;
.title {
display: flex;
color: var(--l1-foreground);
font-family: Inter;
font-size: var(--periscope-font-size-small);
font-style: normal;
font-weight: 500;
line-height: 18px;
letter-spacing: 0.88px;
text-transform: uppercase;
margin-bottom: 8px;
}
.export-format {
padding: 12px 4px;
display: flex;
flex-direction: column;
// radio option labels — radix popover content inherits the root font
// size; pin to the app's 13px base the antd popover used to impose
label {
font-size: var(--periscope-font-size-base);
}
}
.export-button {
width: 100%;
}
}

View File

@@ -0,0 +1,92 @@
import { Download } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui/popover';
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { Typography } from '@signozhq/ui/typography';
import {
ClientExportData,
useClientExport,
} from 'hooks/useExportData/useClientExport';
import { ExportFormat } from 'lib/exportData/types';
import { useCallback, useState } from 'react';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import './ExportMenu.styles.scss';
interface ExportMenuProps {
dataSource: DataSource;
// The queryRange response object the view holds — the hook picks the
// serializer (timeseries / table) from what it carries.
data: ClientExportData;
query?: Query;
yAxisUnit?: string;
fileName?: string;
}
// Download menu for in-memory query results (client-side serialization).
// The raw/list backend export keeps its own menu in DownloadOptionsMenu.
export default function ExportMenu({
dataSource,
data,
query,
yAxisUnit,
fileName,
}: ExportMenuProps): JSX.Element {
const [exportFormat, setExportFormat] = useState<string>(ExportFormat.Csv);
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
const { isExporting, handleExport: handleClientExport } = useClientExport({
data,
query,
yAxisUnit,
fileName,
});
const handleExport = useCallback((): void => {
setIsPopoverOpen(false);
handleClientExport({ format: exportFormat as ExportFormat });
}, [exportFormat, handleClientExport]);
return (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<TooltipSimple title="Download">
<PopoverTrigger asChild>
<Button
variant="ghost"
color="secondary"
size="icon"
aria-label="Download"
data-testid={`export-menu-${dataSource}`}
disabled={isExporting}
loading={isExporting}
>
<Download size={14} />
</Button>
</PopoverTrigger>
</TooltipSimple>
<PopoverContent align="end" className="export-menu-popover">
<div className="export-format">
<Typography.Text className="title">FORMAT</Typography.Text>
<RadioGroup value={exportFormat} onChange={setExportFormat}>
<RadioGroupItem value={ExportFormat.Csv}>csv</RadioGroupItem>
<RadioGroupItem value={ExportFormat.Jsonl}>jsonl</RadioGroupItem>
</RadioGroup>
</div>
<Button
variant="solid"
color="primary"
className="export-button"
onClick={handleExport}
disabled={isExporting}
loading={isExporting}
prefix={<Download size={16} />}
>
Export
</Button>
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,85 @@
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
import { fireEvent, render, screen } from 'tests/test-utils';
import { DataSource } from 'types/common/queryBuilder';
import ExportMenu from '../ExportMenu';
const mockHandleExport = jest.fn();
let mockIsExporting = false;
jest.mock('hooks/useExportData/useClientExport', () => ({
useClientExport: (): unknown => ({
isExporting: mockIsExporting,
handleExport: mockHandleExport,
}),
}));
const data = {
statusCode: 200,
error: null,
message: '',
payload: { data: { result: [], resultType: 'time_series' } },
} as unknown as MetricQueryRangeSuccessResponse;
const TEST_ID = `export-menu-${DataSource.LOGS}`;
function renderMenu(): void {
render(
<ExportMenu
dataSource={DataSource.LOGS}
data={data}
fileName="logs-timeseries"
/>,
);
}
describe('ExportMenu', () => {
beforeEach(() => {
mockHandleExport.mockReset();
mockIsExporting = false;
});
it('renders the download trigger button', () => {
renderMenu();
expect(screen.getByTestId(TEST_ID)).toBeInTheDocument();
});
it('shows only format options — no shape, row-count, or column controls', () => {
renderMenu();
fireEvent.click(screen.getByTestId(TEST_ID));
expect(screen.getByText('FORMAT')).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'csv' })).toBeInTheDocument();
expect(screen.getByRole('radio', { name: 'jsonl' })).toBeInTheDocument();
expect(screen.queryByText('Number of Rows')).not.toBeInTheDocument();
expect(screen.queryByText('Columns')).not.toBeInTheDocument();
expect(screen.queryByRole('radio', { name: 'long' })).not.toBeInTheDocument();
expect(screen.queryByRole('radio', { name: 'wide' })).not.toBeInTheDocument();
});
it('exports as csv by default', () => {
renderMenu();
fireEvent.click(screen.getByTestId(TEST_ID));
fireEvent.click(screen.getByText('Export'));
expect(mockHandleExport).toHaveBeenCalledTimes(1);
expect(mockHandleExport).toHaveBeenCalledWith({ format: 'csv' });
});
it('exports as jsonl when selected', () => {
renderMenu();
fireEvent.click(screen.getByTestId(TEST_ID));
fireEvent.click(screen.getByRole('radio', { name: 'jsonl' }));
fireEvent.click(screen.getByText('Export'));
expect(mockHandleExport).toHaveBeenCalledWith({ format: 'jsonl' });
});
it('disables the trigger while an export is in progress', () => {
mockIsExporting = true;
renderMenu();
expect(screen.getByTestId(TEST_ID)).toBeDisabled();
});
});

View File

@@ -20,7 +20,7 @@ import { Globe, Inbox, SquarePen } from '@signozhq/icons';
import AnnouncementsModal from './AnnouncementsModal';
import FeedbackModal from './FeedbackModal';
import ShareURLModal from './ShareURLModal';
import ShareURLModal, { type ShareURLExtraOption } from './ShareURLModal';
import './HeaderRightSection.styles.scss';
import { Typography } from '@signozhq/ui/typography';
@@ -29,12 +29,15 @@ interface HeaderRightSectionProps {
enableAnnouncements: boolean;
enableShare: boolean;
enableFeedback: boolean;
/** Optional page-specific toggle for the share dialog (e.g. "Include variables"). */
shareModalExtraOption?: ShareURLExtraOption;
}
function HeaderRightSection({
enableAnnouncements,
enableShare,
enableFeedback,
shareModalExtraOption,
}: HeaderRightSectionProps): JSX.Element | null {
const location = useLocation();
@@ -185,7 +188,7 @@ function HeaderRightSection({
rootClassName="header-section-popover-root"
className="shareable-link-popover"
placement="bottomRight"
content={<ShareURLModal />}
content={<ShareURLModal extraOption={shareModalExtraOption} />}
open={openShareURLModal}
destroyTooltipOnHide
arrow={false}

View File

@@ -24,7 +24,22 @@ const routesToBeSharedWithTime = [
ROUTES.METER_EXPLORER,
];
function ShareURLModal(): JSX.Element {
/**
* An optional, page-specific toggle in the share dialog (e.g. a dashboard's
* "Include variables"). When enabled, `apply` mutates the URL params that go into
* the shared link. Keeps this shared modal generic — the page owns what it adds.
*/
export interface ShareURLExtraOption {
label: string;
defaultEnabled?: boolean;
apply: (params: URLSearchParams) => void;
}
interface ShareURLModalProps {
extraOption?: ShareURLExtraOption;
}
function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
const urlQuery = useUrlQuery();
const location = useLocation();
const { selectedTime } = useSelector<AppState, GlobalReducer>(
@@ -34,6 +49,9 @@ function ShareURLModal(): JSX.Element {
const [enableAbsoluteTime, setEnableAbsoluteTime] = useState(
selectedTime !== 'custom',
);
const [enableExtraOption, setEnableExtraOption] = useState(
extraOption?.defaultEnabled ?? false,
);
const startTime = urlQuery.get(QueryParams.startTime);
const endTime = urlQuery.get(QueryParams.endTime);
@@ -93,6 +111,11 @@ function ShareURLModal(): JSX.Element {
}
}
if (extraOption && enableExtraOption) {
extraOption.apply(urlQuery);
currentUrl = getAbsoluteUrl(`${location.pathname}?${urlQuery.toString()}`);
}
return currentUrl;
};
@@ -143,6 +166,20 @@ function ShareURLModal(): JSX.Element {
</>
)}
{extraOption && (
<div className="absolute-relative-time-toggler-container">
<Typography.Text className="absolute-relative-time-toggler-label">
{extraOption.label}
</Typography.Text>
<div className="absolute-relative-time-toggler">
<Switch
value={enableExtraOption}
onChange={(): void => setEnableExtraOption((prev) => !prev)}
/>
</div>
</div>
)}
<div className="share-link">
<div className="url-share-container">
<div className="url-share-container-header">

View File

@@ -713,6 +713,19 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
}
};
// Row buttons select without letting the wrapping checkbox also toggle:
// stop propagation, run the selection, then drop the active/chip focus.
const selectFromButton = (
e: React.MouseEvent,
source: 'option' | 'checkbox',
): void => {
e.stopPropagation();
e.preventDefault();
handleItemSelection(source);
setActiveChipIndex(-1);
setActiveIndex(-1);
};
return (
<div
key={option.value || `option-${index}`}
@@ -726,13 +739,6 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
selected: isSelected,
active: isActive,
})}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
handleItemSelection('option');
setActiveChipIndex(-1);
setActiveIndex(-1);
}}
onKeyDown={(e): void => {
if ((e.key === 'Enter' || e.key === SPACEKEY) && isActive) {
e.stopPropagation();
@@ -752,13 +758,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
<Checkbox
value={isSelected}
className="option-checkbox"
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
handleItemSelection('checkbox');
setActiveChipIndex(-1);
setActiveIndex(-1);
}}
onClick={(e): void => selectFromButton(e, 'checkbox')}
>
<div className="option-content">
<Typography.Text truncate={1} className="option-label-text">
@@ -768,11 +768,19 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
<div className="option-badge">{capitalize(option.type)}</div>
)}
{option.value && ensureValidOption(option.value) && (
<Button type="text" className="only-btn">
<Button
type="text"
className="only-btn"
onClick={(e): void => selectFromButton(e, 'option')}
>
{currentToggleTagValue({ option: option.value })}
</Button>
)}
<Button type="text" className="toggle-btn">
<Button
type="text"
className="toggle-btn"
onClick={(e): void => selectFromButton(e, 'checkbox')}
>
Toggle
</Button>
</div>

View File

@@ -656,6 +656,28 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
it('UI-03b: clicking "Only" selects just that value', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
onChange={mockOnChange}
value={['frontend']}
/>,
);
const combobox = screen.getByRole('combobox');
await user.click(combobox);
// "Only" renders for each non-selected row (hidden until hover via CSS,
// which jsdom doesn't apply, so it's clickable here).
const onlyButtons = await screen.findAllByText('Only');
mockOnChange.mockClear();
await user.click(onlyButtons[0]);
expect(mockOnChange).toHaveBeenCalledTimes(1);
expect(mockOnChange.mock.calls[0][0]).toStrictEqual(['backend']);
});
it('UI-04: Should display values with loading info at bottom', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} loading />,
@@ -1449,7 +1471,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(mockOnChange).toHaveBeenCalledWith(
['custom-value'],
[{ label: 'custom-value', value: 'custom-value' }],
[{ label: 'custom-value', value: 'custom-value', type: 'custom' }],
);
});
});

View File

@@ -0,0 +1,21 @@
import { prioritizeOrAddOptionForMultiSelect } from '../utils';
describe('prioritizeOrAddOptionForMultiSelect ordering', () => {
it('hoists selected then preserves the given (sorted) order in each group', () => {
const sorted = ['apple', 'banana', 'cherry', 'date', 'elderberry'].map(
(v) => ({ label: v, value: v }),
);
// selection given in a non-sorted order on purpose
const result = prioritizeOrAddOptionForMultiSelect(sorted, [
'date',
'banana',
]);
expect(result.map((o) => o.value)).toStrictEqual([
'banana',
'date',
'apple',
'cherry',
'elderberry',
]);
});
});

View File

@@ -458,7 +458,9 @@ $custom-border-color: #2c3044;
.option-item {
padding: 8px 12px;
cursor: pointer;
// Not the whole row — only the checkbox and the action buttons get the
// pointer (set below), so inert areas don't look clickable.
cursor: default;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -494,6 +496,13 @@ $custom-border-color: #2c3044;
.option-checkbox {
width: 100%;
cursor: default;
// The checkbox button is the only pointer target on the left; the label
// still toggles on click but keeps a default cursor.
> button {
cursor: pointer;
}
// @signozhq/ui Checkbox renders children inside a <label> that is
// content-sized by default. Make it fill the row (min-width: 0 lets it
@@ -535,9 +544,9 @@ $custom-border-color: #2c3044;
}
}
// Size the buttons to the row's resting content height (20px) and fully
// override antd's default 32px Button box, so revealing them on hover
// never changes the row height.
// "Only"/"All" is the primary action — a filled pill that reads as a
// button; "Toggle" is a secondary hint in plain text. Sized to the row's
// resting height so revealing them on hover never shifts it.
.only-btn,
.toggle-btn {
display: none;
@@ -545,14 +554,39 @@ $custom-border-color: #2c3044;
justify-content: center;
height: 18px;
min-height: 0;
padding: 0 6px;
font-size: 12px;
line-height: 1;
border: none;
box-shadow: none;
}
&:hover {
background-color: unset;
.only-btn {
padding: 4px 8px;
// Black interior + a visible border so the pill stands out clearly
// against the near-black row when revealed on hover.
border: 1px solid var(--l3-border);
border-radius: 3px;
background-color: var(--bg-ink-500, #0b0c0e);
color: var(--l1-foreground);
cursor: pointer;
}
.toggle-btn {
padding: 0 6px;
border: none;
background-color: transparent;
color: var(--l2-foreground);
cursor: pointer;
}
// Toggle appears over the checkbox area; "Only/All" takes over the row
// content and hides Toggle there (higher specificity wins).
&:hover {
.toggle-btn {
display: flex;
}
.option-badge {
display: none;
}
}
@@ -560,6 +594,7 @@ $custom-border-color: #2c3044;
.only-btn {
display: flex;
}
.toggle-btn {
display: none;
}
@@ -569,15 +604,6 @@ $custom-border-color: #2c3044;
}
}
}
.option-checkbox:hover {
.toggle-btn {
display: flex;
}
.option-badge {
display: none;
}
}
}
.loading-container {

View File

@@ -247,11 +247,6 @@
var(--l2-background)
) !important;
&,
.ͼ1a {
color: var(--query-builder-v2-color, var(--l2-foreground));
}
::-moz-selection {
background: var(
--query-builder-v2-selection-background-color,

View File

@@ -293,11 +293,6 @@ $max-recents-shown: 5;
var(--l2-background)
) !important;
&,
.ͼ1a {
color: var(--query-builder-v2-color, var(--l2-foreground));
}
::-moz-selection {
background: var(
--query-builder-v2-selection-background-color,

View File

@@ -52,6 +52,8 @@ import type { SignalType } from 'types/api/v5/queryRange';
import { queryExamples, SUGGESTIONS_SECTION } from './constants';
import {
combineInitialAndUserExpression,
dedupeOptionsByLabel,
getFieldContextPrefix,
getRecentOptions,
renderRecentDeleteButton,
} from './utils';
@@ -222,6 +224,12 @@ function QuerySearch({
QueryKeyDataSuggestionsProps[] | null
>(null);
// dedupe keySuggestions by label/name
const dedupedKeySuggestions = useMemo(
() => dedupeOptionsByLabel(keySuggestions || []),
[keySuggestions],
);
const [showExamples] = useState(false);
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
@@ -246,9 +254,11 @@ function QuerySearch({
[key: string]: QueryKeyDataSuggestionsProps[];
}): any[] =>
Object.values(keys).flatMap((items: QueryKeyDataSuggestionsProps[]) =>
items.map(({ name, fieldDataType }) => ({
items.map(({ name, fieldDataType, fieldContext }) => ({
label: name,
type: fieldDataType === 'string' ? 'keyword' : fieldDataType,
fieldContext,
fieldDataType,
info: '',
details: '',
})),
@@ -307,13 +317,17 @@ function QuerySearch({
if (response.data.data) {
const { keys } = response.data.data;
const options = generateOptions(keys);
// Use a Map to deduplicate by label and preserve order: new options take precedence
// Deduplicate by full variant identity (name + context + data type), NOT by
// label. deduping by label removes varient which is not expected. If we need
// to dedupe by label use dedupedKeySuggestions not dedupe the source itself
const variantId = (opt: QueryKeyDataSuggestionsProps): string =>
`${opt.label}|${opt.fieldContext ?? ''}|${opt.fieldDataType ?? ''}`;
const merged = new Map<string, QueryKeyDataSuggestionsProps>();
options.forEach((opt) => merged.set(opt.label, opt));
options.forEach((opt) => merged.set(variantId(opt), opt));
if (searchText && lastKeyRef.current !== searchText) {
(keySuggestions || []).forEach((opt) => {
if (!merged.has(opt.label)) {
merged.set(opt.label, opt);
if (!merged.has(variantId(opt))) {
merged.set(variantId(opt), opt);
}
});
}
@@ -919,8 +933,55 @@ function QuerySearch({
if (queryContext.isInKey) {
const searchText = word?.text.toLowerCase().trim() ?? '';
const fieldContextMatch = getFieldContextPrefix(searchText);
options = (keySuggestions || []).filter((option) =>
if (fieldContextMatch) {
const { context: fieldContext, remainder } = fieldContextMatch;
// Fetch the context's page when the prefix is typed exactly eg.("attribute.")
if (remainder === '' && lastFetchedKeyRef.current !== searchText) {
debouncedFetchKeySuggestions(searchText);
}
//suggestions that actually do start with <fieldContext>.
const nameMatches = (keySuggestions || [])
.filter((option) => option.label.toLowerCase().includes(searchText))
.map((option) => ({ ...option, boost: 100 }));
//suggestions which do not start with the prefix but qualifies for suggestion
const contextQualified = (keySuggestions || [])
.filter(
(option) =>
option.fieldContext === fieldContext &&
option.label.toLowerCase().includes(remainder),
)
.map((option) => ({
...option,
label: `${fieldContext}.${option.label}`,
boost: 0,
}));
const contextOptions = dedupeOptionsByLabel([
...nameMatches,
...contextQualified,
]);
// If contextOptions is empty fetch again.
if (
contextOptions.length === 0 &&
lastFetchedKeyRef.current !== searchText
) {
debouncedFetchKeySuggestions(searchText);
}
return {
from: word?.from ?? 0,
to: word?.to ?? cursorPos.ch,
options: addSpaceToOptions(contextOptions),
};
}
options = dedupedKeySuggestions.filter((option) =>
option.label.toLowerCase().includes(searchText),
);
@@ -969,9 +1030,26 @@ function QuerySearch({
// If we have a key context, add that info to the operator suggestions
if (keyName) {
// Find the key details from suggestions
const keyDetails = (keySuggestions || []).find((k) => k.label === keyName);
const keyType = keyDetails?.type || '';
const keyContextMatch = getFieldContextPrefix(keyName);
// key-suggestion can contain multiple variants of a single key
// In variants we capture ones that match the label to typed keyName exactly or,
// if it has a prefix fieldContext remove it and then match.
const variants = (keySuggestions || []).filter(
(k) =>
k.label === keyName ||
(keyContextMatch !== null &&
k.fieldContext === keyContextMatch.context &&
k.label === keyContextMatch.remainder),
);
const variantTypes = new Set(
variants
.map((k) =>
k.type === 'keyword' ? QUERY_BUILDER_KEY_TYPES.STRING : k.type,
)
.filter(Boolean),
);
//if there are multi-variant, show all suggestions else just the one
const keyType = variantTypes.size === 1 ? [...variantTypes][0] : '';
// Filter operators based on key type
if (keyType) {
@@ -1214,7 +1292,7 @@ function QuerySearch({
if (curChar === '(') {
// In expression context, suggest keys, functions, or nested parentheses
options = [
...(keySuggestions || []),
...dedupedKeySuggestions,
{ label: '(', type: 'parenthesis', info: 'Open nested group' },
{ label: 'NOT', type: 'operator', info: 'Negate expression' },
...options.filter((opt) => opt.type === 'function'),

View File

@@ -0,0 +1,120 @@
import {
combineInitialAndUserExpression,
dedupeOptionsByLabel,
getFieldContextPrefix,
getUserExpressionFromCombined,
} from '../utils';
describe('entityLogsExpression', () => {
describe('combineInitialAndUserExpression', () => {
it('returns user when initial is empty', () => {
expect(combineInitialAndUserExpression('', 'body contains error')).toBe(
'body contains error',
);
});
it('returns initial when user is empty', () => {
expect(combineInitialAndUserExpression('k8s.pod.name = "x"', '')).toBe(
'k8s.pod.name = "x"',
);
});
it('wraps user in parentheses with AND', () => {
expect(
combineInitialAndUserExpression('k8s.pod.name = "x"', 'body = "a"'),
).toBe('k8s.pod.name = "x" AND (body = "a")');
});
});
describe('getUserExpressionFromCombined', () => {
it('returns empty when combined equals initial', () => {
expect(
getUserExpressionFromCombined('k8s.pod.name = "x"', 'k8s.pod.name = "x"'),
).toBe('');
});
it('extracts user from wrapped form', () => {
expect(
getUserExpressionFromCombined(
'k8s.pod.name = "x"',
'k8s.pod.name = "x" AND (body = "a")',
),
).toBe('body = "a"');
});
it('extracts user from legacy AND without parens', () => {
expect(
getUserExpressionFromCombined(
'k8s.pod.name = "x"',
'k8s.pod.name = "x" AND body = "a"',
),
).toBe('body = "a"');
});
it('returns full combined when initial is empty', () => {
expect(getUserExpressionFromCombined('', 'service.name = "a"')).toBe(
'service.name = "a"',
);
});
});
});
describe('getFieldContextPrefix', () => {
it('matches a complete context prefix with a remainder', () => {
expect(getFieldContextPrefix('attribute.status')).toStrictEqual({
context: 'attribute',
remainder: 'status',
});
});
it('matches a bare context prefix with empty remainder', () => {
expect(getFieldContextPrefix('resource.')).toStrictEqual({
context: 'resource',
remainder: '',
});
});
it('matches every backend field context', () => {
['attribute', 'resource', 'span', 'body', 'log', 'metric'].forEach((ctx) => {
expect(getFieldContextPrefix(`${ctx}.x`)).toStrictEqual({
context: ctx,
remainder: 'x',
});
});
});
it('does not match a partial context name', () => {
expect(getFieldContextPrefix('attr')).toBeNull();
expect(getFieldContextPrefix('attribute')).toBeNull();
});
it('does not match a non-context key with dots', () => {
expect(getFieldContextPrefix('status.code')).toBeNull();
});
it('matches context case-insensitively but keeps remainder casing', () => {
expect(getFieldContextPrefix('Attribute.Status')).toStrictEqual({
context: 'attribute',
remainder: 'Status',
});
});
});
describe('dedupeOptionsByLabel', () => {
it('keeps the first occurrence per label, preserving order', () => {
expect(
dedupeOptionsByLabel([
{ label: 'status.code', type: 'keyword' },
{ label: 'status.code', type: 'number' },
{ label: 'duration', type: 'number' },
]),
).toStrictEqual([
{ label: 'status.code', type: 'keyword' },
{ label: 'duration', type: 'number' },
]);
});
it('returns an empty array for empty input', () => {
expect(dedupeOptionsByLabel([])).toStrictEqual([]);
});
});

View File

@@ -1,58 +0,0 @@
import {
combineInitialAndUserExpression,
getUserExpressionFromCombined,
} from '../utils';
describe('entityLogsExpression', () => {
describe('combineInitialAndUserExpression', () => {
it('returns user when initial is empty', () => {
expect(combineInitialAndUserExpression('', 'body contains error')).toBe(
'body contains error',
);
});
it('returns initial when user is empty', () => {
expect(combineInitialAndUserExpression('k8s.pod.name = "x"', '')).toBe(
'k8s.pod.name = "x"',
);
});
it('wraps user in parentheses with AND', () => {
expect(
combineInitialAndUserExpression('k8s.pod.name = "x"', 'body = "a"'),
).toBe('k8s.pod.name = "x" AND (body = "a")');
});
});
describe('getUserExpressionFromCombined', () => {
it('returns empty when combined equals initial', () => {
expect(
getUserExpressionFromCombined('k8s.pod.name = "x"', 'k8s.pod.name = "x"'),
).toBe('');
});
it('extracts user from wrapped form', () => {
expect(
getUserExpressionFromCombined(
'k8s.pod.name = "x"',
'k8s.pod.name = "x" AND (body = "a")',
),
).toBe('body = "a"');
});
it('extracts user from legacy AND without parens', () => {
expect(
getUserExpressionFromCombined(
'k8s.pod.name = "x"',
'k8s.pod.name = "x" AND body = "a"',
),
).toBe('body = "a"');
});
it('returns full combined when initial is empty', () => {
expect(getUserExpressionFromCombined('', 'service.name = "a"')).toBe(
'service.name = "a"',
);
});
});
});

View File

@@ -1,6 +1,16 @@
export const RECENTS_SECTION = { name: 'Recent searches', rank: 1 } as const;
export const SUGGESTIONS_SECTION = { name: 'Suggestions', rank: 2 } as const;
// TODO: move to using TelemetrytypesFieldContextDTO when we migrate getKeySuggestions API (Part of https://github.com/SigNoz/engineering-pod/issues/5289)
export const FIELD_CONTEXTS = [
'attribute',
'resource',
'span',
'body',
'log',
'metric',
] as const;
// Custom CodeMirror Completion.type for recent-query entries. Used to discriminate
// recents from regular autocomplete completions in renderers and event handlers.
export const RECENT_COMPLETION_TYPE = 'recent';

View File

@@ -9,11 +9,45 @@ import type { SignalType } from 'types/api/v5/queryRange';
import 'utils/timeUtils';
import {
FIELD_CONTEXTS,
RECENT_COMPLETION_TYPE,
RECENTS_DISPLAY_CAP,
RECENTS_SECTION,
} from './constants';
export interface FieldContextPrefixMatch {
context: string;
remainder: string;
}
// This function checks if the text(query key) starts with a fieldContext
// This util strictly checks that and returns context and remainder back.
// This helps differentiate if the typed key was prefixed with a context or
// was it an actual queryKey like (attribute.abc)
export function getFieldContextPrefix(
text: string,
): FieldContextPrefixMatch | null {
const lower = text.toLowerCase();
const context = FIELD_CONTEXTS.find((ctx) => lower.startsWith(`${ctx}.`));
return context ? { context, remainder: text.slice(context.length + 1) } : null;
}
// Keeps the first occurrence per label, preserving order. Key suggestions hold
// one entry per (name, fieldContext, fieldDataType) variant; This means query builder
// could show multiple labels and this avoids that.
export function dedupeOptionsByLabel<T extends { label: string }>(
options: T[],
): T[] {
const seen = new Set<string>();
return options.filter((option) => {
if (seen.has(option.label)) {
return false;
}
seen.add(option.label);
return true;
});
}
export function combineInitialAndUserExpression(
initial: string,
user: string,

View File

@@ -0,0 +1,217 @@
import { initialQueriesMap } from 'constants/queryBuilder';
import { rest, server } from 'mocks-server/server';
import { render, userEvent, waitFor } from 'tests/test-utils';
import { DataSource } from 'types/common/queryBuilder';
import QuerySearch from '../QuerySearch/QuerySearch';
import { mockCodeMirrorDomApis } from './codemirrorDomMocks';
const CM_EDITOR_SELECTOR = '.cm-editor .cm-content';
const CM_OPTION_SELECTOR = '.cm-tooltip-autocomplete .cm-completionLabel';
beforeAll(() => {
mockCodeMirrorDomApis();
});
jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: (): boolean => false,
}));
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): { dashboardData: undefined } => ({
dashboardData: undefined,
}),
}));
jest.mock('hooks/queryBuilder/useQueryBuilder', () => {
const handleRunQuery = jest.fn();
return {
__esModule: true,
useQueryBuilder: (): { handleRunQuery: () => void } => ({ handleRunQuery }),
handleRunQuery,
};
});
// Keys fixture: status.code exists as 3 variants (attribute string/number +
// resource string); attribute.a.b.c is a key literally named with the prefix.
const KEYS_FIXTURE = {
'status.code': [
{
name: 'status.code',
fieldContext: 'attribute',
fieldDataType: 'string',
signal: 'logs',
},
{
name: 'status.code',
fieldContext: 'attribute',
fieldDataType: 'number',
signal: 'logs',
},
{
name: 'status.code',
fieldContext: 'resource',
fieldDataType: 'string',
signal: 'logs',
},
],
'attribute.a.b.c': [
{
name: 'attribute.a.b.c',
fieldContext: 'attribute',
fieldDataType: 'string',
signal: 'logs',
},
],
'duration.nano': [
{
name: 'duration.nano',
fieldContext: 'span',
fieldDataType: 'number',
signal: 'logs',
},
],
};
const fetchedSearchTexts: string[] = [];
beforeEach(() => {
fetchedSearchTexts.length = 0;
server.use(
rest.get('http://localhost/api/v1/fields/keys', (req, res, ctx) => {
fetchedSearchTexts.push(req.url.searchParams.get('searchText') ?? '');
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: { complete: true, keys: KEYS_FIXTURE },
}),
);
}),
rest.get('http://localhost/api/v1/fields/values', (_req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: { values: { stringValues: [], numberValues: [] } },
}),
),
),
);
});
async function renderAndType(text: string): Promise<HTMLElement> {
render(
<QuerySearch
onChange={jest.fn() as jest.MockedFunction<(v: string) => void>}
queryData={initialQueriesMap.logs.builder.queryData[0]}
dataSource={DataSource.LOGS}
/>,
);
await waitFor(() => {
expect(document.querySelector(CM_EDITOR_SELECTOR)).toBeInTheDocument();
});
const editor = document.querySelector(CM_EDITOR_SELECTOR) as HTMLElement;
await userEvent.click(editor);
await userEvent.type(editor, text);
return editor;
}
// Types a key, waits for its suggestion to render (proving the debounced key
// fetch resolved into state), then types a space to enter operator context.
async function typeKeyThenEnterOperatorContext(
key: string,
expectedKeyLabel: string,
): Promise<void> {
const editor = await renderAndType(key);
await waitFor(() => {
expect(visibleOptionLabels()).toContain(expectedKeyLabel);
});
// skipClick: a click would reset the caret to position 0 under the mocked
// DOM rects; we need the space appended at the end of the typed key.
await userEvent.type(editor, ' ', { skipClick: true });
await waitFor(() => {
expect(visibleOptionLabels()).toContain('=');
});
}
function visibleOptionLabels(): string[] {
return Array.from(document.querySelectorAll(CM_OPTION_SELECTOR)).map(
(el) => el.textContent ?? '',
);
}
describe('QuerySearch context-prefixed key suggestions', () => {
it('shows one deduped suggestion per key name in normal mode', async () => {
await renderAndType('statu');
await waitFor(() => {
expect(visibleOptionLabels()).toContain('status.code');
});
const statusOptions = visibleOptionLabels().filter(
(label) => label === 'status.code',
);
expect(statusOptions).toHaveLength(1);
});
it('shows context-scoped suggestions for a complete context prefix', async () => {
await renderAndType('attribute.');
await waitFor(() => {
expect(visibleOptionLabels()).toContain('attribute.status.code');
});
const labels = visibleOptionLabels();
// Literal key name match ranks first
expect(labels[0]).toBe('attribute.a.b.c');
// Context-qualified form of the literal key is also present
expect(labels).toContain('attribute.attribute.a.b.c');
// span-only key is not suggested under attribute.
expect(labels).not.toContain('attribute.duration.nano');
});
it('fetches the context page when the prefix is typed exactly', async () => {
await renderAndType('attribute.');
await waitFor(() => {
expect(fetchedSearchTexts).toContain('attribute.');
});
});
});
describe('QuerySearch operator suggestions by key type', () => {
it('shows all operators for a key with ambiguous types', async () => {
// status.code is string + number across variants → no type filtering
await typeKeyThenEnterOperatorContext('status.code', 'status.code');
const labels = visibleOptionLabels();
expect(labels).toContain('>');
expect(labels).toContain('LIKE');
});
it('shows numeric operators for a single-type number key', async () => {
await typeKeyThenEnterOperatorContext('duration.nano', 'duration.nano');
const labels = visibleOptionLabels();
expect(labels).toContain('>');
expect(labels).not.toContain('LIKE');
});
it('narrows operators when a context prefix disambiguates the type', async () => {
// status.code is ambiguous globally, but resource.status.code is string-only
await typeKeyThenEnterOperatorContext(
'resource.status.code',
'resource.status.code',
);
const labels = visibleOptionLabels();
expect(labels).toContain('LIKE');
expect(labels).not.toContain('>');
});
});

View File

@@ -8,78 +8,13 @@ import type { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/ty
import { DataSource } from 'types/common/queryBuilder';
import QuerySearch from '../QuerySearch/QuerySearch';
import { mockCodeMirrorDomApis } from './codemirrorDomMocks';
const CM_EDITOR_SELECTOR = '.cm-editor .cm-content';
// Mock DOM APIs that CodeMirror needs
beforeAll(() => {
// Mock getClientRects and getBoundingClientRect for Range objects
const mockRect: DOMRect = {
width: 100,
height: 20,
top: 0,
left: 0,
right: 100,
bottom: 20,
x: 0,
y: 0,
toJSON: (): DOMRect => mockRect,
} as DOMRect;
// Create a minimal Range mock with only what CodeMirror actually uses
const createMockRange = (): Range => {
let startContainer: Node = document.createTextNode('');
let endContainer: Node = document.createTextNode('');
let startOffset = 0;
let endOffset = 0;
const mockRange = {
// CodeMirror uses these for text measurement
getClientRects: (): DOMRectList =>
({
length: 1,
item: (index: number): DOMRect | null => (index === 0 ? mockRect : null),
0: mockRect,
*[Symbol.iterator](): Generator<DOMRect> {
yield mockRect;
},
}) as unknown as DOMRectList,
getBoundingClientRect: (): DOMRect => mockRect,
// CodeMirror calls these to set up text ranges
setStart: (node: Node, offset: number): void => {
startContainer = node;
startOffset = offset;
},
setEnd: (node: Node, offset: number): void => {
endContainer = node;
endOffset = offset;
},
// Minimal Range properties (TypeScript requires these)
get startContainer(): Node {
return startContainer;
},
get endContainer(): Node {
return endContainer;
},
get startOffset(): number {
return startOffset;
},
get endOffset(): number {
return endOffset;
},
get collapsed(): boolean {
return startContainer === endContainer && startOffset === endOffset;
},
commonAncestorContainer: document.body,
};
return mockRange as unknown as Range;
};
// Mock document.createRange to return a new Range instance each time
document.createRange = (): Range => createMockRange();
// Mock getBoundingClientRect for elements
Element.prototype.getBoundingClientRect = (): DOMRect => mockRect;
mockCodeMirrorDomApis();
});
jest.mock('hooks/useDarkMode', () => ({

View File

@@ -0,0 +1,71 @@
// Mocks the DOM measurement APIs CodeMirror needs to render in jsdom
// (Range client rects + element bounding rects). Call from a beforeAll in
// specs that render the real CodeMirror editor.
export function mockCodeMirrorDomApis(): void {
const mockRect: DOMRect = {
width: 100,
height: 20,
top: 0,
left: 0,
right: 100,
bottom: 20,
x: 0,
y: 0,
toJSON: (): DOMRect => mockRect,
} as DOMRect;
// Create a minimal Range mock with only what CodeMirror actually uses
const createMockRange = (): Range => {
let startContainer: Node = document.createTextNode('');
let endContainer: Node = document.createTextNode('');
let startOffset = 0;
let endOffset = 0;
const mockRange = {
// CodeMirror uses these for text measurement
getClientRects: (): DOMRectList =>
({
length: 1,
item: (index: number): DOMRect | null => (index === 0 ? mockRect : null),
0: mockRect,
*[Symbol.iterator](): Generator<DOMRect> {
yield mockRect;
},
}) as unknown as DOMRectList,
getBoundingClientRect: (): DOMRect => mockRect,
// CodeMirror calls these to set up text ranges
setStart: (node: Node, offset: number): void => {
startContainer = node;
startOffset = offset;
},
setEnd: (node: Node, offset: number): void => {
endContainer = node;
endOffset = offset;
},
// Minimal Range properties (TypeScript requires these)
get startContainer(): Node {
return startContainer;
},
get endContainer(): Node {
return endContainer;
},
get startOffset(): number {
return startOffset;
},
get endOffset(): number {
return endOffset;
},
get collapsed(): boolean {
return startContainer === endContainer && startOffset === endOffset;
},
commonAncestorContainer: document.body,
};
return mockRange as unknown as Range;
};
// Mock document.createRange to return a new Range instance each time
document.createRange = (): Range => createMockRange();
// Mock getBoundingClientRect for elements
Element.prototype.getBoundingClientRect = (): DOMRect => mockRect;
}

View File

@@ -9,6 +9,7 @@ import { extractQueryPairs } from 'utils/queryContextUtils';
import {
convertAggregationToExpression,
convertExpressionToFilters,
convertFiltersToExpression,
convertFiltersToExpressionWithExistingQuery,
formatValueForExpression,
@@ -1598,4 +1599,36 @@ describe('formatValueForExpression', () => {
expect(formatValueForExpression([123] as any)).toBe('[123]');
});
});
// Regression: an escaped single quote must not gain a backslash on each
// expression <-> filters round-trip (broke Create Alert from a panel).
describe('escaped quote round-trip', () => {
const EXPR = "name = 'it\\'s a ribbon'"; // name = 'it\'s a ribbon' (single backslash)
it('stores the unescaped value in the filter item', () => {
const items = convertExpressionToFilters(EXPR);
expect(items).toHaveLength(1);
expect(items[0].value).toBe("it's a ribbon");
});
it('is lossless: expression -> filters -> expression', () => {
const items = convertExpressionToFilters(EXPR);
expect(convertFiltersToExpression({ items, op: 'AND' }).expression).toBe(
EXPR,
);
});
it('is idempotent across repeated existing-query conversions', () => {
const pass1 = convertFiltersToExpressionWithExistingQuery(
{ items: [], op: 'AND' },
EXPR,
);
expect(pass1.filter.expression).toBe(EXPR);
const pass2 = convertFiltersToExpressionWithExistingQuery(
pass1.filters,
pass1.filter.expression,
);
expect(pass2.filter.expression).toBe(EXPR);
});
});
});

View File

@@ -176,7 +176,9 @@ function formatSingleValueForFilter(
}
if (isQuoted(value)) {
return unquote(value);
// Unescape `\'` → `'` (inverse of formatSingleValue) so the round-trip doesn't
// double the backslash each pass.
return unquote(value).replace(/\\'/g, "'");
}
}
@@ -768,6 +770,34 @@ export const removeVariableFromExpression = (
return removeKeysFromExpression(expression, keysToRemove, `$${variableName}`);
};
// Appends `clause` as a top-level AND term, parenthesising the base only when it
// has a top-level OR (AND binds tighter, so `a OR b AND c` would misbind).
export const appendAndClause = (
expression: string | undefined,
clause: string,
): string => {
const base = expression?.trim();
if (!base) {
return clause;
}
const chars = CharStreams.fromString(base);
const lexer = new FilterQueryLexer(chars);
lexer.removeErrorListeners();
const tokenStream = new CommonTokenStream(lexer);
const parser = new FilterQueryParser(tokenStream);
parser.removeErrorListeners();
const tree = parser.query();
if (parser.syntaxErrorsCount > 0) {
return `(${base}) AND ${clause}`;
}
const hasTopLevelOr =
tree.expression().orExpression().andExpression_list().length > 1;
return hasTopLevelOr ? `(${base}) AND ${clause}` : `${base} AND ${clause}`;
};
/**
* Convert old having format to new having format
* @param having - Array of old having objects with columnName, op, and value

View File

@@ -11,6 +11,8 @@ import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
import { isKeyMatch } from './utils';
import { CheckedState } from '../../types';
import { SectionType } from './v2/itemRules';
export const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
export const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'not in', 'nin'];
@@ -148,6 +150,8 @@ export function applyCheckboxToggle({
value,
checked,
isOnlyOrAllClicked,
previousState,
sectionType,
}: {
currentQuery: Query;
activeQueryIndex: number;
@@ -157,6 +161,8 @@ export function applyCheckboxToggle({
value: string;
checked: boolean;
isOnlyOrAllClicked: boolean;
previousState?: CheckedState;
sectionType?: SectionType;
}): Query {
const activeItems =
currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items;
@@ -216,6 +222,7 @@ export function applyCheckboxToggle({
);
if (currentFilter) {
const runningOperator = currentFilter?.op;
switch (runningOperator) {
case 'in':
if (checked) {
@@ -246,9 +253,29 @@ export function applyCheckboxToggle({
});
}
} else if (!checked) {
// if we are removing some value when the running operator is IN we filter.
// example - key IN [value1,currentSelectedValue] becomes key IN [value1] in case of array
if (isArray(currentFilter.value)) {
// Related section: clicking to exclude creates NOT_IN for just this value
if (sectionType === SectionType.RELATED) {
const newFilter: TagFilterItem = {
id: uuid(),
op: getNotInOperator(source),
key: filter.attributeKey,
value,
};
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
});
if (query.filter?.expression) {
query.filter.expression = removeKeysFromExpression(
query.filter.expression,
[filter.attributeKey.key],
);
}
} else if (isArray(currentFilter.value)) {
// if we are removing some value when the running operator is IN we filter.
// example - key IN [value1,currentSelectedValue] becomes key IN [value1] in case of array
const newFilter = {
...currentFilter,
value: currentFilter.value.filter((val) => val !== value),
@@ -275,11 +302,38 @@ export function applyCheckboxToggle({
}
break;
case 'nin':
case 'not in':
// if the current running operator is NIN then when unchecking the value it gets
// added to the clause like key NIN [value1 , currentUnselectedValue]
if (!checked) {
// in case of array add the currentUnselectedValue to the list.
case 'not in': {
// NOT IN means "exclude these values"
// Check if value is currently in the exclusion list
const isValueInFilter = isArray(currentFilter.value)
? currentFilter.value.includes(value)
: currentFilter.value === value;
// When clicking unchecked "Other" item, user wants to SELECT it
// Replace NOT IN filter with IN [value]
if (previousState === 'unchecked' && checked) {
const newFilter: TagFilterItem = {
id: uuid(),
op: getOperatorValue(OPERATORS.IN),
key: filter.attributeKey,
value,
};
query.filters.items = query.filters.items.map((item) => {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
});
if (query.filter?.expression) {
query.filter.expression = removeKeysFromExpression(
query.filter.expression,
[filter.attributeKey.key],
);
}
} else if (!checked || !isValueInFilter) {
// Add to NOT IN when:
// - checked=false (user explicitly unchecked to exclude)
// - checked=true but value not in filter (clicking "other" value to exclude)
if (isArray(currentFilter.value)) {
const newFilter = {
...currentFilter,
@@ -292,7 +346,6 @@ export function applyCheckboxToggle({
return item;
});
} else {
// in case of not an array make it one!
const newFilter = {
...currentFilter,
value: [currentFilter.value as string, value],
@@ -304,8 +357,9 @@ export function applyCheckboxToggle({
return item;
});
}
} else if (checked) {
// opposite of above!
} else {
// Remove from NOT IN when value IS in filter and checked=true
// (user wants to include this value back)
if (isArray(currentFilter.value)) {
const newFilter = {
...currentFilter,
@@ -346,6 +400,7 @@ export function applyCheckboxToggle({
}
}
break;
}
case '=':
if (checked) {
const newFilter = {
@@ -389,10 +444,11 @@ export function applyCheckboxToggle({
}
}
} else {
// case - when there is no filter for the current key that means all are selected right now.
// No filter for this key - all are visually selected.
// checked=true → user wants to select (IN), checked=false → exclude (NOT IN)
const newFilterItem: TagFilterItem = {
id: uuid(),
op: getNotInOperator(source),
op: checked ? getOperatorValue(OPERATORS.IN) : getNotInOperator(source),
key: filter.attributeKey,
value,
};

View File

@@ -10,6 +10,8 @@ import {
applyCheckboxToggle,
clearFilterFromQuery,
} from './checkboxFilterQuery';
import { CheckedState } from '../../types';
import { SectionType } from './v2/itemRules';
interface UseCheckboxFilterActionsProps {
filter: IQuickFiltersConfig;
@@ -24,6 +26,8 @@ interface UseCheckboxFilterActionsReturn {
value: string,
checked: boolean,
isOnlyOrAllClicked: boolean,
previousState?: CheckedState,
sectionType?: SectionType,
) => void;
onClear: () => void;
}
@@ -53,6 +57,8 @@ function useCheckboxFilterActions({
value: string,
checked: boolean,
isOnlyOrAllClicked: boolean,
previousState?: CheckedState,
sectionType?: SectionType,
): void => {
dispatch(
applyCheckboxToggle({
@@ -64,6 +70,8 @@ function useCheckboxFilterActions({
value,
checked,
isOnlyOrAllClicked,
previousState,
sectionType,
}),
);
};

View File

@@ -0,0 +1,103 @@
.checkboxFilter {
display: flex;
flex-direction: column;
padding: var(--spacing-6);
gap: var(--spacing-6);
border-bottom: 1px solid var(--l1-border);
}
.search {
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);
--input-focus-background: var(--l2-background);
--input-border-color: var(--l2-border);
--input-focus-border-color: var(--l2-border);
}
.searchSpinner {
color: var(--l2-foreground);
animation: spin 0.8s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.values {
display: flex;
flex-direction: column;
}
.sectionValues {
display: flex;
flex-direction: column;
gap: var(--spacing-4);
}
.loadingValues {
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-6) 0;
}
.loadingMore {
align-self: center;
}
.noData {
align-self: center;
}
.showMore {
display: flex;
align-items: center;
justify-content: center;
}
.showMoreText {
color: var(--accent-primary);
cursor: pointer;
}
.goToDocs {
display: flex;
flex-direction: column;
gap: 44px;
}
.goToDocsContainer {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
margin-top: var(--spacing-2);
}
.goToDocsMessage {
color: var(--l2-foreground);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
}
.goToDocsButton {
display: flex;
align-items: center;
gap: var(--spacing-2);
cursor: pointer;
margin: 0 0 var(--spacing-2);
padding: 0;
}
.goToDocsButtonText {
color: var(--bg-robin-400);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
}

View File

@@ -0,0 +1,126 @@
import { render, RenderResult } from 'tests/test-utils';
import { server, rest } from 'mocks-server/server';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import {
FiltersType,
IQuickFiltersConfig,
QuickFilterCheckboxUseFieldApis,
QuickFiltersSource,
} from '../../../types';
import CheckboxFilterV2 from './CheckboxFilterV2';
export const DEFAULT_FILTER: IQuickFiltersConfig = {
type: FiltersType.CHECKBOX,
title: 'Environment',
attributeKey: {
key: 'deployment.environment',
dataType: DataTypes.String,
type: 'tag',
},
dataSource: DataSource.TRACES,
defaultOpen: true,
};
export const DEFAULT_USE_FIELD_APIS: QuickFilterCheckboxUseFieldApis = {
startUnixMilli: 1700000000000,
endUnixMilli: 1700003600000,
existingQuery: null,
};
export function mockFieldsValuesAPI(response: {
relatedValues?: (string | null)[];
stringValues?: (string | null)[];
numberValues?: (number | null)[];
}): void {
server.use(
rest.get('http://localhost/api/v1/fields/values', (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: response.relatedValues ?? [],
stringValues: response.stringValues ?? [],
numberValues: response.numberValues ?? [],
},
},
}),
),
),
);
}
export function mockFieldsValuesAPILoading(): void {
server.use(
rest.get('http://localhost/api/v1/fields/values', (_, res, ctx) =>
res(ctx.delay(10000)),
),
);
}
export function setupServer(): void {
beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
}
export interface FilterItemConfig {
op: string;
value: string | string[];
}
export function renderWithFilter(
onFilterChange: jest.Mock,
filterItem?: FilterItemConfig,
): RenderResult {
const items: TagFilterItem[] = filterItem
? [
{
key: { key: 'deployment.environment' },
op: filterItem.op,
value: filterItem.value,
} as TagFilterItem,
]
: [];
return render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
onFilterChange={onFilterChange}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items, op: 'AND' },
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
}
export function getFilterFromCall(
onFilterChange: jest.Mock,
callIndex = 0,
): TagFilterItem | undefined {
const query = onFilterChange.mock.calls[callIndex]?.[0] as Query | undefined;
return query?.builder.queryData[0]?.filters?.items?.find(
(item) => item.key?.key === 'deployment.environment',
);
}

View File

@@ -0,0 +1,241 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { Skeleton } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { LoaderCircle } from '@signozhq/icons';
import {
IQuickFiltersConfig,
QuickFilterCheckboxUseFieldApis,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { NON_SELECTED_OPERATORS } from '../checkboxFilterQuery';
import useActiveQueryIndex from '../useActiveQueryIndex';
import useCheckboxDisclosure from '../useCheckboxDisclosure';
import useCheckboxFilterActions from '../useCheckboxFilterActions';
import useCheckboxFilterState from '../useCheckboxFilterState';
import { useFieldValues } from './useFieldValues';
import { useExistingQuery } from './useExistingQuery';
import { isKeyMatch } from '../utils';
import { CheckboxFilterV2Header } from './CheckboxFilterV2Header';
import { CheckboxFilterV2Section } from './CheckboxFilterV2Section';
import { buildSelectedSet, useSectionedValues } from './useSectionedValues';
import { useStaleRelatedExclusions } from './useStaleRelatedExclusions';
import styles from './CheckboxFilterV2.module.scss';
interface CheckboxFilterV2Props {
filter: IQuickFiltersConfig;
source: QuickFiltersSource;
onFilterChange?: (query: Query) => void;
useFieldApis: QuickFilterCheckboxUseFieldApis;
}
export default function CheckboxFilterV2(
props: CheckboxFilterV2Props,
): JSX.Element {
const { source, filter, onFilterChange, useFieldApis } = props;
const [searchText, setSearchText] = useState<string>('');
const [userToggleState, setUserToggleState] = useState<boolean | null>(null);
const { currentQuery } = useQueryBuilder();
const activeQueryIndex = useActiveQueryIndex(source);
const {
isOpen,
isSomeFilterPresentForCurrentAttribute,
visibleItemsCount,
onToggleOpen,
onShowMore,
} = useCheckboxDisclosure({ filter, activeQueryIndex });
// Auto-preserve open state when filter is present
useEffect(() => {
if (isSomeFilterPresentForCurrentAttribute && userToggleState === null) {
setUserToggleState(true);
}
}, [isSomeFilterPresentForCurrentAttribute, userToggleState]);
const { existingQuery, hasExistingQuery } = useExistingQuery({
useFieldApis,
activeQueryIndex,
});
const { relatedValues, allValues, isLoading, isFetching } = useFieldValues({
filter,
searchText,
existingQuery,
metricNamespace: useFieldApis.metricNamespace,
startUnixMilli: useFieldApis.startUnixMilli,
endUnixMilli: useFieldApis.endUnixMilli,
enabled: isOpen,
});
// Track if initial load completed (don't show skeleton after first load)
// Must track if loading ever started, otherwise hasLoadedOnce gets set
// immediately on first render when query is disabled (isLoading=false)
const hasLoadedOnce = useRef(false);
const wasLoading = useRef(false);
if (isLoading) {
wasLoading.current = true;
}
if (!isLoading && wasLoading.current && !hasLoadedOnce.current) {
hasLoadedOnce.current = true;
}
// Combine for state derivation
const attributeValues = useMemo(() => {
const combined = [...relatedValues, ...allValues];
return [...new Set(combined)];
}, [relatedValues, allValues]);
const { currentFilterState, isFilterDisabled, isMultipleValuesTrueForTheKey } =
useCheckboxFilterState({ filter, attributeValues, activeQueryIndex });
const { onChange, onClear } = useCheckboxFilterActions({
filter,
source,
attributeValues,
activeQueryIndex,
onFilterChange,
});
const setSearchTextDebounced = useDebouncedFn((...args) => {
setSearchText(args[0] as string);
}, DEBOUNCE_DELAY);
const currentFilterOp = useMemo(() => {
const filterSync = currentQuery?.builder.queryData?.[
activeQueryIndex
]?.filters?.items.find((item) =>
isKeyMatch(item.key?.key, filter.attributeKey.key),
);
return filterSync?.op;
}, [
currentQuery?.builder.queryData,
activeQueryIndex,
filter.attributeKey.key,
]);
const isNotInOperator = NON_SELECTED_OPERATORS.includes(currentFilterOp || '');
const selectedValues = useMemo(
() => [
...buildSelectedSet(
currentFilterState,
isSomeFilterPresentForCurrentAttribute,
isNotInOperator,
),
],
[currentFilterState, isSomeFilterPresentForCurrentAttribute, isNotInOperator],
);
const isRefreshing = isFetching && !isLoading && searchText === '';
const relatedExclusions = useStaleRelatedExclusions({
selectedValues,
isFetching,
isRefreshing,
});
const { sections, totalCount } = useSectionedValues({
relatedValues,
allValues,
currentFilterState,
isSomeFilterPresentForCurrentAttribute,
isNotInOperator,
hasExistingQuery,
visibleItemsCount,
relatedExclusions,
});
return (
<div className={styles.checkboxFilter} data-testid="checkbox-filter-v2">
<CheckboxFilterV2Header
title={filter.title}
isOpen={isOpen}
showClearAll={!!attributeValues.length}
onToggleOpen={onToggleOpen}
onClear={onClear}
isSomeFilterPresentForCurrentAttribute={
isSomeFilterPresentForCurrentAttribute
}
/>
{isOpen && isLoading && !hasLoadedOnce.current && (
<section>
<Skeleton paragraph={{ rows: 4 }} />
</section>
)}
{isOpen && (!isLoading || hasLoadedOnce.current) && (
<>
<section className={styles.search}>
<Input
placeholder="Filter values"
onChange={(e): void => setSearchTextDebounced(e.target.value)}
disabled={isFilterDisabled}
data-testid="checkbox-filter-search"
suffix={
isFetching ? (
<LoaderCircle
size={14}
className={styles.searchSpinner}
data-testid="checkbox-filter-search-loading"
/>
) : null
}
/>
</section>
{totalCount > 0 && (
<section className={styles.values}>
{sections.map((section, index) => (
<CheckboxFilterV2Section
key={section.type}
section={section}
index={index}
isFilterDisabled={isFilterDisabled}
filter={filter}
isSomeFilterPresentForCurrentAttribute={
isSomeFilterPresentForCurrentAttribute
}
isMultipleValuesTrueForTheKey={isMultipleValuesTrueForTheKey}
onChange={onChange}
/>
))}
</section>
)}
{totalCount === 0 && hasLoadedOnce.current && !isFetching && (
<section
className={styles.noData}
data-testid={
searchText
? 'checkbox-filter-no-search-results'
: 'checkbox-filter-empty'
}
>
<Typography.Text>No values found</Typography.Text>
</section>
)}
{visibleItemsCount < totalCount &&
!(searchText && (isLoading || isFetching)) && (
<section className={styles.showMore}>
<Typography.Text
className={styles.showMoreText}
onClick={onShowMore}
data-testid="checkbox-filter-show-more"
>
Show More...
</Typography.Text>
</section>
)}
</>
)}
</div>
);
}

View File

@@ -0,0 +1,33 @@
.header {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.leftAction {
display: flex;
align-items: center;
gap: var(--spacing-3);
}
.title {
color: var(--l2-foreground);
font-size: 14px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
text-transform: capitalize;
}
.rightAction {
display: flex;
align-items: center;
min-width: 48px;
}
.clearAll {
font-size: 12px;
color: var(--accent-primary);
cursor: pointer;
}

View File

@@ -0,0 +1,62 @@
import { Typography } from '@signozhq/ui/typography';
import { ChevronDown, ChevronRight } from '@signozhq/icons';
import styles from './CheckboxFilterV2Header.module.scss';
interface CheckboxFilterHeaderProps {
title: string;
isOpen: boolean;
showClearAll: boolean;
onToggleOpen: () => void;
onClear: () => void;
isSomeFilterPresentForCurrentAttribute: boolean;
}
export function CheckboxFilterV2Header({
title,
isOpen,
showClearAll,
onToggleOpen,
onClear,
isSomeFilterPresentForCurrentAttribute,
}: CheckboxFilterHeaderProps): JSX.Element {
return (
<section
role="button"
tabIndex={0}
className={styles.header}
onClick={onToggleOpen}
onKeyDown={(e): void => {
if (e.key === 'Enter' || e.key === ' ') {
onToggleOpen();
}
}}
data-testid="checkbox-filter-header"
data-state={isOpen ? 'open' : 'closed'}
>
<section className={styles.leftAction}>
{isOpen ? (
<ChevronDown size={13} cursor="pointer" />
) : (
<ChevronRight size={13} cursor="pointer" />
)}
<Typography.Text className={styles.title}>{title}</Typography.Text>
</section>
<section className={styles.rightAction}>
{isOpen && showClearAll && isSomeFilterPresentForCurrentAttribute && (
<Typography.Text
className={styles.clearAll}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
onClear();
}}
data-testid="checkbox-filter-clear-all"
>
Clear
</Typography.Text>
)}
</section>
</section>
);
}

View File

@@ -0,0 +1,106 @@
import {
IQuickFiltersConfig,
CheckedState,
} from 'components/QuickFilters/types';
import { CheckboxFilterV2ValueRow } from './CheckboxFilterV2ValueRow';
import { SectionDivider } from './SectionDivider';
import { Section } from './useSectionedValues';
import { SectionType } from './itemRules';
import styles from './CheckboxFilterV2.module.scss';
interface SectionConfig {
label: string;
tooltip?: string;
}
function getSectionConfig(type: SectionType): SectionConfig | null {
switch (type) {
case SectionType.SELECTED:
return { label: 'Selected' };
case SectionType.RELATED:
return {
label: 'Related',
tooltip: 'Values that are filtered by your current selection.',
};
case SectionType.ALL_VALUES:
return { label: 'All values' };
default:
return null;
}
}
interface CheckboxFilterV2SectionProps {
section: Section;
index: number;
isFilterDisabled: boolean;
filter: IQuickFiltersConfig;
isSomeFilterPresentForCurrentAttribute: boolean;
isMultipleValuesTrueForTheKey: boolean;
onChange: (
value: string,
checked: boolean,
isOnly: boolean,
previousState?: CheckedState,
sectionType?: SectionType,
) => void;
}
export function CheckboxFilterV2Section(
props: CheckboxFilterV2SectionProps,
): JSX.Element | null {
const {
section,
index,
isFilterDisabled,
filter,
isSomeFilterPresentForCurrentAttribute,
isMultipleValuesTrueForTheKey,
onChange,
} = props;
if (section.items.length === 0) {
return null;
}
const config = getSectionConfig(section.type);
// Show divider for all sections except first SELECTED section
const showDivider =
config !== null && (index > 0 || section.type !== SectionType.SELECTED);
return (
<div data-testid={`section-${section.type}`} className={styles.sectionValues}>
{showDivider && config && (
<SectionDivider label={config.label} tooltip={config.tooltip} />
)}
{section.items.map(({ value, badge, checkedState }) => {
const isChecked = checkedState === 'checked';
return (
<CheckboxFilterV2ValueRow
key={value}
value={value}
checkedState={checkedState}
disabled={isFilterDisabled}
title={filter.title}
badge={badge}
onlyButtonLabel={
isSomeFilterPresentForCurrentAttribute
? isChecked && !isMultipleValuesTrueForTheKey
? 'All'
: 'Only'
: 'Only'
}
customRendererForValue={filter.customRendererForValue}
onCheckboxChange={(checked, previousState): void =>
onChange(value, checked, false, previousState, section.type)
}
onOnlyOrAllClick={(): void => onChange(value, isChecked, true)}
/>
);
})}
</div>
);
}

View File

@@ -0,0 +1,164 @@
.valueRow {
display: flex;
align-items: center;
gap: var(--spacing-4);
min-height: 24px;
}
.checkbox {
display: inline-flex;
align-items: center;
}
.valueButton {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: var(--spacing-2);
width: calc(100% - 24px);
cursor: pointer;
}
.content {
display: flex;
align-items: center;
gap: var(--spacing-2);
min-width: 0;
}
.valueLabel {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.actions {
display: grid;
align-items: center;
justify-items: end;
// Stack badge / only / toggle in a single cell so the crossfade overlaps
// instead of laying them side-by-side mid-transition.
> * {
grid-area: 1 / 1;
}
}
.badge {
opacity: 1;
transition:
opacity 0.16s ease,
display 0.16s allow-discrete;
}
.onlyButton {
display: none;
align-items: center;
justify-content: center;
opacity: 0;
transform: translateX(4px);
transition:
opacity 0.16s ease,
transform 0.16s ease,
display 0.16s allow-discrete;
--button-height: 21px;
--button-padding: var(--spacing-5);
&:hover {
background-color: unset;
}
}
.toggleButton {
display: none;
align-items: center;
justify-content: center;
opacity: 0;
transform: translateX(4px);
transition:
opacity 0.16s ease,
transform 0.16s ease,
display 0.16s allow-discrete;
--button-height: 21px;
--button-padding: var(--spacing-5);
&:hover {
background-color: unset;
}
}
.isDisabled {
cursor: not-allowed;
.valueLabel {
color: var(--l3-foreground);
}
.onlyButton {
cursor: not-allowed;
color: var(--l3-foreground);
}
.toggleButton {
cursor: not-allowed;
color: var(--l3-foreground);
}
}
.valueButton:hover {
.onlyButton {
display: flex;
opacity: 1;
transform: translateX(0);
@starting-style {
opacity: 0;
transform: translateX(4px);
}
}
.badge {
display: none;
opacity: 0;
}
}
.checkbox:hover ~ .valueButton {
.toggleButton {
display: flex;
opacity: 1;
transform: translateX(0);
@starting-style {
opacity: 0;
transform: translateX(4px);
}
}
.badge {
display: none;
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.badge,
.onlyButton,
.toggleButton {
transition: none;
}
}
.indicatorFalse {
width: 2px;
height: 11px;
border-radius: 2px;
background: var(--danger-background);
}
.indicatorTrue {
width: 2px;
height: 11px;
border-radius: 2px;
background: var(--bg-forest-500);
}

View File

@@ -0,0 +1,118 @@
import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Checkbox } from '@signozhq/ui/checkbox';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { BadgeConfig } from './itemRules';
import { CheckedState } from '../../../types';
import styles from './CheckboxFilterV2ValueRow.module.scss';
interface ValueRowProps {
value: string;
checkedState: CheckedState;
disabled: boolean;
title: string;
onlyButtonLabel: string;
customRendererForValue?: (value: string) => JSX.Element;
onCheckboxChange: (checked: boolean, previousState: CheckedState) => void;
onOnlyOrAllClick: () => void;
badge: BadgeConfig | null;
}
function toCheckboxValue(state: CheckedState): boolean | 'indeterminate' {
if (state === 'indeterminate') {
return 'indeterminate';
}
return state === 'checked';
}
const INDICATOR_CLASS_MAP = {
false: styles.indicatorFalse,
true: styles.indicatorTrue,
} as Record<string, string>;
export function CheckboxFilterV2ValueRow({
value,
checkedState,
disabled,
title,
onlyButtonLabel,
customRendererForValue,
onCheckboxChange,
onOnlyOrAllClick,
badge,
}: ValueRowProps): JSX.Element {
const indicatorClass = INDICATOR_CLASS_MAP[value];
return (
<div
className={styles.valueRow}
data-testid={`checkbox-value-row-${value}`}
data-state={checkedState}
data-disabled={disabled}
>
<div className={styles.checkbox}>
<Checkbox
onChange={(isChecked): void =>
onCheckboxChange(isChecked === true, checkedState)
}
value={toCheckboxValue(checkedState)}
disabled={disabled}
color="primary"
testId={`checkbox-${title}-${value}`}
/>
</div>
<div
role="button"
tabIndex={disabled ? -1 : 0}
className={cx(styles.valueButton, disabled && styles.isDisabled)}
onClick={(): void => {
if (disabled) {
return;
}
onOnlyOrAllClick();
}}
onKeyDown={(e): void => {
if (disabled) {
return;
}
if (e.key === 'Enter' || e.key === ' ') {
onOnlyOrAllClick();
}
}}
>
<div className={styles.content}>
{indicatorClass && <div className={indicatorClass} />}
{customRendererForValue ? (
customRendererForValue(value)
) : (
<Typography.Text title={value} className={styles.valueLabel}>
{value}
</Typography.Text>
)}
</div>
<div className={styles.actions}>
{badge && (
<Badge
variant="outline"
color={badge.color}
className={styles.badge}
testId={`badge-${badge.key}`}
>
{badge.label}
</Badge>
)}
<Button variant="ghost" color="secondary" className={styles.onlyButton}>
{onlyButtonLabel}
</Button>
<Button variant="ghost" color="secondary" className={styles.toggleButton}>
Toggle
</Button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,29 @@
.divider {
display: flex;
align-items: center;
gap: var(--spacing-4);
padding: var(--spacing-2) 0;
margin-bottom: calc(-1 * var(--spacing-4));
}
.line {
flex: 1;
height: 1px;
background: var(--l1-border);
}
.label {
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;
}
.infoIcon {
color: var(--l3-foreground);
cursor: help;
flex-shrink: 0;
}

View File

@@ -0,0 +1,33 @@
import { Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { Info } from '@signozhq/icons';
import styles from './SectionDivider.module.scss';
interface SectionDividerProps {
label: string;
tooltip?: string;
}
export function SectionDivider({
label,
tooltip,
}: SectionDividerProps): JSX.Element {
const testId = label.toLowerCase().replace(/\s+/g, '-');
return (
<div className={styles.divider} data-testid={`section-divider-${testId}`}>
<div className={styles.line} />
<Typography.Text className={styles.label}>{label}</Typography.Text>
{tooltip && (
<Tooltip title={tooltip}>
<Info
size={12}
className={styles.infoIcon}
data-testid="section-divider-info"
/>
</Tooltip>
)}
<div className={styles.line} />
</div>
);
}

View File

@@ -0,0 +1,302 @@
import { screen } from '@testing-library/react';
import { server, rest } from 'mocks-server/server';
import { render } from 'tests/test-utils';
import { QuickFiltersSource } from '../../../../types';
import CheckboxFilterV2 from '../CheckboxFilterV2';
import {
DEFAULT_FILTER,
DEFAULT_USE_FIELD_APIS,
setupServer,
} from '../CheckboxFilterV2.testUtils';
const USE_FIELD_APIS_AUTO_DERIVE = {
...DEFAULT_USE_FIELD_APIS,
existingQuery: undefined,
};
setupServer();
describe('CheckboxFilterV2 - existingQuery calculation', () => {
const captureExistingQuery = (): Promise<string | null> =>
new Promise((resolve) => {
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
const existingQuery = req.url.searchParams.get('existingQuery');
resolve(existingQuery);
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: [],
stringValues: ['test'],
numberValues: [],
},
},
}),
);
}),
);
});
describe('useFieldApis.existingQuery takes precedence', () => {
it('uses useFieldApis.existingQuery when provided', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'custom.query = "value"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'should.be.ignored = "yes"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBe('custom.query = "value"');
});
it('returns undefined when useFieldApis.existingQuery is null', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: null,
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'should.be.ignored = "yes"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBeNull();
});
});
describe('V5 filter.expression preferred over V3 filters.items', () => {
it('uses V5 filter.expression when both exist', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'service.name', dataType: 'string', type: 'tag' },
op: '=',
value: 'from-v3-items',
},
],
op: 'AND',
},
filter: { expression: 'v5.expression = "preferred"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBe('v5.expression = "preferred"');
});
it('uses V5 filter.expression when no V3 items exist', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'only.v5 = "expression"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBe('only.v5 = "expression"');
});
});
describe('V3 filters.items fallback', () => {
it('converts V3 filters.items to expression when no V5 expression exists', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'service.name', dataType: 'string', type: 'tag' },
op: '=',
value: 'api-service',
},
],
op: 'AND',
},
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBe("service.name = 'api-service'");
});
it('converts multiple V3 filters.items with AND operator', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'service.name', dataType: 'string', type: 'tag' },
op: '=',
value: 'api',
},
{
key: { key: 'env', dataType: 'string', type: 'tag' },
op: '=',
value: 'prod',
},
],
op: 'AND',
},
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBe("service.name = 'api' AND env = 'prod'");
});
it('returns undefined when no filters exist', async () => {
const queryPromise = captureExistingQuery();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-test');
const capturedQuery = await queryPromise;
expect(capturedQuery).toBeNull();
});
});
});

View File

@@ -0,0 +1,768 @@
import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { server, rest } from 'mocks-server/server';
import { render } from 'tests/test-utils';
import { QuickFiltersSource } from '../../../../types';
import CheckboxFilterV2 from '../CheckboxFilterV2';
import {
DEFAULT_FILTER,
DEFAULT_USE_FIELD_APIS,
getFilterFromCall,
mockFieldsValuesAPI,
renderWithFilter,
setupServer,
} from '../CheckboxFilterV2.testUtils';
setupServer();
describe('CheckboxFilterV2 - interactions', () => {
describe('search functionality', () => {
it('filters values based on search text', async () => {
const user = userEvent.setup();
let searchTextReceived = '';
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
searchTextReceived = req.url.searchParams.get('searchText') || '';
const values =
searchTextReceived === ''
? ['production', 'staging', 'development']
: ['production'];
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: [],
stringValues: values,
numberValues: [],
},
},
}),
);
}),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-production');
expect(screen.getByTestId('checkbox-value-row-staging')).toBeInTheDocument();
const searchInput = screen.getByTestId('checkbox-filter-search');
await user.type(searchInput, 'prod');
await waitFor(() => {
expect(searchTextReceived).toBe('prod');
});
await waitFor(() => {
expect(
screen.queryByTestId('checkbox-value-row-staging'),
).not.toBeInTheDocument();
});
});
it('filters values via search while preserving existingQuery context', async () => {
const user = userEvent.setup();
let requestCount = 0;
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
requestCount += 1;
const searchText = req.url.searchParams.get('searchText') || '';
if (requestCount === 1) {
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: ['production'],
stringValues: ['staging', 'development'],
numberValues: [],
},
},
}),
);
}
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: searchText === 'prod' ? ['production'] : [],
stringValues: searchText === 'prod' ? ['production'] : ['staging'],
numberValues: [],
},
},
}),
);
}),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-production');
// Related values now appear in "Related" section (no badge, uses divider instead)
expect(screen.getByTestId('section-divider-related')).toBeInTheDocument();
const searchInput = screen.getByTestId('checkbox-filter-search');
await user.type(searchInput, 'prod');
await waitFor(() => {
expect(
screen.queryByTestId('checkbox-value-row-staging'),
).not.toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-production'),
).toBeInTheDocument();
});
});
it('shows filtered results in all_values section when searching with existingQuery', async () => {
const user = userEvent.setup();
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
const searchText = req.url.searchParams.get('searchText') || '';
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: [],
stringValues: searchText === '' ? ['prod', 'staging'] : ['prod-match'],
numberValues: [],
},
},
}),
);
}),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
);
await screen.findByTestId('checkbox-value-row-prod');
const searchInput = screen.getByTestId('checkbox-filter-search');
await user.type(searchInput, 'prod');
await waitFor(() => {
expect(screen.getByTestId('section-all_values')).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-prod-match'),
).toBeInTheDocument();
});
});
it('shows empty search results message when no matches found', async () => {
const user = userEvent.setup();
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
const searchText = req.url.searchParams.get('searchText') || '';
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: [],
stringValues: searchText === '' ? ['prod', 'staging'] : [],
numberValues: [],
},
},
}),
);
}),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-prod');
const searchInput = screen.getByTestId('checkbox-filter-search');
await user.type(searchInput, 'xyz-no-match');
await waitFor(() => {
expect(
screen.getByTestId('checkbox-filter-no-search-results'),
).toBeInTheDocument();
});
});
it('shows both RELATED and ALL_VALUES sections with non-overlapping values', async () => {
// This tests the bug fix where different pod names in relatedValues vs stringValues
// caused all items to go to ALL_VALUES instead of showing RELATED section
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
// Non-overlapping: different pod instances
relatedValues: ['pod-a-instance-1', 'pod-b-instance-1'],
stringValues: ['pod-a-instance-2', 'pod-b-instance-2'],
numberValues: [],
},
},
}),
),
),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
);
// Wait for values to load
await screen.findByTestId('checkbox-value-row-pod-a-instance-1');
// RELATED section should exist with relatedValues
expect(screen.getByTestId('section-related')).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-pod-a-instance-1'),
).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-pod-b-instance-1'),
).toBeInTheDocument();
// ALL_VALUES section should exist with stringValues
expect(screen.getByTestId('section-all_values')).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-pod-a-instance-2'),
).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-pod-b-instance-2'),
).toBeInTheDocument();
});
it('shows both sections during search with filtered non-overlapping values', async () => {
const user = userEvent.setup();
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
const searchText = req.url.searchParams.get('searchText') || '';
// Simulate API filtering - both arrays filtered but remain non-overlapping
const relatedValues =
searchText === '' ? ['pod-a-v1', 'pod-b-v1', 'pod-c-v1'] : ['pod-a-v1']; // filtered to match 'pod-a'
const stringValues =
searchText === '' ? ['pod-a-v2', 'pod-b-v2', 'pod-c-v2'] : ['pod-a-v2']; // filtered to match 'pod-a'
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues,
stringValues,
numberValues: [],
},
},
}),
);
}),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
);
await screen.findByTestId('checkbox-value-row-pod-a-v1');
const searchInput = screen.getByTestId('checkbox-filter-search');
await user.type(searchInput, 'pod-a');
// After search, both sections should still appear with filtered results
await waitFor(() => {
// RELATED section with filtered relatedValues
expect(screen.getByTestId('section-related')).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-pod-a-v1'),
).toBeInTheDocument();
// ALL_VALUES section with filtered stringValues
expect(screen.getByTestId('section-all_values')).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-pod-a-v2'),
).toBeInTheDocument();
// Other values should be filtered out
expect(
screen.queryByTestId('checkbox-value-row-pod-b-v1'),
).not.toBeInTheDocument();
});
});
});
describe('header interactions', () => {
it('collapses when header clicked on open filter', async () => {
const user = userEvent.setup();
mockFieldsValuesAPI({
stringValues: ['production'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-production');
const header = screen.getByTestId('checkbox-filter-header');
expect(header).toHaveAttribute('data-state', 'open');
await user.click(header);
expect(header).toHaveAttribute('data-state', 'closed');
expect(
screen.queryByTestId('checkbox-value-row-production'),
).not.toBeInTheDocument();
});
it('expands when header clicked on closed filter', async () => {
const user = userEvent.setup();
mockFieldsValuesAPI({
stringValues: ['production'],
});
render(
<CheckboxFilterV2
filter={{ ...DEFAULT_FILTER, defaultOpen: false }}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
const header = screen.getByTestId('checkbox-filter-header');
expect(header).toHaveAttribute('data-state', 'closed');
await user.click(header);
expect(header).toHaveAttribute('data-state', 'open');
await screen.findByTestId('checkbox-value-row-production');
});
});
describe('show more functionality', () => {
it('shows "Show More..." when more than 10 values', async () => {
const values = Array.from(
{ length: 15 },
(_, i) => `value-${String(i).padStart(2, '0')}`,
);
mockFieldsValuesAPI({ stringValues: values });
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-value-00');
expect(screen.getByTestId('checkbox-filter-show-more')).toBeInTheDocument();
expect(
screen.queryByTestId('checkbox-value-row-value-10'),
).not.toBeInTheDocument();
});
it('loads more values when "Show More..." clicked', async () => {
const user = userEvent.setup();
const values = Array.from(
{ length: 15 },
(_, i) => `value-${String(i).padStart(2, '0')}`,
);
mockFieldsValuesAPI({ stringValues: values });
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-value-00');
await user.click(screen.getByTestId('checkbox-filter-show-more'));
await screen.findByTestId('checkbox-value-row-value-10');
expect(
screen.getByTestId('checkbox-value-row-value-14'),
).toBeInTheDocument();
});
});
describe('clear functionality', () => {
it('shows clear button when filter is open and has filter applied', async () => {
mockFieldsValuesAPI({
stringValues: ['production'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'in',
value: ['production'],
},
],
op: 'AND',
},
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-production');
expect(screen.getByTestId('checkbox-filter-clear-all')).toBeInTheDocument();
});
it('hides clear button when no filter applied for attribute', async () => {
mockFieldsValuesAPI({
stringValues: ['production'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-production');
expect(
screen.queryByTestId('checkbox-filter-clear-all'),
).not.toBeInTheDocument();
});
it('calls onFilterChange when clear clicked', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
stringValues: ['production'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
onFilterChange={onFilterChange}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'in',
value: ['production'],
},
],
op: 'AND',
},
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-production');
await user.click(screen.getByTestId('checkbox-filter-clear-all'));
expect(onFilterChange).toHaveBeenCalled();
});
});
describe('value row interactions', () => {
it('calls onFilterChange when checkbox value clicked', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
stringValues: ['production', 'staging'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
onFilterChange={onFilterChange}
/>,
);
const productionRow = await screen.findByTestId(
'checkbox-value-row-production',
);
await user.click(within(productionRow).getByText('production'));
expect(onFilterChange).toHaveBeenCalled();
});
it('creates NOT IN filter when unchecking related item with no existing filter', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
relatedValues: ['valueA'],
stringValues: ['valueB'],
});
// Start with no filter, related items show as checked
// Clicking unchecks them → creates NOT IN filter to exclude
renderWithFilter(onFilterChange);
const rowA = await screen.findByTestId('checkbox-value-row-valueA');
expect(rowA).toHaveAttribute('data-state', 'checked');
await user.click(within(rowA).getByRole('checkbox'));
expect(onFilterChange).toHaveBeenCalledTimes(1);
const filter = getFilterFromCall(onFilterChange);
expect(filter?.op).toBe('not in');
expect(filter?.value).toBe('valueA');
});
it('converts NOT IN to IN when toggling unchecked (other) item', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
relatedValues: ['valueA'],
stringValues: ['valueB'],
});
// Clicking unchecked "Other" item with NOT IN filter should convert to IN [B]
renderWithFilter(onFilterChange, { op: 'not in', value: ['valueA'] });
const rowB = await screen.findByTestId('checkbox-value-row-valueB');
expect(rowB).toHaveAttribute('data-state', 'unchecked');
await user.click(within(rowB).getByRole('checkbox'));
expect(onFilterChange).toHaveBeenCalledTimes(1);
const filter = getFilterFromCall(onFilterChange);
expect(filter?.op).toBe('in');
expect(filter?.value).toBe('valueB');
});
it('accumulates both values in IN when toggling checked (related) then unchecked (other)', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
relatedValues: ['valueA'],
stringValues: ['valueB'],
});
// Start with IN filter for valueA
renderWithFilter(onFilterChange, { op: 'in', value: ['valueA'] });
const rowA = await screen.findByTestId('checkbox-value-row-valueA');
expect(rowA).toHaveAttribute('data-state', 'checked');
const rowB = screen.getByTestId('checkbox-value-row-valueB');
expect(rowB).toHaveAttribute('data-state', 'unchecked');
// Toggle B (unchecked -> should add to IN)
await user.click(within(rowB).getByRole('checkbox'));
expect(onFilterChange).toHaveBeenCalledTimes(1);
const filter = getFilterFromCall(onFilterChange);
expect(filter?.op).toBe('in');
expect(filter?.value).toStrictEqual(['valueA', 'valueB']);
});
it('adds to NOT IN when toggling checked (related) with existing NOT IN filter', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
relatedValues: ['valueA'],
stringValues: ['valueB'],
});
// Start with NOT IN filter for valueB
renderWithFilter(onFilterChange, { op: 'not in', value: ['valueB'] });
const rowA = await screen.findByTestId('checkbox-value-row-valueA');
// Related values show as checked
expect(rowA).toHaveAttribute('data-state', 'checked');
// Toggle A (checked -> should add to NOT IN)
await user.click(within(rowA).getByRole('checkbox'));
expect(onFilterChange).toHaveBeenCalledTimes(1);
const filter = getFilterFromCall(onFilterChange);
expect(filter?.op).toBe('not in');
expect(filter?.value).toStrictEqual(['valueB', 'valueA']);
});
it('creates NOT IN for single value when toggling related item with existing IN filter', async () => {
const user = userEvent.setup();
const onFilterChange = jest.fn();
mockFieldsValuesAPI({
relatedValues: ['relatedValue'],
stringValues: ['otherValue'],
});
// Start with IN filter for otherValue (Selected section)
// relatedValue is in Related section (checked visually)
renderWithFilter(onFilterChange, { op: 'in', value: ['otherValue'] });
const selectedRow = await screen.findByTestId(
'checkbox-value-row-otherValue',
);
expect(selectedRow).toHaveAttribute('data-state', 'checked');
const relatedRow = screen.getByTestId('checkbox-value-row-relatedValue');
expect(relatedRow).toHaveAttribute('data-state', 'checked');
// Toggle related item (checked -> should create NOT IN with just this value)
await user.click(within(relatedRow).getByRole('checkbox'));
expect(onFilterChange).toHaveBeenCalledTimes(1);
const filter = getFilterFromCall(onFilterChange);
expect(filter?.op).toBe('not in');
expect(filter?.value).toBe('relatedValue');
});
});
describe('custom renderer', () => {
it('uses customRendererForValue when provided', async () => {
mockFieldsValuesAPI({
stringValues: ['production'],
});
const customRenderer = (value: string): JSX.Element => (
<span data-testid="custom-rendered">{`ENV: ${value}`}</span>
);
render(
<CheckboxFilterV2
filter={{ ...DEFAULT_FILTER, customRendererForValue: customRenderer }}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('custom-rendered');
expect(screen.getByText('ENV: production')).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,499 @@
import { screen, within } from '@testing-library/react';
import { render } from 'tests/test-utils';
import { QuickFiltersSource } from '../../../../types';
import CheckboxFilterV2 from '../CheckboxFilterV2';
import {
DEFAULT_FILTER,
DEFAULT_USE_FIELD_APIS,
mockFieldsValuesAPI,
setupServer,
} from '../CheckboxFilterV2.testUtils';
setupServer();
describe('CheckboxFilterV2 - item rules', () => {
describe('no existing query', () => {
it('all values show as checked with no badge when no query exists', async () => {
mockFieldsValuesAPI({
stringValues: ['production', 'staging'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
const productionRow = await screen.findByTestId(
'checkbox-value-row-production',
);
expect(within(productionRow).getByText('production')).toBeInTheDocument();
const stagingRow = screen.getByTestId('checkbox-value-row-staging');
expect(productionRow).toHaveAttribute('data-state', 'checked');
expect(stagingRow).toHaveAttribute('data-state', 'checked');
// No section dividers when no existing query (all in "selected" section)
expect(
screen.queryByTestId('section-divider-related'),
).not.toBeInTheDocument();
expect(
screen.queryByTestId('section-divider-all-values'),
).not.toBeInTheDocument();
});
});
describe('with existing query (related values)', () => {
it('shows related values in "Related" section with checked state', async () => {
mockFieldsValuesAPI({
relatedValues: ['production'],
stringValues: ['staging', 'development'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
const productionRow = await screen.findByTestId(
'checkbox-value-row-production',
);
expect(within(productionRow).getByText('production')).toBeInTheDocument();
// Related values show as checked in "Related" section
expect(screen.getByTestId('section-divider-related')).toBeInTheDocument();
expect(productionRow).toHaveAttribute('data-state', 'checked');
// Other values show as unchecked in "All values" section
expect(screen.getByTestId('section-divider-all-values')).toBeInTheDocument();
const stagingRow = screen.getByTestId('checkbox-value-row-staging');
expect(stagingRow).toHaveAttribute('data-state', 'unchecked');
});
it('shows no badge for values not in relatedValues (unchecked state in All values section)', async () => {
mockFieldsValuesAPI({
relatedValues: ['production'],
stringValues: ['staging'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
const stagingRow = await screen.findByTestId('checkbox-value-row-staging');
expect(within(stagingRow).getByText('staging')).toBeInTheDocument();
expect(stagingRow).toHaveAttribute('data-state', 'unchecked');
expect(within(stagingRow).queryByTestId(/^badge-/)).not.toBeInTheDocument();
});
it('shows related values as checked when hasFilterForThisKey=true and isInRelatedValues=true', async () => {
mockFieldsValuesAPI({
relatedValues: ['production', 'staging'],
stringValues: ['development'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'in',
value: ['production'],
},
],
op: 'AND',
},
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
// production is selected - in selected section
const productionRow = await screen.findByTestId(
'checkbox-value-row-production',
);
expect(productionRow).toHaveAttribute('data-state', 'checked');
// staging is related - in related section with checked state
const stagingRow = screen.getByTestId('checkbox-value-row-staging');
expect(stagingRow).toHaveAttribute('data-state', 'checked');
expect(screen.getByTestId('section-divider-related')).toBeInTheDocument();
});
});
describe('selected values with IN operator', () => {
it('shows checked state with no badge for IN-selected values', async () => {
mockFieldsValuesAPI({
stringValues: ['production', 'staging'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'in',
value: ['production'],
},
],
op: 'AND',
},
},
],
},
},
} as never,
},
);
const productionRow = await screen.findByTestId(
'checkbox-value-row-production',
);
expect(productionRow).toHaveAttribute('data-state', 'checked');
expect(
within(productionRow).queryByTestId(/^badge-/),
).not.toBeInTheDocument();
const stagingRow = screen.getByTestId('checkbox-value-row-staging');
expect(stagingRow).toHaveAttribute('data-state', 'unchecked');
});
});
describe('selected values with NOT IN operator', () => {
it('shows unchecked state with no badge for NOT_IN-selected values', async () => {
mockFieldsValuesAPI({
stringValues: ['production', 'staging'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'not in',
value: ['production'],
},
],
op: 'AND',
},
},
],
},
},
} as never,
},
);
const productionRow = await screen.findByTestId(
'checkbox-value-row-production',
);
expect(productionRow).toHaveAttribute('data-state', 'unchecked');
expect(
within(productionRow).queryByTestId(/^badge-/),
).not.toBeInTheDocument();
const stagingRow = screen.getByTestId('checkbox-value-row-staging');
expect(stagingRow).toHaveAttribute('data-state', 'unchecked');
expect(within(stagingRow).queryByTestId(/^badge-/)).not.toBeInTheDocument();
});
});
describe('section ordering', () => {
it('orders selected section before related section before other section', async () => {
mockFieldsValuesAPI({
relatedValues: ['related-value'],
stringValues: ['other-value', 'selected-value'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'in',
value: ['selected-value'],
},
],
op: 'AND',
},
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-selected-value');
const allRows = screen.getAllByTestId(/^checkbox-value-row-/);
const values = allRows.map((row) =>
row.getAttribute('data-testid')?.replace('checkbox-value-row-', ''),
);
expect(values[0]).toBe('selected-value');
expect(values[1]).toBe('related-value');
expect(values[2]).toBe('other-value');
});
it('sorts alphabetically within same section', async () => {
mockFieldsValuesAPI({
relatedValues: ['zebra', 'alpha', 'mike'],
stringValues: [],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: { items: [], op: 'AND' },
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
await screen.findByTestId('checkbox-value-row-alpha');
const allRows = screen.getAllByTestId(/^checkbox-value-row-/);
const values = allRows.map((row) =>
row.getAttribute('data-testid')?.replace('checkbox-value-row-', ''),
);
expect(values).toStrictEqual(['alpha', 'mike', 'zebra']);
});
});
describe('mixed state scenarios', () => {
it('handles mixed state: IN-selected + related + other in same list', async () => {
mockFieldsValuesAPI({
relatedValues: ['related-env'],
stringValues: ['other-env', 'selected-env'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'in',
value: ['selected-env'],
},
],
op: 'AND',
},
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
const selectedRow = await screen.findByTestId(
'checkbox-value-row-selected-env',
);
expect(selectedRow).toHaveAttribute('data-state', 'checked');
expect(within(selectedRow).queryByTestId(/^badge-/)).not.toBeInTheDocument();
// Related values now show as checked
const relatedRow = screen.getByTestId('checkbox-value-row-related-env');
expect(relatedRow).toHaveAttribute('data-state', 'checked');
expect(screen.getByTestId('section-divider-related')).toBeInTheDocument();
const otherRow = screen.getByTestId('checkbox-value-row-other-env');
expect(otherRow).toHaveAttribute('data-state', 'unchecked');
expect(within(otherRow).queryByTestId(/^badge-/)).not.toBeInTheDocument();
});
it('handles NOT_IN-selected alongside related values', async () => {
mockFieldsValuesAPI({
relatedValues: ['related-env'],
stringValues: ['other-env', 'excluded-env'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={{
...DEFAULT_USE_FIELD_APIS,
existingQuery: 'service.name = "api"',
}}
/>,
undefined,
{
queryBuilderOverrides: {
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: { key: 'deployment.environment' },
op: 'not in',
value: ['excluded-env'],
},
],
op: 'AND',
},
filter: { expression: 'service.name = "api"' },
},
],
},
},
} as never,
},
);
const excludedRow = await screen.findByTestId(
'checkbox-value-row-excluded-env',
);
expect(excludedRow).toHaveAttribute('data-state', 'unchecked');
expect(within(excludedRow).queryByTestId(/^badge-/)).not.toBeInTheDocument();
// Related values show as checked
const relatedRow = screen.getByTestId('checkbox-value-row-related-env');
expect(relatedRow).toHaveAttribute('data-state', 'checked');
expect(screen.getByTestId('section-divider-related')).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,207 @@
import { screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { server, rest } from 'mocks-server/server';
import { render } from 'tests/test-utils';
import { QuickFiltersSource } from '../../../../types';
import CheckboxFilterV2 from '../CheckboxFilterV2';
import {
DEFAULT_FILTER,
DEFAULT_USE_FIELD_APIS,
mockFieldsValuesAPI,
mockFieldsValuesAPILoading,
setupServer,
} from '../CheckboxFilterV2.testUtils';
setupServer();
describe('CheckboxFilterV2 - states', () => {
describe('loading states', () => {
it('shows skeleton while loading initial data', async () => {
mockFieldsValuesAPILoading();
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
expect(screen.getByTestId('checkbox-filter-v2')).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByTestId('checkbox-filter-v2').querySelector('.ant-skeleton'),
).toBeInTheDocument();
});
});
it('shows skeleton when initially closed filter is opened for the first time', async () => {
const user = userEvent.setup();
mockFieldsValuesAPILoading();
const closedFilter = { ...DEFAULT_FILTER, defaultOpen: false };
render(
<CheckboxFilterV2
filter={closedFilter}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
// Filter starts closed - no skeleton, no content
expect(
screen.getByTestId('checkbox-filter-v2').querySelector('.ant-skeleton'),
).not.toBeInTheDocument();
expect(
screen.queryByTestId('checkbox-filter-empty'),
).not.toBeInTheDocument();
// Click header to open
const header = screen.getByTestId('checkbox-filter-header');
await user.click(header);
// Should show skeleton while loading, NOT "No values found"
await waitFor(() => {
expect(
screen.getByTestId('checkbox-filter-v2').querySelector('.ant-skeleton'),
).toBeInTheDocument();
});
expect(
screen.queryByTestId('checkbox-filter-empty'),
).not.toBeInTheDocument();
});
it('shows search spinner when fetching after initial load', async () => {
const user = userEvent.setup();
let requestCount = 0;
server.use(
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
requestCount += 1;
if (requestCount === 1) {
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
values: {
relatedValues: [],
stringValues: ['production', 'staging'],
numberValues: [],
},
},
}),
);
}
return res(ctx.delay(10000));
}),
);
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-production');
const searchInput = screen.getByTestId('checkbox-filter-search');
await user.type(searchInput, 'prod');
await waitFor(() => {
expect(
screen.getByTestId('checkbox-filter-search-loading'),
).toBeInTheDocument();
});
});
});
describe('empty states', () => {
it('shows "No values found" when API returns empty arrays', async () => {
mockFieldsValuesAPI({
relatedValues: [],
stringValues: [],
numberValues: [],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
const emptySection = await screen.findByTestId('checkbox-filter-empty');
expect(emptySection).toBeInTheDocument();
});
});
describe('value rendering', () => {
it('renders values from API response', async () => {
mockFieldsValuesAPI({
stringValues: ['production', 'staging', 'development'],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
await screen.findByTestId('checkbox-value-row-production');
expect(screen.getByTestId('checkbox-value-row-staging')).toBeInTheDocument();
expect(
screen.getByTestId('checkbox-value-row-development'),
).toBeInTheDocument();
});
it('renders number values converted to strings', async () => {
mockFieldsValuesAPI({
numberValues: [200, 404, 500],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
const row200 = await screen.findByTestId('checkbox-value-row-200');
expect(within(row200).getByText('200')).toBeInTheDocument();
expect(
within(screen.getByTestId('checkbox-value-row-404')).getByText('404'),
).toBeInTheDocument();
expect(
within(screen.getByTestId('checkbox-value-row-500')).getByText('500'),
).toBeInTheDocument();
});
it('filters null/undefined values from response', async () => {
mockFieldsValuesAPI({
stringValues: ['valid', null, '', undefined as unknown as string],
});
render(
<CheckboxFilterV2
filter={DEFAULT_FILTER}
source={QuickFiltersSource.TRACES_EXPLORER}
useFieldApis={DEFAULT_USE_FIELD_APIS}
/>,
);
const validRow = await screen.findByTestId('checkbox-value-row-valid');
expect(within(validRow).getByText('valid')).toBeInTheDocument();
expect(screen.queryAllByTestId(/^checkbox-value-row-/)).toHaveLength(1);
});
});
});

View File

@@ -0,0 +1,156 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CheckboxFilterV2Header } from '../CheckboxFilterV2Header';
describe('CheckboxFilterV2Header', () => {
const defaultProps = {
title: 'Environment',
isOpen: false,
showClearAll: true,
isSomeFilterPresentForCurrentAttribute: true,
onToggleOpen: jest.fn(),
onClear: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('collapsed state', () => {
it('renders title', () => {
render(<CheckboxFilterV2Header {...defaultProps} isOpen={false} />);
expect(screen.getByText('Environment')).toBeInTheDocument();
});
it('sets data-state="closed" when collapsed', () => {
render(<CheckboxFilterV2Header {...defaultProps} isOpen={false} />);
const header = screen.getByTestId('checkbox-filter-header');
expect(header).toHaveAttribute('data-state', 'closed');
});
it('does not show clear button when collapsed', () => {
render(
<CheckboxFilterV2Header {...defaultProps} isOpen={false} showClearAll />,
);
expect(
screen.queryByTestId('checkbox-filter-clear-all'),
).not.toBeInTheDocument();
});
});
describe('expanded state', () => {
it('sets data-state="open" when expanded', () => {
render(<CheckboxFilterV2Header {...defaultProps} isOpen />);
const header = screen.getByTestId('checkbox-filter-header');
expect(header).toHaveAttribute('data-state', 'open');
});
it('shows clear button when expanded + showClearAll=true', () => {
render(<CheckboxFilterV2Header {...defaultProps} isOpen showClearAll />);
expect(screen.getByTestId('checkbox-filter-clear-all')).toBeInTheDocument();
expect(screen.getByText('Clear')).toBeInTheDocument();
});
it('hides clear button when showClearAll=false', () => {
render(
<CheckboxFilterV2Header {...defaultProps} isOpen showClearAll={false} />,
);
expect(
screen.queryByTestId('checkbox-filter-clear-all'),
).not.toBeInTheDocument();
});
it('hides clear button when no filter present for attribute', () => {
render(
<CheckboxFilterV2Header
{...defaultProps}
isOpen
showClearAll
isSomeFilterPresentForCurrentAttribute={false}
/>,
);
expect(
screen.queryByTestId('checkbox-filter-clear-all'),
).not.toBeInTheDocument();
});
});
describe('interactions', () => {
it('calls onToggleOpen on header click', async () => {
const user = userEvent.setup();
const onToggleOpen = jest.fn();
render(
<CheckboxFilterV2Header {...defaultProps} onToggleOpen={onToggleOpen} />,
);
await user.click(screen.getByTestId('checkbox-filter-header'));
expect(onToggleOpen).toHaveBeenCalledTimes(1);
});
it('calls onToggleOpen on Enter key', async () => {
const user = userEvent.setup();
const onToggleOpen = jest.fn();
render(
<CheckboxFilterV2Header {...defaultProps} onToggleOpen={onToggleOpen} />,
);
screen.getByTestId('checkbox-filter-header').focus();
await user.keyboard('{Enter}');
expect(onToggleOpen).toHaveBeenCalledTimes(1);
});
it('calls onToggleOpen on Space key', async () => {
const user = userEvent.setup();
const onToggleOpen = jest.fn();
render(
<CheckboxFilterV2Header {...defaultProps} onToggleOpen={onToggleOpen} />,
);
screen.getByTestId('checkbox-filter-header').focus();
await user.keyboard(' ');
expect(onToggleOpen).toHaveBeenCalledTimes(1);
});
it('calls onClear on clear button click', async () => {
const user = userEvent.setup();
const onClear = jest.fn();
render(
<CheckboxFilterV2Header {...defaultProps} isOpen onClear={onClear} />,
);
await user.click(screen.getByTestId('checkbox-filter-clear-all'));
expect(onClear).toHaveBeenCalledTimes(1);
});
it('clear button click does not trigger onToggleOpen', async () => {
const user = userEvent.setup();
const onToggleOpen = jest.fn();
const onClear = jest.fn();
render(
<CheckboxFilterV2Header
{...defaultProps}
isOpen
onToggleOpen={onToggleOpen}
onClear={onClear}
/>,
);
await user.click(screen.getByTestId('checkbox-filter-clear-all'));
expect(onClear).toHaveBeenCalledTimes(1);
expect(onToggleOpen).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,297 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { BadgeConfig } from '../itemRules';
import { CheckedState } from '../../../../types';
import { CheckboxFilterV2ValueRow } from '../CheckboxFilterV2ValueRow';
describe('CheckboxFilterV2ValueRow', () => {
const defaultProps = {
value: 'production',
checkedState: 'unchecked' as CheckedState,
disabled: false,
title: 'Environment',
onlyButtonLabel: 'Only',
onCheckboxChange: jest.fn(),
onOnlyOrAllClick: jest.fn(),
badge: null as BadgeConfig | null,
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('checked states', () => {
it('sets data-state="unchecked" for unchecked state', () => {
render(
<CheckboxFilterV2ValueRow {...defaultProps} checkedState="unchecked" />,
);
const row = screen.getByTestId('checkbox-value-row-production');
expect(row).toHaveAttribute('data-state', 'unchecked');
});
it('sets data-state="checked" for checked state', () => {
render(
<CheckboxFilterV2ValueRow {...defaultProps} checkedState="checked" />,
);
const row = screen.getByTestId('checkbox-value-row-production');
expect(row).toHaveAttribute('data-state', 'checked');
});
});
describe('badge variations', () => {
it('renders no badge when badge=null', () => {
render(<CheckboxFilterV2ValueRow {...defaultProps} badge={null} />);
expect(screen.queryByTestId(/^badge-/)).not.toBeInTheDocument();
});
it('renders "Not in" warning badge', () => {
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
badge={{ key: 'not_in', label: 'Not in', color: 'warning' }}
/>,
);
expect(screen.getByTestId('badge-not_in')).toBeInTheDocument();
expect(screen.getByText('Not in')).toBeInTheDocument();
});
it('renders "Related" robin badge', () => {
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
badge={{ key: 'related', label: 'Related', color: 'robin' }}
/>,
);
expect(screen.getByTestId('badge-related')).toBeInTheDocument();
expect(screen.getByText('Related')).toBeInTheDocument();
});
it('renders "Other" secondary badge', () => {
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
badge={{ key: 'other', label: 'Other', color: 'secondary' }}
/>,
);
expect(screen.getByTestId('badge-other')).toBeInTheDocument();
expect(screen.getByText('Other')).toBeInTheDocument();
});
});
describe('only/all button label', () => {
it('shows "Only" label by default', () => {
render(
<CheckboxFilterV2ValueRow {...defaultProps} onlyButtonLabel="Only" />,
);
expect(screen.getByText('Only')).toBeInTheDocument();
});
it('shows "All" label when appropriate', () => {
render(<CheckboxFilterV2ValueRow {...defaultProps} onlyButtonLabel="All" />);
expect(screen.getByText('All')).toBeInTheDocument();
});
});
describe('disabled state', () => {
it('sets data-disabled=true when disabled', () => {
render(<CheckboxFilterV2ValueRow {...defaultProps} disabled />);
const row = screen.getByTestId('checkbox-value-row-production');
expect(row).toHaveAttribute('data-disabled', 'true');
});
it('does not call onOnlyOrAllClick when disabled + clicked', async () => {
const user = userEvent.setup();
const onOnlyOrAllClick = jest.fn();
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
disabled
onOnlyOrAllClick={onOnlyOrAllClick}
/>,
);
await user.click(screen.getByText('production'));
expect(onOnlyOrAllClick).not.toHaveBeenCalled();
});
it('does not call onOnlyOrAllClick on keydown when disabled', async () => {
const user = userEvent.setup();
const onOnlyOrAllClick = jest.fn();
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
disabled
onOnlyOrAllClick={onOnlyOrAllClick}
/>,
);
screen.getByText('production').focus();
await user.keyboard('{Enter}');
expect(onOnlyOrAllClick).not.toHaveBeenCalled();
});
});
describe('special value indicators', () => {
it('renders row for "true" value', () => {
render(<CheckboxFilterV2ValueRow {...defaultProps} value="true" />);
expect(screen.getByTestId('checkbox-value-row-true')).toBeInTheDocument();
expect(screen.getByText('true')).toBeInTheDocument();
});
it('renders row for "false" value', () => {
render(<CheckboxFilterV2ValueRow {...defaultProps} value="false" />);
expect(screen.getByTestId('checkbox-value-row-false')).toBeInTheDocument();
expect(screen.getByText('false')).toBeInTheDocument();
});
it('renders row for regular values', () => {
render(<CheckboxFilterV2ValueRow {...defaultProps} value="production" />);
expect(
screen.getByTestId('checkbox-value-row-production'),
).toBeInTheDocument();
expect(screen.getByText('production')).toBeInTheDocument();
});
});
describe('interactions', () => {
it('renders checkbox with correct testId', () => {
render(
<CheckboxFilterV2ValueRow {...defaultProps} checkedState="unchecked" />,
);
expect(
screen.getByTestId('checkbox-Environment-production'),
).toBeInTheDocument();
});
it('calls onOnlyOrAllClick on value text click', async () => {
const user = userEvent.setup();
const onOnlyOrAllClick = jest.fn();
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
onOnlyOrAllClick={onOnlyOrAllClick}
/>,
);
await user.click(screen.getByText('production'));
expect(onOnlyOrAllClick).toHaveBeenCalledTimes(1);
});
it('calls onOnlyOrAllClick on Enter key', async () => {
const user = userEvent.setup();
const onOnlyOrAllClick = jest.fn();
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
onOnlyOrAllClick={onOnlyOrAllClick}
/>,
);
const valueButton = screen
.getByText('production')
.closest('[role="button"]');
await user.tab();
await user.tab();
if (valueButton && document.activeElement === valueButton) {
await user.keyboard('{Enter}');
}
expect(onOnlyOrAllClick).toHaveBeenCalled();
});
it('calls onOnlyOrAllClick on Space key', async () => {
const user = userEvent.setup();
const onOnlyOrAllClick = jest.fn();
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
onOnlyOrAllClick={onOnlyOrAllClick}
/>,
);
const valueButton = screen
.getByText('production')
.closest('[role="button"]');
await user.tab();
await user.tab();
if (valueButton && document.activeElement === valueButton) {
await user.keyboard(' ');
}
expect(onOnlyOrAllClick).toHaveBeenCalled();
});
it('shows Toggle button', () => {
render(<CheckboxFilterV2ValueRow {...defaultProps} />);
expect(screen.getByText('Toggle')).toBeInTheDocument();
});
});
describe('custom renderer', () => {
it('uses customRendererForValue when provided', () => {
const customRenderer = (value: string): JSX.Element => (
<span data-testid="custom-render">{`Custom: ${value}`}</span>
);
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
customRendererForValue={customRenderer}
/>,
);
expect(screen.getByTestId('custom-render')).toBeInTheDocument();
expect(screen.getByText('Custom: production')).toBeInTheDocument();
});
it('shows default value text when no custom renderer', () => {
render(<CheckboxFilterV2ValueRow {...defaultProps} />);
expect(screen.getByText('production')).toBeInTheDocument();
});
});
describe('state combinations', () => {
it('checked + not_in badge', () => {
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
checkedState="unchecked"
badge={{ key: 'not_in', label: 'Not in', color: 'warning' }}
/>,
);
expect(screen.getByTestId('badge-not_in')).toBeInTheDocument();
});
it('disabled + badge still shows badge', () => {
render(
<CheckboxFilterV2ValueRow
{...defaultProps}
disabled
badge={{ key: 'other', label: 'Other', color: 'secondary' }}
/>,
);
expect(screen.getByTestId('badge-other')).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,131 @@
import { deriveItemConfig, ItemContext, SectionType } from '../itemRules';
describe('itemRules', () => {
describe('deriveItemConfig', () => {
it('no query at all → section selected, no badge', () => {
const ctx: ItemContext = {
isSelectedOnFilter: false,
isInRelatedValues: true,
isNotInOperator: false,
hasExistingQuery: false,
hasFilterForThisKey: false,
};
const result = deriveItemConfig(ctx);
expect(result.section).toBe(SectionType.SELECTED);
expect(result.badge).toBeNull();
});
it('selected + IN operator → section selected, no badge', () => {
const ctx: ItemContext = {
isSelectedOnFilter: true,
isInRelatedValues: true,
isNotInOperator: false,
hasExistingQuery: true,
hasFilterForThisKey: true,
};
const result = deriveItemConfig(ctx);
expect(result.section).toBe(SectionType.SELECTED);
expect(result.badge).toBeNull();
});
it('selected + NOT IN operator → section selected, no badge, unchecked', () => {
const ctx: ItemContext = {
isSelectedOnFilter: true,
isInRelatedValues: false,
isNotInOperator: true,
hasExistingQuery: true,
hasFilterForThisKey: true,
};
const result = deriveItemConfig(ctx);
expect(result.section).toBe(SectionType.SELECTED);
expect(result.badge).toBeNull();
expect(result.checkedState).toBe('unchecked');
});
it('has query, not selected, in related → section related, checked', () => {
const ctx: ItemContext = {
isSelectedOnFilter: false,
isInRelatedValues: true,
isNotInOperator: false,
hasExistingQuery: true,
hasFilterForThisKey: false,
};
const result = deriveItemConfig(ctx);
expect(result.section).toBe(SectionType.RELATED);
expect(result.badge).toBeNull();
expect(result.checkedState).toBe('checked');
});
it('has query, has filter for this key, in related → section related, checked', () => {
const ctx: ItemContext = {
isSelectedOnFilter: false,
isInRelatedValues: true,
isNotInOperator: false,
hasExistingQuery: true,
hasFilterForThisKey: true,
};
const result = deriveItemConfig(ctx);
expect(result.section).toBe(SectionType.RELATED);
expect(result.badge).toBeNull();
expect(result.checkedState).toBe('checked');
});
it('has query, not in related → section all_values, unchecked', () => {
const ctx: ItemContext = {
isSelectedOnFilter: false,
isInRelatedValues: false,
isNotInOperator: false,
hasExistingQuery: true,
hasFilterForThisKey: false,
};
const result = deriveItemConfig(ctx);
expect(result.section).toBe(SectionType.ALL_VALUES);
expect(result.badge).toBeNull();
expect(result.checkedState).toBe('unchecked');
});
it('has query + filter for key, not selected, not in related → section all_values, unchecked', () => {
const ctx: ItemContext = {
isSelectedOnFilter: false,
isInRelatedValues: false,
isNotInOperator: false,
hasExistingQuery: true,
hasFilterForThisKey: true,
};
const result = deriveItemConfig(ctx);
expect(result.section).toBe(SectionType.ALL_VALUES);
expect(result.badge).toBeNull();
expect(result.checkedState).toBe('unchecked');
});
it('no query but has filter for key, not selected → fallback to checked (DEFAULT_CONFIG)', () => {
const ctx: ItemContext = {
isSelectedOnFilter: false,
isInRelatedValues: false,
isNotInOperator: false,
hasExistingQuery: false,
hasFilterForThisKey: true,
};
const result = deriveItemConfig(ctx);
expect(result.section).toBe(SectionType.SELECTED);
expect(result.badge).toBeNull();
expect(result.checkedState).toBe('checked');
});
});
});

View File

@@ -0,0 +1,382 @@
import { renderHook } from '@testing-library/react';
import { SectionType } from '../itemRules';
import { useSectionedValues, SectionedItem } from '../useSectionedValues';
function flattenSections(
sections: { type: SectionType; items: SectionedItem[] }[],
): SectionedItem[] {
return sections.flatMap((s) => s.items);
}
describe('useSectionedValues', () => {
const baseInput = {
relatedValues: ['val1', 'val2'],
allValues: ['val1', 'val2', 'val3'],
currentFilterState: {},
isSomeFilterPresentForCurrentAttribute: false,
isNotInOperator: false,
hasExistingQuery: false,
visibleItemsCount: 10,
relatedExclusions: [] as string[],
};
it('no query at all → all items in selected section, no badges', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
hasExistingQuery: false,
isSomeFilterPresentForCurrentAttribute: false,
}),
);
expect(result.current.sections).toHaveLength(1);
expect(result.current.sections[0].type).toBe(SectionType.SELECTED);
expect(result.current.sections[0].items).toHaveLength(3);
result.current.sections[0].items.forEach((item) => {
expect(item.badge).toBeNull();
});
});
it('has query, no filter for key → related and all_values sections', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: false,
}),
);
const allItems = flattenSections(result.current.sections);
const relatedItems = allItems.filter(
(item) => item.value === 'val1' || item.value === 'val2',
);
const otherItems = allItems.filter((item) => item.value === 'val3');
// Related values should be in related section, checked
relatedItems.forEach((item) => {
expect(item.section).toBe(SectionType.RELATED);
expect(item.checkedState).toBe('checked');
});
// Other values should be in all_values section, unchecked
otherItems.forEach((item) => {
expect(item.section).toBe(SectionType.ALL_VALUES);
expect(item.checkedState).toBe('unchecked');
});
});
it('has query + filter for key, selected value → in selected section', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: true,
currentFilterState: { val1: true, val2: false, val3: false },
}),
);
const allItems = flattenSections(result.current.sections);
const selectedItem = allItems.find((item) => item.value === 'val1');
expect(selectedItem?.section).toBe(SectionType.SELECTED);
expect(selectedItem?.badge).toBeNull();
});
it('has query + filter for key, NOT IN operator → excluded values in selected section, no badge', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: true,
isNotInOperator: true,
currentFilterState: { val1: false, val2: true, val3: true },
}),
);
const allItems = flattenSections(result.current.sections);
// val1 is unchecked + NOT IN = excluded
const excludedItem = allItems.find((item) => item.value === 'val1');
expect(excludedItem?.section).toBe(SectionType.SELECTED);
expect(excludedItem?.badge).toBeNull();
expect(excludedItem?.checkedState).toBe('unchecked');
});
it('items within same section sorted alphabetically', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
relatedValues: ['zebra', 'apple', 'mango'],
allValues: ['zebra', 'apple', 'mango'],
hasExistingQuery: false,
isSomeFilterPresentForCurrentAttribute: false,
}),
);
// All items have section selected, should be sorted alphabetically
const allItems = flattenSections(result.current.sections);
const values = allItems.map((item) => item.value);
expect(values).toStrictEqual(['apple', 'mango', 'zebra']);
});
describe('filtered results from API', () => {
it('keeps items in their natural sections with filtered API results', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: true,
currentFilterState: { val1: true },
}),
);
const sectionTypes = result.current.sections.map((s) => s.type);
expect(sectionTypes).toContain(SectionType.SELECTED);
expect(sectionTypes).toContain(SectionType.RELATED);
});
it('returns empty sections array when no values', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
relatedValues: [],
allValues: [],
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: false,
currentFilterState: {},
}),
);
expect(result.current.sections).toHaveLength(0);
expect(result.current.totalCount).toBe(0);
});
it('non-related items go to ALL_VALUES section', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
relatedValues: [],
allValues: ['other1', 'other2', 'other3'],
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: false,
}),
);
const allValuesSection = result.current.sections.find(
(s) => s.type === SectionType.ALL_VALUES,
);
expect(allValuesSection?.items).toHaveLength(3);
});
it('handles non-overlapping relatedValues and allValues correctly', () => {
// This tests the bug where different pod names in relatedValues vs allValues
// caused all items to go to ALL_VALUES instead of RELATED
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
relatedValues: ['pod-a-1', 'pod-b-1', 'pod-c-1'],
allValues: ['pod-a-2', 'pod-b-2', 'pod-c-2'],
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: false,
}),
);
const sectionTypes = result.current.sections.map((s) => s.type);
// RELATED section should exist with relatedValues items
expect(sectionTypes).toContain(SectionType.RELATED);
const relatedSection = result.current.sections.find(
(s) => s.type === SectionType.RELATED,
);
expect(relatedSection?.items).toHaveLength(3);
expect(relatedSection?.items.map((i) => i.value)).toStrictEqual(
expect.arrayContaining(['pod-a-1', 'pod-b-1', 'pod-c-1']),
);
// ALL_VALUES section should exist with allValues items
expect(sectionTypes).toContain(SectionType.ALL_VALUES);
const allValuesSection = result.current.sections.find(
(s) => s.type === SectionType.ALL_VALUES,
);
expect(allValuesSection?.items).toHaveLength(3);
expect(allValuesSection?.items.map((i) => i.value)).toStrictEqual(
expect.arrayContaining(['pod-a-2', 'pod-b-2', 'pod-c-2']),
);
});
});
describe('stale related values during refresh (relatedExclusions)', () => {
// Scenario: key A selected (existing query) + key B had value "oldSelected".
// User selects a different value on key B -> "oldSelected" is de-selected.
// keepPreviousData keeps the stale response where "oldSelected" is still in
// relatedValues, so it would wrongly render as "related". It is excluded.
it('subtracts only the excluded value from the RELATED section', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
currentFilterState: { newValue: true },
isSomeFilterPresentForCurrentAttribute: true,
hasExistingQuery: true,
// stale API data kept via keepPreviousData
relatedValues: ['oldSelected', 'otherRelated'],
allValues: ['newValue'],
relatedExclusions: ['oldSelected'],
}),
);
const allItems = flattenSections(result.current.sections);
// oldSelected must NOT render as related (dropped entirely - not in
// allValues nor selected)
expect(
allItems.find((item) => item.value === 'oldSelected'),
).toBeUndefined();
// other related values are kept
expect(allItems.find((item) => item.value === 'otherRelated')?.section).toBe(
SectionType.RELATED,
);
});
it('preserves every related value that is not the excluded (previously selected) one', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
currentFilterState: { newValue: true },
isSomeFilterPresentForCurrentAttribute: true,
hasExistingQuery: true,
// oldSelected was just de-selected; the rest are genuinely related
relatedValues: ['oldSelected', 'relatedA', 'relatedB', 'relatedC'],
allValues: ['newValue'],
relatedExclusions: ['oldSelected'],
}),
);
const relatedSection = result.current.sections.find(
(s) => s.type === SectionType.RELATED,
);
const relatedValues = relatedSection?.items.map((i) => i.value) ?? [];
// all non-excluded related values are kept
expect(relatedValues).toStrictEqual(['relatedA', 'relatedB', 'relatedC']);
// and they stay checked
relatedSection?.items.forEach((item) => {
expect(item.checkedState).toBe('checked');
});
// the excluded value is gone
expect(relatedValues).not.toContain('oldSelected');
});
it('keeps the RELATED section when other related values remain', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
currentFilterState: { newValue: true },
isSomeFilterPresentForCurrentAttribute: true,
hasExistingQuery: true,
relatedValues: ['oldSelected', 'otherRelated'],
allValues: ['newValue'],
relatedExclusions: ['oldSelected'],
}),
);
const sectionTypes = result.current.sections.map((s) => s.type);
expect(sectionTypes).toContain(SectionType.RELATED);
expect(sectionTypes).toContain(SectionType.SELECTED);
});
it('shows the excluded value normally when exclusions are empty', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
currentFilterState: { newValue: true },
isSomeFilterPresentForCurrentAttribute: true,
hasExistingQuery: true,
relatedValues: ['oldSelected', 'otherRelated'],
allValues: ['newValue'],
relatedExclusions: [],
}),
);
const allItems = flattenSections(result.current.sections);
expect(allItems.find((item) => item.value === 'oldSelected')?.section).toBe(
SectionType.RELATED,
);
});
});
describe('section ordering', () => {
it('sections appear in order: SELECTED → RELATED → ALL_VALUES', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
relatedValues: ['related1'],
allValues: ['all1'],
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: true,
currentFilterState: { selected1: true },
}),
);
const sectionTypes = result.current.sections.map((s) => s.type);
// Verify order
const selectedIdx = sectionTypes.indexOf(SectionType.SELECTED);
const relatedIdx = sectionTypes.indexOf(SectionType.RELATED);
const allValuesIdx = sectionTypes.indexOf(SectionType.ALL_VALUES);
expect(selectedIdx).toBeLessThan(relatedIdx);
expect(relatedIdx).toBeLessThan(allValuesIdx);
});
it('RELATED section appears before ALL_VALUES even with many items', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
relatedValues: ['r1', 'r2', 'r3', 'r4', 'r5'],
allValues: ['a1', 'a2', 'a3', 'a4', 'a5'],
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: false,
visibleItemsCount: 100,
}),
);
const sectionTypes = result.current.sections.map((s) => s.type);
expect(sectionTypes[0]).toBe(SectionType.RELATED);
expect(sectionTypes[1]).toBe(SectionType.ALL_VALUES);
});
it('visibleItemsCount limits total items across sections', () => {
const { result } = renderHook(() =>
useSectionedValues({
...baseInput,
relatedValues: ['r1', 'r2', 'r3'],
allValues: ['a1', 'a2', 'a3'],
hasExistingQuery: true,
isSomeFilterPresentForCurrentAttribute: false,
visibleItemsCount: 4,
}),
);
const totalItems = result.current.sections.reduce(
(sum, s) => sum + s.items.length,
0,
);
expect(totalItems).toBe(4);
// RELATED section should get priority (3 items)
const relatedSection = result.current.sections.find(
(s) => s.type === SectionType.RELATED,
);
expect(relatedSection?.items).toHaveLength(3);
// ALL_VALUES gets remaining (1 item)
const allValuesSection = result.current.sections.find(
(s) => s.type === SectionType.ALL_VALUES,
);
expect(allValuesSection?.items).toHaveLength(1);
});
});
});

View File

@@ -0,0 +1,118 @@
import { CheckedState } from '../../../types';
export enum SectionType {
SELECTED = 'selected',
RELATED = 'related',
ALL_VALUES = 'all_values',
}
export interface BadgeConfig {
key: string;
label: string;
color: 'robin' | 'warning' | 'secondary';
}
export interface ItemConfig {
section: SectionType;
badge: BadgeConfig | null;
checkedState: CheckedState;
}
export interface ItemContext {
isSelectedOnFilter: boolean;
isInRelatedValues: boolean;
isNotInOperator: boolean;
hasExistingQuery: boolean;
hasFilterForThisKey: boolean;
}
export interface DerivedItem extends ItemConfig {
value: string;
}
interface ItemRule {
condition: (ctx: ItemContext) => boolean;
config: ItemConfig;
}
const ITEM_RULES: ItemRule[] = [
// No existing query and no filter → all checked (selected section)
{
condition: (ctx): boolean =>
!ctx.hasExistingQuery && !ctx.hasFilterForThisKey,
config: {
section: SectionType.SELECTED,
badge: null,
checkedState: 'checked',
},
},
// Selected with NOT IN operator → unchecked, no badge
{
condition: (ctx): boolean => ctx.isSelectedOnFilter && ctx.isNotInOperator,
config: {
section: SectionType.SELECTED,
badge: null,
checkedState: 'unchecked',
},
},
// Selected with IN operator → checked
{
condition: (ctx): boolean => ctx.isSelectedOnFilter && !ctx.isNotInOperator,
config: {
section: SectionType.SELECTED,
badge: null,
checkedState: 'checked',
},
},
// Related values (from existing query) → checked
{
condition: (ctx): boolean => ctx.hasExistingQuery && ctx.isInRelatedValues,
config: {
section: SectionType.RELATED,
badge: null,
checkedState: 'checked',
},
},
// All values (has existing query but not related) → unchecked
{
condition: (ctx): boolean => ctx.hasExistingQuery,
config: {
section: SectionType.ALL_VALUES,
badge: null,
checkedState: 'unchecked',
},
},
];
// Fallback when no rule matches
const DEFAULT_CONFIG: ItemConfig = {
section: SectionType.SELECTED,
badge: null,
checkedState: 'checked',
};
export function deriveItemConfig(ctx: ItemContext): ItemConfig {
for (const rule of ITEM_RULES) {
if (rule.condition(ctx)) {
return rule.config;
}
}
return DEFAULT_CONFIG;
}
export function deriveItems(
values: string[],
relatedSet: Set<string>,
selectedOnFilterSet: Set<string>,
ctx: Omit<ItemContext, 'isSelectedOnFilter' | 'isInRelatedValues'>,
): DerivedItem[] {
return values.map((value) => {
const itemCtx: ItemContext = {
...ctx,
isSelectedOnFilter: selectedOnFilterSet.has(value),
isInRelatedValues: relatedSet.has(value),
};
const config = deriveItemConfig(itemCtx);
return { value, ...config };
});
}

View File

@@ -0,0 +1,61 @@
import { useMemo } from 'react';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
import { QuickFilterCheckboxUseFieldApis } from 'components/QuickFilters/types';
interface UseExistingQueryParams {
useFieldApis: QuickFilterCheckboxUseFieldApis;
activeQueryIndex: number;
}
interface UseExistingQueryResult {
existingQuery: string | undefined;
hasExistingQuery: boolean;
}
export function useExistingQuery({
useFieldApis,
activeQueryIndex,
}: UseExistingQueryParams): UseExistingQueryResult {
const { currentQuery } = useQueryBuilder();
const existingQuery = useMemo(() => {
if (useFieldApis.existingQuery === null) {
return undefined;
}
if (useFieldApis.existingQuery) {
return useFieldApis.existingQuery;
}
const queryData = currentQuery.builder.queryData?.[activeQueryIndex];
// Prefer V5 filter.expression
if (queryData?.filter?.expression) {
return queryData.filter.expression;
}
// Fall back to V3 filters.items
if (queryData?.filters?.items?.length) {
return convertFiltersToExpression(queryData.filters).expression;
}
return undefined;
}, [
useFieldApis.existingQuery,
currentQuery.builder.queryData,
activeQueryIndex,
]);
// Check if ANY filters exist in query (V3 items or V5 expression)
// This is separate from existingQuery because existingQuery can be explicitly
// disabled (null) while filters still exist in the query for UI purposes
const hasExistingQuery = useMemo(() => {
const queryData = currentQuery.builder.queryData?.[activeQueryIndex];
const hasV3Items = (queryData?.filters?.items?.length ?? 0) > 0;
const hasV5Expression = !!queryData?.filter?.expression;
return hasV3Items || hasV5Expression || !!existingQuery;
}, [currentQuery.builder.queryData, activeQueryIndex, existingQuery]);
return { existingQuery, hasExistingQuery };
}

View File

@@ -0,0 +1,97 @@
import { useMemo } from 'react';
import { useGetFieldsValues } from 'api/generated/services/fields';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
import { DataSource } from 'types/common/queryBuilder';
import { FIELD_API_CACHE_TIME } from 'constants/queryCacheTime';
interface UseFieldValuesProps {
filter: IQuickFiltersConfig;
searchText: string;
existingQuery?: string;
metricNamespace?: string;
startUnixMilli?: number;
endUnixMilli?: number;
enabled: boolean;
}
interface UseFieldValuesReturn {
relatedValues: string[];
allValues: string[];
isLoading: boolean;
isFetching: boolean;
}
const DATA_SOURCE_TO_SIGNAL: Record<DataSource, TelemetrytypesSignalDTO> = {
[DataSource.METRICS]: TelemetrytypesSignalDTO.metrics,
[DataSource.TRACES]: TelemetrytypesSignalDTO.traces,
[DataSource.LOGS]: TelemetrytypesSignalDTO.logs,
};
export function useFieldValues({
filter,
searchText,
existingQuery,
metricNamespace,
startUnixMilli,
endUnixMilli,
enabled,
}: UseFieldValuesProps): UseFieldValuesReturn {
const { data, isLoading, isFetching } = useGetFieldsValues(
{
signal: filter.dataSource
? DATA_SOURCE_TO_SIGNAL[filter.dataSource]
: undefined,
name: filter.attributeKey.key,
searchText,
existingQuery,
metricNamespace,
startUnixMilli,
// This field does not affect the backend but I wanted to keep it here
// in case we add the support in the future
endUnixMilli,
},
{
query: {
enabled,
cacheTime: FIELD_API_CACHE_TIME,
keepPreviousData: true,
},
},
);
const relatedValues: string[] = useMemo(() => {
const values = data?.data?.values;
if (!values) {
return [];
}
return (
values.relatedValues?.filter(
(value): value is string =>
value !== null && value !== undefined && value !== '',
) || []
);
}, [data]);
const allValues: string[] = useMemo(() => {
const values = data?.data?.values;
if (!values) {
return [];
}
const stringValues =
values.stringValues?.filter(
(value): value is string =>
value !== null && value !== undefined && value !== '',
) || [];
const numberValues =
values.numberValues
?.filter((value): value is number => value !== null && value !== undefined)
.map((value) => value.toString()) || [];
return [...stringValues, ...numberValues];
}, [data]);
return { relatedValues, allValues, isLoading, isFetching };
}

View File

@@ -0,0 +1,152 @@
import { useMemo } from 'react';
import { BadgeConfig, deriveItems, SectionType } from './itemRules';
import { CheckedState } from '../../../types';
interface SectionedValuesInput {
relatedValues: string[];
allValues: string[];
currentFilterState: Record<string, boolean>;
isSomeFilterPresentForCurrentAttribute: boolean;
isNotInOperator: boolean;
hasExistingQuery: boolean;
visibleItemsCount: number;
relatedExclusions: string[];
}
export interface SectionedItem {
value: string;
section: SectionType;
badge: BadgeConfig | null;
checkedState: CheckedState;
}
export interface Section {
type: SectionType;
items: SectionedItem[];
}
interface SectionedValuesOutput {
sections: Section[];
totalCount: number;
}
const SECTION_ORDER: SectionType[] = [
SectionType.SELECTED,
SectionType.RELATED,
SectionType.ALL_VALUES,
];
export function buildSelectedSet(
currentFilterState: Record<string, boolean>,
isSomeFilterPresentForCurrentAttribute: boolean,
isNotInOperator: boolean,
): Set<string> {
const selectedSet = new Set<string>();
if (!isSomeFilterPresentForCurrentAttribute) {
return selectedSet;
}
for (const [val, isChecked] of Object.entries(currentFilterState)) {
// NOT IN: unchecked = explicitly excluded
// IN: checked = explicitly selected
const shouldAdd = isNotInOperator ? !isChecked : isChecked;
if (shouldAdd) {
selectedSet.add(val);
}
}
return selectedSet;
}
export function useSectionedValues({
relatedValues,
allValues,
currentFilterState,
isSomeFilterPresentForCurrentAttribute,
isNotInOperator,
hasExistingQuery,
visibleItemsCount,
relatedExclusions,
}: SectionedValuesInput): SectionedValuesOutput {
const items = useMemo(() => {
const selectedSet = buildSelectedSet(
currentFilterState,
isSomeFilterPresentForCurrentAttribute,
isNotInOperator,
);
const exclusionSet = new Set(relatedExclusions);
const effectiveRelatedValues = relatedValues.filter(
(value) => !exclusionSet.has(value),
);
// Combine all values - API already filters both arrays by searchText
const allUniqueValues = Array.from(
new Set([...effectiveRelatedValues, ...allValues]),
);
// Include selected values at top - may not be in API response
const finalValues = [
...new Set([...Array.from(selectedSet), ...allUniqueValues]),
];
const relatedSet = new Set(effectiveRelatedValues);
return deriveItems(finalValues, relatedSet, selectedSet, {
isNotInOperator,
hasExistingQuery,
hasFilterForThisKey: isSomeFilterPresentForCurrentAttribute,
});
}, [
relatedValues,
allValues,
currentFilterState,
isSomeFilterPresentForCurrentAttribute,
isNotInOperator,
hasExistingQuery,
relatedExclusions,
]);
const sections = useMemo(() => {
// Group items by section
const sectionMap = new Map<SectionType, SectionedItem[]>();
for (const sectionType of SECTION_ORDER) {
sectionMap.set(sectionType, []);
}
for (const item of items) {
sectionMap.get(item.section)?.push(item);
}
// Sort items within each section alphabetically
for (const sectionItems of sectionMap.values()) {
sectionItems.sort((a, b) => a.value.localeCompare(b.value));
}
// Apply visibleItemsCount across all sections
let remaining = visibleItemsCount;
const result: Section[] = [];
for (const sectionType of SECTION_ORDER) {
const sectionItems = sectionMap.get(sectionType) || [];
if (sectionItems.length === 0) {
continue;
}
const itemsToTake = Math.min(sectionItems.length, remaining);
if (itemsToTake === 0) {
break;
}
result.push({
type: sectionType,
items: sectionItems.slice(0, itemsToTake),
});
remaining -= itemsToTake;
}
return result;
}, [items, visibleItemsCount]);
return { sections, totalCount: items.length };
}

View File

@@ -0,0 +1,36 @@
import { useEffect, useMemo, useRef } from 'react';
const EMPTY: string[] = [];
interface UseStaleRelatedExclusionsParams {
selectedValues: string[];
isFetching: boolean;
isRefreshing: boolean;
}
export function useStaleRelatedExclusions({
selectedValues,
isFetching,
isRefreshing,
}: UseStaleRelatedExclusionsParams): string[] {
const selectionAtLastFetchRef = useRef<string[]>(selectedValues);
useEffect(() => {
if (!isFetching) {
selectionAtLastFetchRef.current = selectedValues;
}
}, [isFetching, selectedValues]);
return useMemo(() => {
if (!isRefreshing) {
return EMPTY;
}
const current = new Set(selectedValues);
const removed = selectionAtLastFetchRef.current.filter(
(value) => !current.has(value),
);
return removed.length ? removed : EMPTY;
}, [isRefreshing, selectedValues]);
}

View File

@@ -1,8 +1,6 @@
.quick-filters-container {
display: flex;
flex-direction: row;
height: 100%;
min-height: 0;
position: relative;
.quick-filters-settings-container {

View File

@@ -32,6 +32,7 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { USER_ROLES } from 'types/roles';
import Checkbox from './FilterRenderers/Checkbox/Checkbox';
import CheckboxV2 from './FilterRenderers/Checkbox/v2/CheckboxFilterV2';
import Duration from './FilterRenderers/Duration/Duration';
import Slider from './FilterRenderers/Slider/Slider';
import useFilterConfig from './hooks/useFilterConfig';
@@ -51,6 +52,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
signal,
showFilterCollapse = true,
showQueryName = true,
useFieldApis,
} = props;
const { user } = useAppContext();
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
@@ -297,21 +299,45 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
{filterConfig.map((filter) => {
switch (filter.type) {
case FiltersType.CHECKBOX:
return (
return useFieldApis ? (
<CheckboxV2
key={filter.attributeKey.key}
source={source}
filter={filter}
onFilterChange={onFilterChange}
useFieldApis={useFieldApis}
/>
) : (
<Checkbox
key={filter.attributeKey.key}
source={source}
filter={filter}
onFilterChange={onFilterChange}
/>
);
case FiltersType.DURATION:
return <Duration filter={filter} onFilterChange={onFilterChange} />;
return (
<Duration
key={filter.attributeKey.key}
filter={filter}
onFilterChange={onFilterChange}
/>
);
case FiltersType.SLIDER:
return <Slider />;
return <Slider key={filter.attributeKey.key} />;
// eslint-disable-next-line sonarjs/no-duplicated-branches
default:
return (
return useFieldApis ? (
<CheckboxV2
key={filter.attributeKey.key}
source={source}
filter={filter}
onFilterChange={onFilterChange}
useFieldApis={useFieldApis}
/>
) : (
<Checkbox
key={filter.attributeKey.key}
source={source}
filter={filter}
onFilterChange={onFilterChange}
@@ -381,4 +407,5 @@ QuickFilters.defaultProps = {
config: [],
showFilterCollapse: true,
showQueryName: true,
useFieldApis: undefined,
};

View File

@@ -26,6 +26,11 @@ export enum SignalType {
METER_EXPLORER = 'meter',
}
/**
* Missing export from signozhq/ui/checkbox, TODO(H4ad): Add and remove this type definition
*/
export type CheckedState = 'checked' | 'unchecked' | 'indeterminate';
export interface IQuickFiltersConfig {
type: FiltersType;
title: string;
@@ -46,6 +51,7 @@ export interface IQuickFiltersProps {
className?: string;
showFilterCollapse?: boolean;
showQueryName?: boolean;
useFieldApis?: QuickFilterCheckboxUseFieldApis;
}
export enum QuickFiltersSource {
@@ -56,3 +62,19 @@ export enum QuickFiltersSource {
EXCEPTIONS = 'exceptions',
METER_EXPLORER = 'meter',
}
/**
* Opt-in: fetch values from the /v1/fields/values API instead of /v3/autocomplete/attribute_values
*/
export type QuickFilterCheckboxUseFieldApis = {
startUnixMilli: number;
endUnixMilli: number;
/**
* If you didn't specify a string, we automatically try to extract this from the currentQuery,
* from the filter.expression or filter.items.
*
* Use null to ignore/disable this behavior.
*/
existingQuery?: string | null;
metricNamespace?: string;
};

View File

@@ -3,7 +3,7 @@ import { Select } from 'antd';
import { Checkbox } from '@signozhq/ui/checkbox';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useListRoles } from 'api/generated/services/role';
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import type { AuthtypesGettableRoleDTO } from 'api/generated/services/sigNoz.schemas';
import cx from 'classnames';
import APIError from 'types/api/error';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -16,7 +16,7 @@ export interface RoleOption {
}
export function useRoles(): {
roles: AuthtypesRoleDTO[];
roles: AuthtypesGettableRoleDTO[];
isLoading: boolean;
isError: boolean;
error: APIError | undefined;
@@ -33,7 +33,7 @@ export function useRoles(): {
}
export function getRoleOptions(
roles: AuthtypesRoleDTO[],
roles: AuthtypesGettableRoleDTO[],
valueField: 'id' | 'name',
): RoleOption[] {
return roles.map((role) => ({
@@ -79,7 +79,7 @@ interface BaseProps {
placeholder?: string;
className?: string;
getPopupContainer?: (trigger: HTMLElement) => HTMLElement;
roles?: AuthtypesRoleDTO[];
roles?: AuthtypesGettableRoleDTO[];
loading?: boolean;
isError?: boolean;
error?: APIError;

View File

@@ -4,7 +4,7 @@ import { Badge } from '@signozhq/ui/badge';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { useCopyToClipboard } from 'react-use';
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
import type { AuthtypesGettableRoleDTO } from 'api/generated/services/sigNoz.schemas';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import { withAuthZContent } from 'lib/authz/components/withAuthZ/withAuthZContent';
import RolesSelect from 'components/RolesSelect';
@@ -28,7 +28,7 @@ interface OverviewTabProps {
localRoles: string[];
onRolesChange: (v: string[]) => void;
isDisabled: boolean;
availableRoles: AuthtypesRoleDTO[];
availableRoles: AuthtypesGettableRoleDTO[];
rolesLoading?: boolean;
rolesError?: boolean;
rolesErrorObj?: APIError | undefined;

View File

@@ -0,0 +1,11 @@
.static {
// Tag chips are not interactive, but the @signozhq Badge darkens outline
// variants on hover — which reads as clickable. Pin the hover background to the
// resting background so hovering a plain tag produces no change.
--badge-outline-hover-background-color: var(
--badge-outline-background-color,
color-mix(in oklab, var(--badge-background) 10%, transparent)
);
cursor: default;
}

View File

@@ -1,5 +1,8 @@
import { type MouseEvent, type ReactNode } from 'react';
import { Badge } from '@signozhq/ui/badge';
import cx from 'classnames';
import styles from './TagBadge.module.scss';
interface TagBadgeProps {
children: ReactNode;
@@ -22,7 +25,7 @@ function TagBadge({
<Badge
color="sienna"
variant="outline"
className={className}
className={cx(styles.static, className)}
closable={closable}
onClose={onClose}
>

View File

@@ -0,0 +1,112 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TagKeyValueInput from './TagKeyValueInput';
const TID = 'tag-key-value-input';
type User = ReturnType<typeof userEvent.setup>;
const startEditingFirstChip = async (user: User): Promise<HTMLElement> => {
await user.dblClick(screen.getAllByTestId(`${TID}-chip`)[0]);
return screen.getByTestId(`${TID}-edit`);
};
describe('TagKeyValueInput — inline chip edit', () => {
it('shows an error and stays in edit mode when Enter commits an invalid value', async () => {
const user = userEvent.setup();
const onTagsChange = jest.fn();
render(<TagKeyValueInput tags={['env:prod']} onTagsChange={onTagsChange} />);
const input = await startEditingFirstChip(user);
await user.clear(input);
await user.type(input, 'novalue{Enter}');
expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent(
'key:value format',
);
// Still editing (input present), and no change committed.
expect(screen.getByTestId(`${TID}-edit`)).toBeInTheDocument();
expect(onTagsChange).not.toHaveBeenCalled();
});
it('shows a duplicate error when Enter commits an existing tag', async () => {
const user = userEvent.setup();
const onTagsChange = jest.fn();
render(
<TagKeyValueInput
tags={['env:prod', 'team:pulse']}
onTagsChange={onTagsChange}
/>,
);
const input = await startEditingFirstChip(user);
await user.clear(input);
await user.type(input, 'team:pulse{Enter}');
expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent(
'already exists',
);
expect(onTagsChange).not.toHaveBeenCalled();
});
it('commits a valid edit on Enter', async () => {
const user = userEvent.setup();
const onTagsChange = jest.fn();
render(<TagKeyValueInput tags={['env:prod']} onTagsChange={onTagsChange} />);
const input = await startEditingFirstChip(user);
await user.clear(input);
await user.type(input, 'env:staging{Enter}');
expect(onTagsChange).toHaveBeenCalledWith(['env:staging']);
expect(screen.queryByTestId(`${TID}-error`)).not.toBeInTheDocument();
});
it('reverts silently (no error) when blurring an invalid edit', async () => {
const user = userEvent.setup();
const onTagsChange = jest.fn();
render(<TagKeyValueInput tags={['env:prod']} onTagsChange={onTagsChange} />);
const input = await startEditingFirstChip(user);
await user.clear(input);
await user.type(input, 'novalue');
await user.tab(); // blur off the edit input
expect(screen.queryByTestId(`${TID}-error`)).not.toBeInTheDocument();
expect(screen.queryByTestId(`${TID}-edit`)).not.toBeInTheDocument();
expect(onTagsChange).not.toHaveBeenCalled();
});
});
describe('TagKeyValueInput — backend-rule validation', () => {
it('rejects a key containing a space on inline edit', async () => {
const user = userEvent.setup();
const onTagsChange = jest.fn();
render(<TagKeyValueInput tags={['env:prod']} onTagsChange={onTagsChange} />);
const input = await startEditingFirstChip(user);
await user.clear(input);
await user.type(input, 'my key:prod{Enter}');
expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent(
'spaces or special characters',
);
expect(screen.getByTestId(`${TID}-edit`)).toBeInTheDocument();
expect(onTagsChange).not.toHaveBeenCalled();
});
it('rejects a value containing a space in the new-tag input', async () => {
const user = userEvent.setup();
const onTagsChange = jest.fn();
render(<TagKeyValueInput tags={[]} onTagsChange={onTagsChange} />);
const input = screen.getByTestId(TID);
await user.type(input, 'env:pro d{Enter}');
expect(screen.getByTestId(`${TID}-error`)).toHaveTextContent(
'spaces or special characters',
);
expect(onTagsChange).not.toHaveBeenCalled();
});
});

View File

@@ -5,7 +5,7 @@ import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import TagBadge from '../TagBadge/TagBadge';
import { parseKeyValueTag } from './utils';
import { validateTag } from './utils';
import styles from './TagKeyValueInput.module.scss';
@@ -43,16 +43,12 @@ function TagKeyValueInput({
if (!raw) {
return;
}
const normalized = parseKeyValueTag(raw);
if (!normalized) {
setError('Tags must be in key:value format (both sides required).');
const result = validateTag(raw, tags);
if ('error' in result) {
setError(result.error);
return;
}
if (tags.includes(normalized)) {
setError('This tag already exists.');
return;
}
onTagsChange([...tags, normalized]);
onTagsChange([...tags, result.tag]);
setInputValue('');
setError('');
};
@@ -82,15 +78,20 @@ function TagKeyValueInput({
const cancelEdit = (): void => {
setEditIndex(-1);
setEditValue('');
setError('');
};
const commitEdit = (): void => {
const normalized = parseKeyValueTag(editValue);
// Drop into a no-op (revert) on invalid or duplicate edits rather than
// stranding the user in an un-exitable edit box.
if (normalized && !tags.some((t, i) => t === normalized && i !== editIndex)) {
onTagsChange(tags.map((t, i) => (i === editIndex ? normalized : t)));
const commitEdit = (revertOnInvalid = false): void => {
const result = validateTag(editValue, tags, editIndex);
if ('error' in result) {
if (revertOnInvalid) {
cancelEdit();
} else {
setError(result.error);
}
return;
}
onTagsChange(tags.map((t, i) => (i === editIndex ? result.tag : t)));
cancelEdit();
};
@@ -121,11 +122,14 @@ function TagKeyValueInput({
value={editValue}
autoFocus
testId={`${testId}-edit`}
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
setEditValue(e.target.value)
}
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
setEditValue(e.target.value);
if (error) {
setError('');
}
}}
onKeyDown={handleEditKeyDown}
onBlur={commitEdit}
onBlur={(): void => commitEdit(true)}
/>
) : (
<TagBadge

View File

@@ -15,3 +15,42 @@ export function parseKeyValueTag(raw: string): string | null {
}
return `${key}:${value}`;
}
const TAG_KEY_REGEX = new RegExp('^[a-zA-Z$_@{#][a-zA-Z0-9$_@#{}:/-]*$');
const TAG_VALUE_REGEX = new RegExp('^[a-zA-Z0-9$_@#{}:.+=/-]*$');
const MAX_TAG_LEN = 32;
export type TagValidation = { tag: string } | { error: string };
export function validateTag(
raw: string,
existingTags: string[],
excludeIndex = -1,
): TagValidation {
const normalized = parseKeyValueTag(raw);
if (!normalized) {
return { error: 'Tags must be in key:value format (both sides required).' };
}
const separator = normalized.indexOf(':');
const key = normalized.slice(0, separator);
const value = normalized.slice(separator + 1);
if (!TAG_KEY_REGEX.test(key)) {
return { error: 'Tag keys cannot contain spaces or special characters.' };
}
if (!TAG_VALUE_REGEX.test(value)) {
return { error: 'Tag values cannot contain spaces or special characters.' };
}
if (key.length > MAX_TAG_LEN || value.length > MAX_TAG_LEN) {
return {
error: `Tag key and value must each be ${MAX_TAG_LEN} characters or fewer.`,
};
}
if (
existingTags.some(
(tag, index) => tag === normalized && index !== excludeIndex,
)
) {
return { error: 'This tag already exists.' };
}
return { tag: normalized };
}

View File

@@ -1,7 +1,11 @@
import { ComponentProps, memo } from 'react';
import { ComponentProps, memo, useCallback, useLayoutEffect } from 'react';
import { TableComponents } from 'react-virtuoso';
import cx from 'classnames';
import {
chromePerformanceTanstackTableBeginHover,
chromePerformanceMeasureTanstackTable,
} from './perfDevtools';
import TanStackRowCells from './TanStackRow';
import {
useClearRowHovered,
@@ -10,18 +14,20 @@ import {
import { FlatItem, TableRowContext } from './types';
import tableStyles from './TanStackTable.module.scss';
import { chromePerformanceNow } from 'lib/chromePerformanceDevTools';
type VirtuosoTableRowProps<TData> = ComponentProps<
type VirtuosoTableRowProps<TData, TItemKey = string> = ComponentProps<
NonNullable<
TableComponents<FlatItem<TData>, TableRowContext<TData>>['TableRow']
TableComponents<FlatItem<TData>, TableRowContext<TData, TItemKey>>['TableRow']
>
>;
function TanStackCustomTableRow<TData>({
function TanStackCustomTableRow<TData, TItemKey = string>({
item,
context,
...props
}: VirtuosoTableRowProps<TData>): JSX.Element {
}: VirtuosoTableRowProps<TData, TItemKey>): JSX.Element {
const renderStart = chromePerformanceNow();
const rowId = item.row.id;
const rowData = item.row.original;
@@ -29,6 +35,23 @@ function TanStackCustomTableRow<TData>({
const setHovered = useSetRowHovered(rowId);
const clearHovered = useClearRowHovered(rowId);
const handleMouseEnter = useCallback((): void => {
chromePerformanceTanstackTableBeginHover(rowId);
setHovered();
}, [rowId, setHovered]);
useLayoutEffect(() => {
chromePerformanceMeasureTanstackTable('Row render', renderStart, {
track: 'Row render',
color: 'secondary',
tooltipText: 'Row render + commit',
properties: [
['rowId', rowId],
['kind', item.kind],
],
});
});
if (item.kind === 'expansion') {
return (
<tr {...props} className={tableStyles.tableRowExpansion}>
@@ -65,7 +88,7 @@ function TanStackCustomTableRow<TData>({
{...props}
className={rowClassName}
style={rowStyle}
onMouseEnter={setHovered}
onMouseEnter={handleMouseEnter}
onMouseLeave={clearHovered}
>
<TanStackRowCells
@@ -84,9 +107,9 @@ function TanStackCustomTableRow<TData>({
// This looks overkill but ensures the table is stable and doesn't re-render on every change
// If you add any new prop to context, remember to update this function
// eslint-disable-next-line sonarjs/cognitive-complexity
function areTableRowPropsEqual<TData>(
prev: Readonly<VirtuosoTableRowProps<TData>>,
next: Readonly<VirtuosoTableRowProps<TData>>,
function areTableRowPropsEqual<TData, TItemKey = string>(
prev: Readonly<VirtuosoTableRowProps<TData, TItemKey>>,
next: Readonly<VirtuosoTableRowProps<TData, TItemKey>>,
): boolean {
if (prev.item.row.id !== next.item.row.id) {
return false;
@@ -141,7 +164,9 @@ function areTableRowPropsEqual<TData>(
return true;
}
export default memo(
TanStackCustomTableRow,
areTableRowPropsEqual,
) as typeof TanStackCustomTableRow;
export default memo(TanStackCustomTableRow, areTableRowPropsEqual as any) as <
TData,
TItemKey = string,
>(
props: VirtuosoTableRowProps<TData, TItemKey>,
) => JSX.Element;

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