Compare commits

..

2 Commits

Author SHA1 Message Date
Aditya Singh
abf60c0af3 feat: move out of using monaco cdn to using monaco from node modules (#12515)
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
cacheci / tests (push) Waiting to run
Release Drafter / update_release_draft (push) Waiting to run
<!--A few plain bullets saying what changed and why, for a reviewer
skimming it - not a wall of text, not a restatement of the diff, not
generated boilerplate.-->
#### Description
This PR prevents `@monaco-editor/react` from loading the core
monaco-editor package from a third party CDN. This fixes an issue where
the CDN is blocked for certain users/tenants, preventing monaco-editor
from loading.

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR
closes https://github.com/SigNoz/engineering-pod/issues/5871

<!--If applicable, include screenshots or screen recordings that clearly
show the behavior before the change and the result after the change. -->
#### Screenshots / Screen Recordings

<!--Anything reviewers should keep in mind while reviewing -->
#### Additional Information
Pager: https://signoz-1.pagerduty.com/incidents/Q108N2EUVQAJN2
Sentry: https://signoz-io.sentry.io/issues/7583880805/

<!--Please delete paragraphs that you did not use before submitting.-->
2026-08-14 06:04:17 +00:00
Abhi kumar
ebc8d86a8d feat(dashboard-v2): format panel values with thousand separators (#12327)
## Pull Request

---

### 📄 Summary

Large numbers in dashboard panels rendered as an undelimited digit run —
`1234567` instead of `1,234,567` — which is hard to read at a glance and
hard to compare between panels.

Unitless values were the visible gap. They format through the `'none'`
unit, which routes to `formatDecimalWithLeadingZeros`, whose
`Intl.NumberFormat` is constructed with `useGrouping: false`. Units that
scale their own value (`bytes` → `1.18 MiB`, `short` → `1.23 Mil`) never
reach four integer digits, so they never showed the problem.

The grouping is applied inside `formatPanelValue` — the single seam
through which V2 panels reach `getYAxisFormattedValue` — rather than at
one call site. That is deliberate: readability of a large scalar is not
specific to one panel kind, so the Number panel, Table value cells and
the threshold-row previews all pick it up from one place instead of each
opting in.

`groupThousands` itself is conservative. It touches only the first
numeric token's integer digits, so fractions, unit labels and
formatter-scaled values pass through untouched, and exponent notation is
skipped (grouping a mantissa reads as noise).

#### Screenshots / Screen Recordings (if applicable)

No capture attached. The visible delta is purely the separators:

| Panel | Unit | Before | After |
|---|---|---|---|
| Number | — | `1234567` | `1,234,567` |
| Number | `percent` | `1234567%` | `1,234,567%` |
| Number | `bytes` | `1.18 MiB` | `1.18 MiB` (unchanged) |
| Table cell | — | `1234567` | `1,234,567` |

#### Issues closed by this PR

Closes #7669

---

###  Change Type
_Select all that apply_

- [x]  Feature
- [ ] 🐛 Bug fix
- [ ] ♻️ Refactor
- [ ] 🛠️ Infra / Tooling
- [ ] 🧪 Test-only

---

### 🐛 Bug Context

**N/A** — this is an enhancement, not a regression. Grouping was never
implemented; `useGrouping: false` in `formatDecimalWithLeadingZeros` is
longstanding and intentional for the axis-tick path it was written for.

---

### 🧪 Testing Strategy

- **Tests added/updated:**
  - `groupThousands.test.ts` (new) — the transform in isolation.
- `parseFormattedValue.test.ts` (new) — this util had no suite; covers
the unit split, including grouped input.
- `formatPanelValue.test.ts` — asserts grouping at the seam, and that
unit-scaled values stay ungrouped.
- `NumberPanel/__tests__/Renderer.test.tsx` — grouped value, grouped
value + separate unit, and unit-scaled value left alone.
- `TablePanel/__tests__/Renderer.test.tsx` — pins the grouping that
Table cells inherit. Worth noting for reviewers: `tableColumns.test.ts`
and `tableCsv.test.ts` both stub `formatPanelValue`, so they are blind
to this change by construction — the renderer test is what actually
covers the Table path.

- **Manual verification:** not a click-through of a live dashboard.
Instead: the full frontend jest suite (7,312 passing; the 3 failures are
in unrelated suites — `QuerySearch`, `AuthDomain` — and were confirmed
flaky/pre-existing by re-running them alone and against a stashed
pristine tree), plus `tsgo --noEmit`, `oxlint`, `oxfmt --check` and
`vite build` all clean. The real `getYAxisFormattedValue` output was
probed directly for 11 value/unit combinations before writing the
transform, and `papaparse.unparse` was run directly to confirm exactly
how a grouped cell serializes.

- **Edge cases covered:** negative values (sign stays outside the first
group), fractions (never grouped), exponent notation (skipped), `∞` /
`-∞` / `NaN` (untouched), prefix and suffix unit decoration (`$
1,234,567`, `1,234,567%`, `1,234,567 ms`), formatter-scaled units,
values below 1000, zero, and idempotency on already-grouped input.

---

### ⚠️ Risk & Impact Assessment

- **Blast radius:** every `formatPanelValue` consumer — the Number
panel, Table panel value cells, the three threshold-row previews in the
config pane, and the Table CSV export. Display-only in all cases; no
spec/DTO or API change, nothing persisted.

- **Potential regressions:**
- **The CSV export is the one behavior change worth a reviewer's
attention.** `formatTableCellText` is shared by the Table renderer and
the export, so a unitless numeric column now serializes as `"1,234,567"`
(papaparse quotes any field containing the delimiter) instead of
`1234567` — spreadsheet `SUM`/`AVG` and downstream `parseFloat` would
read it as text. Columns with a unit were *already* display-text in the
export (`295.43 ms`, `1.18 MiB`) by design ("reusing the on-screen cell
formatting", V1 parity), so only unitless numeric columns change in
kind. Called out explicitly because it is an accepted trade-off, not an
oversight — if we would rather keep the export numeric, the contained
fix is a flag threaded through `formatTableCellText` from `tableCsv.ts`
only, leaving the render path grouped.
- Table **sorting and threshold evaluation are unaffected** — both read
`toCellNumber(raw)`, never the formatted string.
- `parseFormattedValue` had to learn to accept `,`, otherwise a grouped
value would fall through to the whole-string fallback and lose its unit
split. Covered by its new suite.

- **Rollback plan:** revert the PR. Display-only with no migration or
persisted state, so a revert is immediate and total.

---

### 📝 Changelog

| Field | Value |
|------|-------|
| Deployment Type | Cloud / OSS / Enterprise |
| Change Type | Feature |
| Description | Large numbers in dashboard panels are now formatted with
thousand separators (`1,234,567`), in the Number panel, Table panel
value cells and threshold labels. |

---

### 📋 Checklist
- [x] Tests added or explicitly not required
- [ ] Manually tested
- [x] Breaking changes documented
- [x] Backward compatibility considered

---

## 👀 Notes for Reviewers

Two things I would want a second pair of eyes on:

1. **"Manually tested" is deliberately unchecked.** This was validated
through tests and direct probes of the real formatter, not by clicking
through a running dashboard. A quick look at a Number panel and a Table
panel — plus a screenshot for this PR — is worth doing before merge.
2. **The CSV trade-off** under Risk & Impact. Grouping at the seam is
what makes the change one line instead of four call sites, but the
export rides the same path. The narrower alternative is described there
if you would rather not accept it.

The three commits are independently reviewable: the transform, the
parser tolerance it requires, then the seam that turns it on.
2026-08-14 06:00:09 +00:00
15 changed files with 240 additions and 153 deletions

View File

@@ -88,6 +88,7 @@
"jest": "30.2.0",
"js-base64": "^3.7.2",
"lodash-es": "^4.17.21",
"monaco-editor": "0.55.1",
"motion": "12.4.13",
"nuqs": "2.8.8",
"overlayscrollbars": "^2.16.0",
@@ -237,4 +238,4 @@
"tmp@<0.2.6": ">=0.2.6 <0.3.0",
"yaml@>=1.0.0 <1.10.3": ">=1.10.3 <2"
}
}
}

View File

@@ -180,6 +180,9 @@ importers:
lodash-es:
specifier: ^4.17.21
version: 4.18.1
monaco-editor:
specifier: 0.55.1
version: 0.55.1
motion:
specifier: 12.4.13
version: 12.4.13(@emotion/is-prop-valid@1.2.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -1755,105 +1758,89 @@ packages:
resolution: {integrity: sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.3.0':
resolution: {integrity: sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.3.0':
resolution: {integrity: sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.3.0':
resolution: {integrity: sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.3.0':
resolution: {integrity: sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.3.0':
resolution: {integrity: sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.3.0':
resolution: {integrity: sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.3.0':
resolution: {integrity: sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.35.0':
resolution: {integrity: sha512-4+4XHLNT5wDT0roYlHTEmH9lDKt0acf9Tv+3hM3iceOirkxrR404/3WjAYZ9F9CkHrxeRcGLJXbi4vluMZ9O+A==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.35.0':
resolution: {integrity: sha512-VVlpEWwizEFIOom0zdoeKuO5nuTswzVE5uHcBNvHzmeHUpNFajY3HFfbQ+zIH4E2kVaZ/yVxmsShW56TtEy4uA==}
engines: {node: '>=20.9.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.35.0':
resolution: {integrity: sha512-N3hzbEpUTJC8pWpPVJvgzGxM+so/MAXc8O2s/53B0LL9ZGpfXpME7Wizkc5d/8fRBlBtkDjzoZGDCqqNDHqLEw==}
engines: {node: '>=20.9.0'}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.35.0':
resolution: {integrity: sha512-l6vmKVPnbS0RhVMbyxP5meAARsbhCnBN4fy31qz0+3a6Rv4jEqfzDrT89y6ZPkCi0AJGnwp2En528yXo401Hpw==}
engines: {node: '>=20.9.0'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.35.0':
resolution: {integrity: sha512-MYlMiPFiv/EKPAHnp3yNZ9AAWFsxga9c5Bkc6wkar6bqzHLlkGVJHRm0u1ei+VXnZxp3Mz9MG9ZIsI8vSOf3sQ==}
engines: {node: '>=20.9.0'}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.35.0':
resolution: {integrity: sha512-TYaItB5oj1ioXjhyn2xrR208vf+YuIIcHptQWRRaBmFhvIvL9D72DXN8w75xup0KXA8UdEAhQ9Qb2S49FD/9Cw==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.35.0':
resolution: {integrity: sha512-DSTb6ijQzqe6DdAaOBVqJ/SYf1vO8EW5bK6X6LRXufEBebf2722VCdvBUtZ3rtV0x2ApfPNDy/p7LrrjaWjiyQ==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.35.0':
resolution: {integrity: sha512-K7ykQ+26Rt6+4BTU80AuGgTPIYX86UxiAKT4rcXX/WNTo7k1ZxpKz+TguHnwVpCqQK3B5PK0vZ0ZBe6nz/ib1w==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.35.0':
resolution: {integrity: sha512-9woLIFORERCr+6cWu87dQ22J34EExkhc73U1kZW0c+RclQqWetoodByp4dWZ/hN8/KVmTRAx2HOnUwib8AwZdA==}
@@ -2234,56 +2221,48 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-arm64-musl@0.54.0':
resolution: {integrity: sha512-QrwJlBFFKnxOd95TAaszpMbZBLzMoYMpGaQTZF8oibacnF5rv8l12IhILhQRPmksWiBqg0YSe2Mnl7ayeJAHSA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxfmt/binding-linux-ppc64-gnu@0.54.0':
resolution: {integrity: sha512-WILatiol/TUHTlhod7R09+7Az/XlhKwmY1MHfLZNmewltPWNN/EwxP2rQSHahibZ/cB8gmckEBjBOByD+5bYsQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-riscv64-gnu@0.54.0':
resolution: {integrity: sha512-f05YMG4BH4G8S4ME6UM6fi1MnJ9094mrnvO5Pa4SJlMfWlUM+1/ZWMEF4NnjM7shZAvbHsHRuVYpUo0PHC4P9Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-riscv64-musl@0.54.0':
resolution: {integrity: sha512-UfL+2hj1ClNqcCRT9s8vBU4axDpjxgVxX96G+9DYAYjoc5b0u15CJtn2jgsi9iM+EbGNc5CW1HVRgwVu76UsSA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxfmt/binding-linux-s390x-gnu@0.54.0':
resolution: {integrity: sha512-3/XZe931Hka+J6NjnaqJzYpsWWxDTuRdUdwSQHnOuJEgbC+SehIMFJS8hsEjV7LBhVSL2OCnRLvbVW8O97XIyw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-x64-gnu@0.54.0':
resolution: {integrity: sha512-Ik93RlObtu43GbxApafayFjwYE06L6Xr08cSwpBPYbDrLp2ReZx0Jm1DqwRyYRnukUJy+rK2WaEvUQOxdytU9Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-x64-musl@0.54.0':
resolution: {integrity: sha512-yZcakmPlD86CNymknd7KfW+FH+qfbqJH+i0h69CYfV1+KMoVeM9UED+8+TDVoU4haxI0NxY7RPCvRLy3Sqd2Qg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxfmt/binding-openharmony-arm64@0.54.0':
resolution: {integrity: sha512-GiVBZNnEZnKu00f1jTg49nomv187d0GQX+O+ocykoLeiaALuEO+swoTehHn9TehTfi7V8H0i0e/yvUjCqnwk1w==}
@@ -2386,56 +2365,48 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-arm64-musl@1.69.0':
resolution: {integrity: sha512-xEPpNppTfN1l/nM7gYSf9iocscu/as+p/7vxkLeLEKnYU+09Dm+5V6IhDYDh+Uz6FajEupWwCLt5SOG0y1PCKg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-ppc64-gnu@1.69.0':
resolution: {integrity: sha512-Ug0+eU7HJBlek+SjklYH62IlOMirEJsdxpihH0kSqX0XdrDD4NdHpQc10fK1JC35yn6KrrcN+uYzlHD38XAf8Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-gnu@1.69.0':
resolution: {integrity: sha512-iEyI3GIg0l/s3G4qy2TlaaWKdzj4PJJStwtlocpDTC00PY9hZueotf6OKUj9+yfQh0lrpBW/pLMgTztbAHKJEg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-musl@1.69.0':
resolution: {integrity: sha512-NjHjpiI4WIKSMwuoJSZi5VToPeoYOS1FR52HLIDG6lidMdqquusgtODb4iLk0+lb1q3Z0nv2/aPRcC/olmpQGg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-s390x-gnu@1.69.0':
resolution: {integrity: sha512-Ai/prDewoItkDXbp38gwGZi41DycZbUTZJ3UidwoHgQC0/DaqC2TGdtBTQLJ6hSD+SAxASzh8+/eSBPmxfOacA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-gnu@1.69.0':
resolution: {integrity: sha512-Gt3KHgp46mRKz4sJeaASmKvD8ayXookRw07RMf+NowhEztGGDZ7VrXpoW96XuKJLjFukWizOFVNjmYb/u7caNQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-musl@1.69.0':
resolution: {integrity: sha512-7tQhJ2+p/oHv1zcfnjYI7YVzC/7iBaVOfIvFYtxdJ5F45mWgEdrCyXZXZGfiLey5t/5JhOhsaMnnv1kAzckd7g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxlint/binding-openharmony-arm64@1.69.0':
resolution: {integrity: sha512-vmWz6TKp/3hfA4lksR0zHBv/6xuX1jhym6eqOjdH2DXsDDHZWcp2f0KG0VCAnlVbIrjk29G4wAWMXb/Hn1YobA==}
@@ -2490,42 +2461,36 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.1':
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.1':
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.1':
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.1':
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.1':
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.1':
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
@@ -3142,28 +3107,24 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.53':
resolution: {integrity: sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.53':
resolution: {integrity: sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.0-beta.53':
resolution: {integrity: sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.0-beta.53':
resolution: {integrity: sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A==}
@@ -3824,49 +3785,41 @@ packages:
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
@@ -6275,28 +6228,24 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.31.1:
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.31.1:
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.31.1:
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.31.1:
resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==}

View File

@@ -2,17 +2,14 @@ import { useTranslation } from 'react-i18next';
import { Input } from '@signozhq/ui/input';
import { Switch } from '@signozhq/ui/switch';
import { Form, Select, Space } from 'antd';
import { FeatureKeys } from 'constants/features';
import { ModalFooterTitle } from 'container/PipelinePage/styles';
import { useAppContext } from 'providers/App/App';
import { ProcessorData } from 'types/api/pipeline/def';
import { formValidationRules } from '../config';
import { ProcessorFormField } from './config';
import { processorFields, ProcessorFormField } from './config';
import CSVInput from './FormFields/CSVInput';
import JsonFlattening from './FormFields/JsonFlattening';
import { FormWrapper, PipelineIndexIcon, StyledSelect } from './styles';
import { resolveProcessorFields } from './utils';
import './styles.scss';
@@ -136,23 +133,16 @@ function ProcessorForm({
selectedProcessorData,
isAdd,
}: ProcessorFormProps): JSX.Element {
const { featureFlags } = useAppContext();
const isBodyJsonEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_JSON_BODY)
?.active || false;
return (
<div className="processor-form-container">
{resolveProcessorFields(processorType, isBodyJsonEnabled).map(
(fieldData: ProcessorFormField) => (
<ProcessorFieldInput
key={fieldData.name + String(fieldData.initialValue)}
fieldData={fieldData}
selectedProcessorData={selectedProcessorData}
isAdd={isAdd}
/>
),
)}
{processorFields[processorType]?.map((fieldData: ProcessorFormField) => (
<ProcessorFieldInput
key={fieldData.name + String(fieldData.initialValue)}
fieldData={fieldData}
selectedProcessorData={selectedProcessorData}
isAdd={isAdd}
/>
))}
</div>
);
}

View File

@@ -1,24 +0,0 @@
import { processorFields, ProcessorFormField } from './config';
const BODY_PARSE_FROM = 'body';
const JSON_BODY_PARSE_FROM = 'body.message';
// With use_json_body the collector normalizes every body into a map before user
// operators run, so a parser pointed at `body` gets a map it cannot read and
// silently extracts nothing. The log text lives at body.message.
export function resolveProcessorFields(
processorType: string,
isBodyJsonEnabled: boolean,
): Array<ProcessorFormField> {
const fields = processorFields[processorType] ?? [];
if (!isBodyJsonEnabled) {
return fields;
}
return fields.map((field) =>
field.name === 'parse_from' && field.initialValue === BODY_PARSE_FROM
? { ...field, initialValue: JSON_BODY_PARSE_FROM }
: field,
);
}

View File

@@ -1,45 +0,0 @@
import { processorFields } from '../PipelineListsView/AddNewProcessor/config';
import { resolveProcessorFields } from '../PipelineListsView/AddNewProcessor/utils';
const parseFromDefault = (
fields: ReturnType<typeof resolveProcessorFields>,
): unknown => fields.find((field) => field.name === 'parse_from')?.initialValue;
describe('resolveProcessorFields', () => {
it.each(['grok_parser', 'regex_parser', 'json_parser'])(
'defaults %s parse_from to body.message when use_json_body is on',
(processorType) => {
expect(parseFromDefault(resolveProcessorFields(processorType, true))).toBe(
'body.message',
);
},
);
it.each(['grok_parser', 'regex_parser', 'json_parser'])(
'keeps %s parse_from as body when use_json_body is off',
(processorType) => {
expect(parseFromDefault(resolveProcessorFields(processorType, false))).toBe(
'body',
);
},
);
it('leaves parse_from defaults that do not point at the body alone', () => {
expect(parseFromDefault(resolveProcessorFields('time_parser', true))).toBe(
'attributes.timestamp',
);
expect(
parseFromDefault(resolveProcessorFields('severity_parser', true)),
).toBe('attributes.logLevel');
});
it('does not mutate the shared config', () => {
resolveProcessorFields('grok_parser', true);
expect(parseFromDefault(processorFields.grok_parser)).toBe('body');
});
it('returns an empty list for an unknown processor type', () => {
expect(resolveProcessorFields('does_not_exist', true)).toStrictEqual([]);
});
});

View File

@@ -15,6 +15,8 @@ import store from 'store';
import APIError from 'types/api/error';
import { installTranslationResilience } from 'translation-resilience';
import 'lib/monaco/setup';
import './ReactI18';
import 'styles.scss';

View File

@@ -0,0 +1,21 @@
import { loader } from '@monaco-editor/react';
import * as monaco from 'monaco-editor';
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
// Serve Monaco's workers from our own origin instead of letting @monaco-editor/loader
// fetch them from cdn.jsdelivr.net at runtime. The CDN default breaks the editor in
// air-gapped/on-prem installs, CDN-blocking corporate networks, and regions where
// jsdelivr is unreachable. Ref: engineering-pod#5871, SIGNOZ-UI-5G0.
self.MonacoEnvironment = {
getWorker(_workerId: string, label: string): Worker {
if (label === 'json') {
return new JsonWorker(); // JSON language service
}
// SigNoz editors use JSON + a hand-registered ClickHouse tokenizer (no worker),
// so the base editor worker covers everything else.
return new EditorWorker(); // base worker — sql, yaml, plaintext etc.
},
};
loader.config({ monaco });

View File

@@ -125,6 +125,37 @@ describe('NumberPanelRenderer', () => {
expect(queryByText('3.14159')).not.toBeInTheDocument();
});
// #7669: large scalars are unreadable as an undelimited digit run.
it('groups large values into thousands', () => {
const { getByText, queryByText } = renderPanel({
panel: panelWith({}),
data: dataWith('1234567'),
});
expect(getByText('1,234,567')).toBeInTheDocument();
expect(queryByText('1234567')).not.toBeInTheDocument();
});
it('groups the value while keeping its unit separate', () => {
const { getByText } = renderPanel({
panel: panelWith({ formatting: { unit: 'percent' } }),
data: dataWith('1234567'),
});
expect(getByText('1,234,567')).toBeInTheDocument();
expect(getByText('%')).toBeInTheDocument();
});
it('leaves a unit-scaled value ungrouped', () => {
const { getByText } = renderPanel({
panel: panelWith({ formatting: { unit: 'bytes' } }),
data: dataWith('1234567'),
});
expect(getByText('1.18')).toBeInTheDocument();
expect(getByText('MiB')).toBeInTheDocument();
});
it('renders No Data when the response has no scalar results', () => {
const { getByTestId } = renderPanel({ data: emptyData });

View File

@@ -93,6 +93,15 @@ describe('TablePanelRenderer', () => {
expect(getByText('cartservice')).toBeInTheDocument();
});
// Value cells share `formatPanelValue`, so they group like the Number panel.
it('groups large value cells into thousands', () => {
const { getByText } = renderPanel({
data: dataWith([['frontend', 1234567]]),
});
expect(getByText('1,234,567')).toBeInTheDocument();
});
it('renders No Data when the response has no scalar results', () => {
const { getByTestId } = renderPanel({ data: emptyData });

View File

@@ -30,4 +30,14 @@ describe('formatPanelValue', () => {
it('renders whole numbers without a trailing decimal', () => {
expect(formatPanelValue(5, undefined, 2)).toBe('5');
});
it('groups the integer part into thousands', () => {
expect(formatPanelValue(1234567, undefined, 2)).toBe('1,234,567');
expect(formatPanelValue(1234567, 'percent', 2)).toBe('1,234,567%');
expect(formatPanelValue(1234567.891, undefined, 2)).toBe('1,234,567.89');
});
it('leaves unit-scaled values ungrouped', () => {
expect(formatPanelValue(1234567, 'bytes', 2)).toBe('1.18 MiB');
});
});

View File

@@ -0,0 +1,55 @@
import { groupThousands } from '../groupThousands';
describe('groupThousands', () => {
it('groups the integer digits of a plain number', () => {
expect(groupThousands('1234567')).toBe('1,234,567');
expect(groupThousands('1000')).toBe('1,000');
expect(groupThousands('1000000000000000000000')).toBe(
'1,000,000,000,000,000,000,000',
);
});
it('leaves values below a thousand alone', () => {
expect(groupThousands('0')).toBe('0');
expect(groupThousands('999')).toBe('999');
expect(groupThousands('295.43')).toBe('295.43');
});
it('groups only the integer part', () => {
expect(groupThousands('1234567.891')).toBe('1,234,567.891');
expect(groupThousands('1234.0001234')).toBe('1,234.0001234');
});
it('keeps the sign outside the first group', () => {
expect(groupThousands('-1234567')).toBe('-1,234,567');
expect(groupThousands('-1234567.891')).toBe('-1,234,567.891');
});
it('preserves suffix and prefix unit decoration', () => {
expect(groupThousands('1234567 ms')).toBe('1,234,567 ms');
expect(groupThousands('1234567%')).toBe('1,234,567%');
expect(groupThousands('$ 1234567')).toBe('$ 1,234,567');
});
it('leaves formatter-scaled values untouched', () => {
expect(groupThousands('1.18 MiB')).toBe('1.18 MiB');
expect(groupThousands('1.23 Mil')).toBe('1.23 Mil');
expect(groupThousands('20.58 mins')).toBe('20.58 mins');
});
it('leaves exponent notation untouched', () => {
expect(groupThousands('1.234567e+21')).toBe('1.234567e+21');
expect(groupThousands('1234567e-8')).toBe('1234567e-8');
});
it('returns non-numeric output unchanged', () => {
expect(groupThousands('∞')).toBe('∞');
expect(groupThousands('-∞')).toBe('-∞');
expect(groupThousands('NaN')).toBe('NaN');
expect(groupThousands('')).toBe('');
});
it('is idempotent on already-grouped input', () => {
expect(groupThousands(groupThousands('1234567'))).toBe('1,234,567');
});
});

View File

@@ -0,0 +1,60 @@
import { parseFormattedValue } from '../parseFormattedValue';
describe('parseFormattedValue', () => {
it('splits a trailing unit label off the numeric core', () => {
expect(parseFormattedValue('295.43 ms')).toStrictEqual({
numericValue: '295.43',
prefixUnit: '',
suffixUnit: 'ms',
});
});
it('splits a leading currency symbol off the numeric core', () => {
expect(parseFormattedValue('$ 1.2K')).toStrictEqual({
numericValue: '1.2K',
prefixUnit: '$',
suffixUnit: '',
});
});
// Regression: the numeric core used to reject `,`, so a grouped value fell
// through to the whole-string fallback and lost its unit split.
it('keeps the unit split for grouped values', () => {
expect(parseFormattedValue('1,234,567 ms')).toStrictEqual({
numericValue: '1,234,567',
prefixUnit: '',
suffixUnit: 'ms',
});
expect(parseFormattedValue('1,234,567%')).toStrictEqual({
numericValue: '1,234,567',
prefixUnit: '',
suffixUnit: '%',
});
expect(parseFormattedValue('$ 1,234,567.89')).toStrictEqual({
numericValue: '1,234,567.89',
prefixUnit: '$',
suffixUnit: '',
});
});
it('treats a unitless value as the numeric core', () => {
expect(parseFormattedValue('1,234,567')).toStrictEqual({
numericValue: '1,234,567',
prefixUnit: '',
suffixUnit: '',
});
});
it('falls back to the whole string when nothing matches', () => {
expect(parseFormattedValue('∞')).toStrictEqual({
numericValue: '∞',
prefixUnit: '',
suffixUnit: '',
});
expect(parseFormattedValue('NaN')).toStrictEqual({
numericValue: 'NaN',
prefixUnit: '',
suffixUnit: '',
});
});
});

View File

@@ -1,16 +1,21 @@
import type { PrecisionOption } from 'components/Graph/types';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { groupThousands } from './groupThousands';
/**
* Formats a scalar for display in a V2 panel, honoring decimal precision. The
* single seam through which panels touch `getYAxisFormattedValue`. Unitless
* values format through the `'none'` unit, which still respects precision — so
* precision isn't silently dropped when no unit is set.
* Formats a scalar for display in a V2 panel, honoring decimal precision and
* grouping the integer part into thousands. The single seam through which panels
* touch `getYAxisFormattedValue`. Unitless values format through the `'none'`
* unit, which still respects precision — so precision isn't silently dropped
* when no unit is set.
*/
export function formatPanelValue(
value: number,
unit?: string,
precision?: PrecisionOption,
): string {
return getYAxisFormattedValue(String(value), unit || 'none', precision);
return groupThousands(
getYAxisFormattedValue(String(value), unit || 'none', precision),
);
}

View File

@@ -0,0 +1,22 @@
const THOUSANDS_BOUNDARY = /(\d)(?=(?:\d{3})+$)/g;
/** Leading number of a formatted value; unit decoration falls outside the match. */
const NUMERIC_TOKEN = /-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/;
/**
* Inserts thousand separators into the integer part of an already-formatted value,
* so large scalars read as `1,234,567`. Fractions, unit labels and formatter-scaled
* values (`1.18 MiB`) are left as-is, as is exponent notation.
*/
export function groupThousands(formatted: string): string {
return formatted.replace(NUMERIC_TOKEN, (token) => {
if (token.includes('e') || token.includes('E')) {
return token;
}
const [integerPart, fraction] = token.split('.');
const grouped = integerPart.replace(THOUSANDS_BOUNDARY, '$1,');
return fraction === undefined ? grouped : `${grouped}.${fraction}`;
});
}

View File

@@ -1,5 +1,5 @@
export interface ParsedFormattedValue {
/** The numeric portion (e.g. "295.43", "1.2K"). */
/** The numeric portion (e.g. "295.43", "1,234,567", "1.2K"). */
numericValue: string;
/** A leading unit symbol such as a currency prefix, if any. */
prefixUnit: string;
@@ -8,13 +8,14 @@ export interface ParsedFormattedValue {
}
/**
* Splits a formatted value (e.g. "$ 1.2K", "295.43 ms") into its numeric core
* and prefix/suffix unit for independent styling. Non-matching input falls back
* to the whole string as the numeric value.
* Splits a formatted value (e.g. "$ 1.2K", "295.43 ms", "1,234,567") into its
* numeric core and prefix/suffix unit for independent styling. The core accepts
* thousand separators, so a grouped value keeps its unit split. Non-matching
* input falls back to the whole string as the numeric value.
*/
export function parseFormattedValue(value: string): ParsedFormattedValue {
const matches = value.match(
/^([^\d.]*)?([\d.]+(?:[eE][+-]?[\d]+)?[KMB]?)([^\d.]*)?$/,
/^([^\d.]*)?([\d.,]+(?:[eE][+-]?[\d]+)?[KMB]?)([^\d.]*)?$/,
);
return {