Compare commits

...

60 Commits

Author SHA1 Message Date
Vinícius Lourenço
5b68cc74a6 docs(storybook): add the visual diff skill 2026-09-08 18:17:57 -03:00
Vinícius Lourenço
38e4652dfb feat(storybook): add the story screenshot diff 2026-09-08 18:17:57 -03:00
Vinícius Lourenço
c5c577b731 feat(storybook): add the story screenshot sweep 2026-09-08 18:17:57 -03:00
Vinícius Lourenço
f9d8ae6e3f feat(storybook): snapshot every story in both themes on chromatic 2026-09-08 18:17:56 -03:00
Vinícius Lourenço
533402fc79 feat(storybook): park every animation once a story has settled 2026-09-08 18:17:56 -03:00
Vinícius Lourenço
3cf6abd554 refactor(app): own the provider tree in src/app 2026-09-08 18:17:15 -03:00
Vinícius Lourenço
274df85659 refactor(storybook): move page story files under stories/ 2026-09-08 18:17:15 -03:00
Vinícius Lourenço
c3cd27ceb7 chore(package): pin the version for testing lib 2026-09-08 18:17:15 -03:00
Vinícius Lourenço
3526f7b6ae chore(app-shell): from 0.99.0 to 0.0.0 2026-09-08 18:17:14 -03:00
Vinícius Lourenço
bc9481ab99 docs(harness): correct which runners mount AppHarness 2026-09-08 18:17:14 -03:00
Vinícius Lourenço
5f7dba50f0 feat(storybook): add the a11y addon 2026-09-08 18:17:14 -03:00
Vinícius Lourenço
038b14fe88 chore: collapse AI-owned story mocks in diffs 2026-09-08 18:17:14 -03:00
Vinícius Lourenço
3600d92f18 docs(storybook): mark story mocks as AI-owned 2026-09-08 18:17:14 -03:00
Vinícius Lourenço
f3a9f7a545 refactor(storybook): rename __mockdata__ to __story_mockdata__ 2026-09-08 18:17:14 -03:00
Vinícius Lourenço
6384be1985 feat(storybook): add story for home page 2026-09-08 18:17:13 -03:00
Vinícius Lourenço
0d1b34986d feat(storybook): add skill to create stories for pages 2026-09-08 18:17:13 -03:00
Vinícius Lourenço
e0bccb9358 feat(storybook): add initial support for storybook 2026-09-08 18:17:13 -03:00
Vikrant Gupta
e7c48e964f fix(billing): keep spacing and link styling consistent when permissions are denied (#12801)
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
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

- The denied usage callout on the billing page keeps the same 8px gap to
the blocks below it that the usage table has, so the layout reads the
same with or without `subscription:read`.
- "pay the bill" in the failed-payment banner is an inline link again,
with a visible disabled state when the user lacks permission.

#### Additional Information

Part of SigNoz/platform-pod#3091.
2026-09-08 18:34:39 +00:00
Pandey
fe8c9d8cf4 chore: remove query progress tracking (#12784)
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
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

- The frontend websocket client for query progress
(`api/common/getQueryStats.ts`) was deleted in #9183, so
`/api/v3/query_progress` and `/ws/query_progress` have had no consumers
since.
- Removes the `query_progress` tracker package, the
`GetQueryProgressUpdates` handler, the `RegisterWebSocketPaths` route
group, and the `websocket.Upgrader` on `APIHandler`.
- Removes the clickhouse reader hooks that reported progress
(`ReportQueryStartForProgressTracking`, `SubscribeToQueryProgress`, and
the `clickhouse.WithProgress` callback in `GetTimeSeriesResultV3`),
along with the untyped `"queryId"` context value they relied on.
- Removes the `X-SIGNOZ-QUERY-ID` header the logs explorer still sent,
plus the now-dead `Sec-WebSocket-Protocol` entries in the CORS allowlist
and the `identn` tokenizer defaults. Livetail streams over SSE, so no
websocket endpoint remains.

#### Additional Information

- `gorilla/websocket` moves from a direct to an indirect dependency
(still pulled in by opamp-go).
- Dropping `Sec-WebSocket-Protocol` from `identn.tokenizer.headers`
changes a default in `conf/example.yaml`. It is only a token-extraction
source for websocket handshakes, of which there are none left, but
calling it out since it is a config-surface change.
2026-09-08 10:56:58 +00:00
dependabot[bot]
2ad3057fa6 chore(deps): bump google.golang.org/grpc from 1.82.1 to 1.83.1 in /scripts/promqltestcorpus (#12758)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from
1.82.1 to 1.83.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/grpc/grpc-go/releases">google.golang.org/grpc's
releases</a>.</em></p>
<blockquote>
<h2>Release 1.83.1</h2>
<h1>Security</h1>
<ul>
<li>xds/rbac: Fix a bug where nested <code>Principal</code> or
<code>Permission</code> rules with <code>:scheme</code> or
<code>grpc-</code> prefixed header matchers were not rejected, which
could cause DENY rules to fail open. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9258">#9258</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/nvxbug"><code>@​nvxbug</code></a></li>
</ul>
</li>
<li>xds/rbac: Fix a bug where the <code>host</code> header matcher was
not being replaced with <code>:authority</code> in nested
<code>Principal</code> or <code>Permission</code> rules. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9258">#9258</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/nvxbug"><code>@​nvxbug</code></a></li>
</ul>
</li>
<li>xds/rbac: Fix a bug where a header matcher whose name was not
lowercase, such as <code>X-Role</code>, matched no header, which could
cause DENY rules to fail open. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9332">#9332</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/alimony"><code>@​alimony</code></a></li>
</ul>
</li>
<li>xds/rbac: Fix a bug where a <code>:scheme</code> or
<code>grpc-</code> prefixed header matcher was accepted when its name
was not lowercase. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9332">#9332</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/alimony"><code>@​alimony</code></a></li>
</ul>
</li>
<li>xds/rbac: Fix a bug where a <code>Host</code> header matcher was not
replaced with <code>:authority</code>. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9332">#9332</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/alimony"><code>@​alimony</code></a></li>
</ul>
</li>
</ul>
<h1>Performance</h1>
<ul>
<li>transport: Restrict memory overhead of buffering small data frames.
(<a
href="https://redirect.github.com/grpc/grpc-go/issues/9331">#9331</a>)</li>
</ul>
<h2>Release 1.83.0</h2>
<h1>Security</h1>
<ul>
<li>server: Stop reading from connections when flooded by HTTP/2 frames
to mitigate resource exhaustion. The default value for this limit is 100
frames, excluding DATA and HEADERS, and may be changed by setting
environment variable
<code>GRPC_GO_EXPERIMENTAL_CONTROL_BUFFER_THROTTLE_LIMIT</code>.</li>
<li>xds/rbac: Support <code>Metadata</code> and
<code>RequestedServerName</code> permissions matcher fields. If present
in a DENY rule, previously these would be ignored and fail-open.</li>
<li>xds/rbac: Fix panic when parsing unsupported fields in
<code>NotRule</code>/<code>NotId</code> permissions.</li>
<li>xds/rbac: Support the deprecated <code>source_ip</code> principal
identifier by treating it as equivalent to
<code>direct_remote_ip</code>.</li>
<li>xds: Fix panic when parsing route header matchers configured with
empty <code>exact_match</code>, <code>prefix_match</code>, or
<code>suffix_match</code> strings. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9223">#9223</a>)</li>
</ul>
<h1>New Features</h1>
<ul>
<li>xds/googlec2p: Enable DirectPath over Interconnect support for
on-premises clients via the <code>force-xds</code> target URI query
parameter. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9133">#9133</a>)</li>
<li>xds: Enable xDS configuration to control which fields get propagated
from ORCA backend metric reports to LRS load reports. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9145">#9145</a>)</li>
<li>authz: Add <code>OnPolicyUpdate</code> callback to
<code>FileWatcherOptions</code> to notify when an authz policy is loaded
or updated. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9142">#9142</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/hnefatl"><code>@​hnefatl</code></a></li>
</ul>
</li>
<li>xds: Add support for the GCP Authentication HTTP Filter, which
automatically fetches and attaches GCP Service Account Identity JWT
tokens to outgoing RPCs.
<ul>
<li>This feature can be enabled by setting environment variable
<code>GRPC_EXPERIMENTAL_XDS_GCP_AUTHENTICATION_FILTER=true</code>. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9119">#9119</a>)</li>
</ul>
</li>
<li>xds: Add support for xDS-based HTTP CONNECT proxies.
<ul>
<li>This feature can be enabled by setting environment variable
<code>GRPC_EXPERIMENTAL_XDS_HTTP_CONNECT=true</code>. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9151">#9151</a>)</li>
</ul>
</li>
<li>xds: Add support for <code>contains_match</code> in route header
matchers. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9223">#9223</a>)</li>
</ul>
<h1>Bug Fixes</h1>
<ul>
<li>credentials/alts: Fix panic when processing malformed frames by
validating that the message frame length exceeds the message type field
size. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9197">#9197</a>)</li>
<li>grpc: Fix compilation on Plan 9 targets (<code>GOOS=plan9</code>),
broken since v1.81.0. (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9255">#9255</a>)
<ul>
<li>Special Thanks: <a
href="https://github.com/Yusufihsangorgel"><code>@​Yusufihsangorgel</code></a></li>
</ul>
</li>
</ul>
<h2>Release 1.82.2</h2>
<h1>Security</h1>
<ul>
<li>server: Reject requests missing both <code>:authority</code> and
<code>Host</code> headers with HTTP 400 and status
<code>Internal</code>. (<a
href="https://redirect.github.com/grpc/grpc-go/pull/9365">grpc/grpc-go#9365</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="1550d9e0cd"><code>1550d9e</code></a>
Change version to 1.83.1 (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9336">#9336</a>)</li>
<li><a
href="ebba6f3f1b"><code>ebba6f3</code></a>
Cherry-pick <a
href="https://redirect.github.com/grpc/grpc-go/issues/9258">#9258</a>
and <a
href="https://redirect.github.com/grpc/grpc-go/issues/9332">#9332</a>
into v1.83.x (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9335">#9335</a>)</li>
<li><a
href="8cfeca0e1e"><code>8cfeca0</code></a>
Cherry-pick <a
href="https://redirect.github.com/grpc/grpc-go/issues/9331">#9331</a> to
v1.83.x (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9333">#9333</a>)</li>
<li><a
href="dec6951305"><code>dec6951</code></a>
Change version to 1.83.1-dev (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9229">#9229</a>)</li>
<li><a
href="4c226daff8"><code>4c226da</code></a>
Change version to 1.83.0 (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9228">#9228</a>)</li>
<li><a
href="c198988aa9"><code>c198988</code></a>
Cherrypick 9223 into v1.83.x (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9279">#9279</a>)</li>
<li><a
href="8ce3ebf24a"><code>8ce3ebf</code></a>
Cherrypick PR 9255 into v1.83.x (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9263">#9263</a>)</li>
<li><a
href="e39384978c"><code>e393849</code></a>
Cherry-pick recent changes from master (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9240">#9240</a>)</li>
<li><a
href="2a112a82f5"><code>2a112a8</code></a>
authz: add onPolicyUpdate callback to authz file watcher (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9142">#9142</a>)</li>
<li><a
href="1a80fca960"><code>1a80fca</code></a>
vet: adds a check to disallow usage of regex.Compile in xDS code (<a
href="https://redirect.github.com/grpc/grpc-go/issues/9216">#9216</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/grpc/grpc-go/compare/v1.82.1...v1.83.1">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-08 09:33:33 +00:00
Aditya Singh
9fc73fc0b1 feat(quick-filters): migrate keys + save/load to new field APIs (#12717)
#### Description

- quick filter keys now come from fields/keys for all pages (logs,
traces, exceptions, api monitoring, meter)..replaces the old v3
filter_suggestions and aggregate attribute_keys calls
- save/load moved to the new v2 /me/filters api that stores
TelemetryFieldKey (name, fieldContext, fieldDataType). removed the old
hand written v1 clients and types
- filters are now identified by name + fieldContext + fieldDataType so
same name fields with different context/datatype don't clash. kept this
contained to the quick filter settings
- values still use the old v3 path for now..that migration (checkboxv2 /
fields/values) is the next PR

#### Screen recording


https://github.com/user-attachments/assets/130c8054-9f7d-4d8d-b224-62da7ccaf41e



#### Issues closed by this PR

Part of https://github.com/SigNoz/engineering-pod/issues/5979
2026-09-08 08:41:06 +00:00
Vikrant Gupta
3931c7163f chore(licensing): remove the legacy billing endpoints (#12771)
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
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

- Removes the legacy `/api/v1/checkout`, `/api/v1/billing`, and
`/api/v1/portal` routes and the licensing API layer that served them,
now that billing goes through the subscription endpoints.

#### Issues closed by this PR

Closes SigNoz/platform-pod#3091
2026-09-07 18:58:03 +00:00
Vikrant Gupta
fdfbf77d2b feat(billing): gate billing on subscription permissions (#12769)
#### Description

- Billing is now authorized through the `subscription` permissions
instead of the admin role, so custom roles can be granted billing
access.
- Denied users keep the page, tab, and controls in view; usage shows the
denied callout and checkout/portal actions are disabled with a tooltip.
- Billing calls the `/api/v1/subscriptions` endpoints added in #12767
through the generated client.

#### Additional Information

Part of SigNoz/platform-pod#3091. Stacked on #12767; retarget to `main`
once that merges.
2026-09-07 17:29:43 +00:00
Abhi kumar
56e7388677 fix(dashboards-v2): keep auto refresh running in full screen and the table page size sticky (#12776)
#### Description

- Auto refresh stopped in full screen: the timer lived inside the time
selector, which the full-screen branch unmounts along with the toolbar,
so the global time window never advanced and panels never refetched. The
timer now lives in a hook plus a headless `AutoRefreshTicker`, mounted
in the full-screen branch so exactly one timer runs in either mode.
- Table panel page size always snapped back to 10: the height-derived
page size was passed straight into antd's controlled
`pagination.pageSize` and `onChange` dropped the new size. A size picked
from the size changer now wins; panels that were never touched still
auto-fit to the panel height on resize.

Closes https://github.com/SigNoz/pulse-pod/issues/329
2026-09-07 16:29:46 +00:00
Naman Verma
861380dc65 feat: add create v2 api for notification channels (#12638)
<!--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 adds one endpoint, `POST /api/v2/notification_channels`, whose
typed `{name, displayName, config:{kind, spec}}` body replaces v1's
pass-through Alertmanager receiver JSON. v1 routes are deliberately
untouched, so the diff is near-purely additive.
List/get/update/delete/test come in the next PR.

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR

Closes https://github.com/SigNoz/pulse-pod/issues/296

<!--Anything reviewers should keep in mind while reviewing -->

Eventually references (rules, routing policies) migrate onto
internal_name, freeing name to become a user-editable display name. But
that will happen post rules migration so that all rules are on v2

<!--Please delete paragraphs that you did not use before submitting.-->
2026-09-07 11:36:12 +00:00
Gaurav Tewari
391f685e57 feat(llm-observability): AI explorer QB and builder_ai_query wiring (#12683)
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
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

- Adds the `builder_ai_query` envelope type, seeded by the AI explorer
via `initialQueryAIWithType`.
- AI queries pull filter-bar keys from
`/api/v1/ai_observability/fields/keys` and values from
`/api/v1/ai_observability/fields/values`.
- `QuerySection` drops the builder's custom order-by (`ExplorerOrderBy`
via `renderOrderBy`) for the list and trace panels, along with
`showTraceOperator`. Ordering for those views is the list's own concern,
not the builder's — the trace view supplies its own control in #12688.
`limit` and `having` are now rendered disabled on every panel instead of
hidden on list.
- Hides the span-scope selector for AI queries — "Root / Entrypoint
Spans" ANDs badly with the gate, since GenAI attributes sit on nested
spans. Derived from the query itself, so no new prop on the shared
builder.
- `createNewBuilderQuery` inherits `builderQueryType` from the first
query, the way it already inherits `source`.
- Response conversion reads aggregation metadata from both builder
envelope types, not just `builder_query`.

TDD -
https://app.notion.com/p/signoz/Query-Builder-Changes-3b4fcc6bcd1980889c7aed782665e78d

#### Issues closed by this PR

Closes -
https://github.com/orgs/SigNoz/projects/39/views/20?pane=issue&itemId=226748421&issue=SigNoz%7Cengineering-pod%7C5889

#### Screenshots / Screen Recordings


https://github.com/user-attachments/assets/985cb178-ffb0-405c-b0a6-82ec242c3d68

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-09-07 07:29:31 +00:00
Naman Verma
e0da06f76d chore: add type for strings that should be unset or non empty (#12774)
Some checks failed
build-staging / prepare (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (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
<!--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

If an API request body has a field that cannot be empty, but backend can
fill in a default value for it, then backend should fill that default
value only when the field is omitted in the request. If the user has
explicitly sent `""`, then backend should reject it so that there is no
request-response drift.

Such fields can be typed as the new `UnsetOrNonEmptyString` which has
custom unmarshalling logic. If the field is set in the request json,
then the custom unmarshal logic is called and explicit `""` is rejected.
If the field is not set, then the function is not called, and further
backend logic is free to fill in the default value.

<!--Anything reviewers should keep in mind while reviewing -->
#### Additional Information

Came out of notification channels V2 API work.

<!--Please delete paragraphs that you did not use before submitting.-->
2026-09-04 15:31:19 +00:00
Naman Verma
85f9924b4b fix: reject trailing / in jira urls instead of trimming (#12772)
<!--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 is so that a round trip drift doesn't happen. This was caught for
incident io in its PR but we missed it out here.

<!--Anything reviewers should keep in mind while reviewing -->
#### Additional Information

Found as a part of round trip testing in v2 notification channels create
API

<!--Please delete paragraphs that you did not use before submitting.-->
2026-09-04 14:43:30 +00:00
Ashwin Bhatkal
c015622258 fix(quick-filters): keep the filter expression in sync with the filter items (#12755)
#### Description

Unchecking a value in a Quick Filter V2 checkbox filter could not be
undone: the first click excluded the value (`not in`), the second
flipped it to `in` instead of clearing it, and the third appeared to do
nothing.

Quick filters dispatch through the URL, and the composite-query parser
merges `filters.items` into `filter.expression` on the way back in. That
merge only adds and rewrites clauses — it never drops one — so a clause
left behind in the expression resurrects a filter the user just removed.
This is why the bug shows up only for `filter.expression` and not for
`filter.items`.

Two causes, both in `applyCheckboxToggle`:

- Several branches removed the key from `filters.items` without removing
it from `filter.expression`. Rather than patching each removal site, the
expression is now re-derived from the updated items once before
returning. That also covers operator swaps (`not in` → `in`), which the
merge cannot rewrite in place because it keys on key + operator. Four
ad-hoc strips became redundant and were dropped.
- Under a `NOT IN` clause, the "user clicked an unchecked value, so
select it" branch fired for every unchecked value. But under `NOT IN`
the unchecked values are exactly the excluded ones, so that branch
swallowed the "re-include this value" case and the removal branch below
it was unreachable. It is now gated on the value not already being
excluded; a genuinely unselected value in **All values** still becomes
`in`.

Toggling a value is now a two-state cycle: no clause ⇄ `not in
['value']`.

The second commit narrows what that re-derivation is allowed to touch,
fixing two adjacent defects of the same kind:

- It stripped **every** clause for the attribute key, including
predicates the checkbox does not own (`CONTAINS`, `EXISTS`, a range).
Clearing a filter deleted such a clause outright — the header's Clear
button stays active even when the checkboxes are disabled by a second
clause on the key — and a plain toggle rewrote it, losing the spelling
the user typed. `removeKeysFromExpression` takes an optional operator
restriction, and the checkbox passes the four operators it actually
emits.
- It matched keys literally while the items side matches by base name
via `isKeyMatch`, so a context-prefixed clause such as
`resource.service.name` was left in the expression and resurrected the
filter. Both sides now agree on which spellings are the same filter.
- `clearFilterFromQuery` stripped the expression at every query index
while filtering items only at the active one, churning a clause in other
queries that the round trip put straight back. It now leaves non-active
queries alone.

#### Issues closed by this PR

Closes https://github.com/SigNoz/platform-pod/issues/3054

#### Additional Information

The 127 pre-existing Quick Filters tests pass both before and after this
change — they assert UI state and the dispatched filter items, never the
resulting expression, which is the gap that let this through.

`checkboxFilterQuery.test.ts` replaces that gap with a table of 40 cases
that assert the structured items and the shipped expression
**together**, each one driven through the real URL round-trip. Against
the code before this PR the same table fails 9 cases. It covers the
incident's own click sequence — re-checking a value that is actually in
the exclusion list — which nothing previously exercised.

One unrelated pre-existing failure in this area, for anyone running the
suite: `QuerySearch.test.tsx › fetches key suggestions on mount for
LOGS` fails on `main` when that spec runs on its own.
2026-09-04 12:22:00 +00:00
Vikrant Gupta
0f36cb9334 feat(subscription): add subscription endpoints with resource authz (#12767)
#### Description

- Adds a `subscription` domain: `POST`, `PUT`, and `GET
/api/v1/subscriptions`, wired with `CheckResources` + `ResourceDef`s on
the `subscription` metaresource (`create`, `list` + `update`, `read`).
Community gets a noop implementation; enterprise talks to Zeus.
- Migration `125_add_subscription_tuples` backfills the admin
subscription tuples for existing organizations.
- The legacy `/api/v1/checkout`, `/api/v1/billing`, and `/api/v1/portal`
routes are untouched; they are deleted once the frontend has moved.

#### Additional Information

Part of SigNoz/platform-pod#3091.
2026-09-04 10:15:17 +00:00
Abhi kumar
e1ee386016 refactor(dashboards-v2): declare per-kind query capabilities instead of inferring them from panel types (#12559)
#### Description

V2 panels answered "how does this panel's query behave?" by comparing
against the legacy `PANEL_TYPES` enum. Each kind now declares it, so
adding a kind means stating its behaviour once instead of finding every
switch that should have mentioned it.

- **Kinds declare their query behaviour** — request type, table
formatting, step-interval and order treatment, paging, list-view
authoring, trace operator. `buildQueryRangeRequest` takes that block, so
the `PANEL_TYPES.BAR` / `.LIST` / `.TABLE` branches are gone. An
exhaustive `Record<PanelKind, …>` test means a new kind can't ship
without declaring its request shape.
- **The capabilities are passed in, not looked up.** The panel registry
carries every renderer with it, so importing it into the data path drags
the app's API client into anything that touches the request builder. The
call sites already resolve the definition.
- **The chart layer no longer infers a time axis from a panel type.**
`UPlotAxisBuilder` decided X-axis date formatting from a hardcoded
`[TIME_SERIES, BAR]` list, so a chart that plots time but isn't one of
those two silently lost its formatted ticks — no type error, no failing
test. Callers now declare `isTimeAxis`.
- **`getPanelDefinition` always resolves.** It was typed to return a
definition for any `PanelKind`, but the registry only holds registered
kinds, and a spec from a newer SigNoz names one this build has never
heard of. Callers coped by truthiness-checking a value the type said
couldn't be falsy — a lint autofix had already deleted one such guard in
`PublicPanel`. Unknown kinds now resolve to `UNSUPPORTED_PANEL`, which
declares nothing and renders as unsupported; `isPanelKindSupported` is
the separate question the lazy fetch and editor session actually needed.
- **Analytics gained `panelKind`** on all seven panel events, alongside
the existing `panelType` so current reports keep resolving. `panelType`
can't distinguish two kinds that map onto it.
- **Removed `ViewPanelQueryBuilder`** — no importers; the View modal
renders `PanelEditorQueryBuilder`. It referenced a stylesheet class that
no longer exists.

Behaviour is unchanged for every registered kind. The one visible
difference: a panel whose kind this build can't render now says so,
instead of rendering a header above an empty body.

#### Issues Closed
Closes https://github.com/SigNoz/pulse-pod/issues/279

#### Additional Information

- **Read it commit by commit** — each is one theme (declare / request
path / axis / builder mode / analytics / registry), and the diff is
mostly deletions once the declarations are in place.
- The legacy enum still appears in ~28 V2 files, all of it *translation
at a boundary* rather than a decision: the V1 `Query` pivot
(`mapCompositeQueryFromQuery` writes `panelType` into
`ICompositeMetricQuery`), URL params (`graphType` / `panelTypes` are a
serialised contract), the shared `QueryBuilderV2` provider (where
`panelType` is provider state read by its subcomponents), and analytics.
A follow-up will quarantine those into a single boundary module with a
lint rule keeping them there.
- The last commit deletes `resolveQueryCapabilities`, added earlier in
this branch: it existed only to absorb a missing definition, which the
registry no longer produces.
2026-09-04 09:54:52 +00:00
Abhi kumar
717d37a945 chore(dashboard): retire the V1 variable runtime (#12710)
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
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
The V1 variable engine had no writers left: nothing wrote selectedValue,
so getDashboardVariables produced undefined values, variableFetchStore
was never updated, and the dependency graph and derived store fields
only fed that store. The shared store's one remaining job is publishing
the open dashboard's dynamic variables for query-builder autocomplete,
which needs a name and an attribute.

Replace it with a suggestion feed and delete the rest, including the
panel variables prop that no GridCard caller passed and
useResolveQuery's dashboardData option that no caller supplied.
useGetResolvedText loses its only variable source and becomes the title
truncation its callers already used it for.



<!--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

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR

Closes https://github.com/SigNoz/pulse-pod/issues/326

<!--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

<!--Please delete paragraphs that you did not use before submitting.-->
2026-09-04 06:31:38 +00:00
Abhi kumar
63b130ccd1 chore(codeowners): retarget the dashboard entries at their new paths (#12707)
#### Description

Post Dashboard v1 cleanup, codeowners update

#### Additional Information

Closes https://github.com/SigNoz/pulse-pod/issues/325
2026-09-04 05:28:03 +00:00
Abhi kumar
618031ddaa chore(dashboard): retire the V1 panel editor and its widget route (#12648)
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
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
> **Stacked PR — merge bottom-up.** This is part of a stack retiring the
V1 dashboard frontend.
>
> | | PR | Change |
> |---|---|---|
> | 1 | #12647 | delete dead V1 dashboard code |
> | 2 | #12648 | retire the V1 panel editor and its widget route |
> | 3 | #12649 | legacy notice for unmigrated public dashboards |
> | 4 | #12650 | retire the V1 dashboard store |
> | 5 | #12651 | move the shared chart layer to `lib/visualization` |
> | 6 | #12652 | consolidate the widget-card stack under
`container/WidgetCard` |
> | 7 | #12653 | split `types/api/dashboard/getAll` |
> | 8 | #12654 | drop the `V2` suffix |
> 
> `CODEOWNERS` for all of the above is split out into **#12707**, which
is *not* part of this stack (based on `main`) and should merge after it.
> 
> Review this one against **#12647**, not `main`.

---

#### Description

`ROUTES.DASHBOARD_WIDGET` (`/dashboard/:dashboardId/:widgetId`) was
still registered but no UI linked to it, and its page fetched `GET
/api/v1/dashboards/{id}` — which the backend answers **501**. V2 serves
panel editing at `/dashboard/:dashboardId/panel/:panelId`.

- Extract the 12 modules other features still need out of
`container/NewWidget` first, then delete the route, the page and the
container.
- Threshold/format/time types → `types/api/widgets/threshold`,
`constants/formats/*`, `constants/timePreference` (fixing the
`alertFomatCategories` spelling on the way).
- `QueryTypeTag`, `PlotTag`, `populateMultipleResults`, the ContextLinks
utils and the four externally-used `utils` exports → `components/`,
`lib/query/`, `utils/contextLinks/`.
- The two raw query editors →
`container/QueryBuilder/rawQueryEditors/{PromQL,ClickHouse}`, where the
rest of the query-builder UI lives.
- Delete the V1 write path (`useUpdateDashboard` →
`api/v1/dashboards/id/update`) and the orphaned bootstrap chain.

#### Additional Information

**Two behaviour changes worth a look:**

1. **Meter Explorer's "Add to dashboard" was already broken.** It built
a V1 editor URL (`/dashboard/:id/new`), so users landed on the 501 page.
It now uses `useGetExportToDashboardLink` like the Logs, Traces and
Metrics explorers. This is a fix, but it touches Meter Explorer.
2. `FullView`'s **Switch to Edit Mode** button is removed — it built its
link with `generateExportToDashboardLink`, which goes away here, and it
was gated on V1 state nothing populates, so it never rendered.

`WidgetHeader`'s **Edit**/**Delete**/**Clone** items are left alone. No
caller lists them in `headerMenuList`, so nothing renders them either
way, and leaving them keeps this PR scoped to the editor route.

Two tests changed assertions rather than just mocks
(`WidgetGraphComponent.test.tsx`, `ExplorerOptionWrapper.test.tsx`) —
those are the diffs to read closely; the rest is mechanical.

Also extracts `ColumnUnit` to break the `getAll` ↔ `threshold` import
cycle the type move would otherwise have created.

**Verification:** `tsgo`, `lint`, `jest` (751 suites / 7386 tests),
`build`, `knip` all clean.
2026-09-03 15:35:54 +00:00
Vikrant Gupta
8680361c68 chore(flagger): remove get_meters_from_zeus feature flag (#12766)
#### Description

- Removes the `get_meters_from_zeus` feature flag; `GET /api/v1/billing`
now always fetches usage from Zeus.


#### Additional Information

Part of SigNoz/platform-pod#3091. First of four PRs; the new FGA-gated
zeus subscription endpoints follow in the next one.
2026-09-03 12:35:08 +00:00
Nikhil Mantri
bfddd10dc6 feat(alert-channel-integrations): Jira add wontFixResolution UI Option (#12765)
#### Description

- Adds a **Won't-fix resolution** input to the Jira channel form
(Advanced section), with a help line explaining what it does. The
backend already supported `wont_fix_resolution`; it was API-only until
now.
- When set (e.g. `Won't Do`), a Jira issue resolved with that resolution
is not reopened when the alert fires again; a new issue is created
instead. Optional; empty keeps the current behavior.
- Editing a channel now preserves an API-set `wont_fix_resolution` on
re-save (it was silently dropped before).

#### Screenshots / Screen Recordings

<img width="1441" height="110" alt="Screenshot 2026-09-03 at 4 10 12 PM"
src="https://github.com/user-attachments/assets/fcefa36d-1f41-4bac-acf9-334356e52267"
/>
2026-09-03 11:02:48 +00:00
Abhi kumar
b232ecf01c chore(dashboard): remove dead V1 dashboard code (#12647)
> **Stacked PR — merge bottom-up.** This is part of a stack retiring the
V1 dashboard frontend.
>
> | | PR | Change |
> |---|---|---|
> | 1 | #12647 | delete dead V1 dashboard code |
> | 2 | #12648 | retire the V1 panel editor and its widget route |
> | 3 | #12649 | legacy notice for unmigrated public dashboards |
> | 4 | #12650 | retire the V1 dashboard store |
> | 5 | #12651 | move the shared chart layer to `lib/visualization` |
> | 6 | #12652 | consolidate the widget-card stack under
`container/WidgetCard` |
> | 7 | #12653 | split `types/api/dashboard/getAll` |
> | 8 | #12654 | drop the `V2` suffix |
> 
> `CODEOWNERS` for all of the above is split out into **#12707**, which
is *not* part of this stack (based on `main`) and should merge after it.

---

#### Description

V2 dashboards serve `/dashboard` and `/dashboard/:id` unconditionally —
there is no feature flag — so the V1 page bodies and everything
reachable only from them are dead.

- Point the routes straight at the V2 pages and delete the V1 shims.
- Delete `container/ListOfDashboard`, the non-`visualization` half of
`container/DashboardContainer`, and the `GridCardLayout` grid shell.
These are mutually entangled (`ListOfDashboard` ↔ `DashboardDescription`
is an import cycle), so they have to go together.
- Rescue the shared code that lived inside them before deleting: the
variable dependency graph → `lib/dashboardVariables/dependencyGraph`,
`uniqueOptions` → `NewSelect`, panel-type items →
`GridCardLayout/panelTypeItems`.
- Drop two menu affordances that no live caller could render: panel
**Delete**/**Clone** (no consumer lists them in `headerMenuList`) and
the **Dashboard Variables** drilldown submenu (route-gated to
`/dashboard/:id`, which V2 serves through its own drilldown).
- V2's variable cycle detection now uses V2's own dependency helpers
instead of casting its form model into V1 types.

#### Additional Information

`container/DashboardContainer/visualization` is deliberately untouched —
it is the shared chart layer, not V1 code. It moves later in the stack.

**Verification:** `tsgo --noEmit`, `lint`, `jest` (766 suites / 7491
tests), `build`, and `knip` (unused files 51 → 41, zero newly orphaned)
all clean.
2026-09-03 10:38:20 +00:00
Vikrant Gupta
7faab60ea7 feat(authz): enable FGA for ingestion keys and limits (#12626)
#### Description

- Moves all ingestion key and limit routes from the legacy `EditAccess`
gate to `CheckResources` + `ResourceDef`s (kinds `ingestion-key` /
`ingestion-limit`) with scoped security schemes.
- Limit create checks `create` on the limit plus `attach` on the parent
key; limit delete checks `delete` plus `detach`, resolving the parent
key via the new upstream get-limit call.
- Grants `attach`/`detach` on `ingestion-key` to the admin and editor
managed roles; migration 120 backfills all ingestion tuples for existing
organizations and refreshes the managed roles' `transaction_groups`.

Stacked on #12625.

#### Issues closed by this PR

Closes SigNoz/platform-pod#2651

#### Additional Information

- Wiremock fixtures now use UUID key/limit IDs — the metaresource FGA
selector only accepts UUIDs.
- Delete requests make one extra upstream GET (parent-key resolution)
before the authz verdict, mirroring the serviceaccount extractor
pattern.
2026-09-03 09:34:15 +00:00
Nityananda Gohain
a75442f31e fix: add quick filters v2 api to support TelemetryFieldKey (#12698)
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
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description
The old API didn't support telemetryFieldKey, so adding a new v2 API to
support it.

This PR
* Migrates old data to the new one.
* Existing API's now internally stores it in the new struct so that they
don't break the UI.

<!--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/5947

## Additional details
* the old api is safe with new field as it is just a subset of it.
2026-09-03 07:49:31 +00:00
Nityananda Gohain
52588c4582 fix: support for related values in ai field values (#12716)
#### Description

Adds support for related values in ai observability field values.

<!--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/5975
2026-09-03 07:12:22 +00:00
Nityananda Gohain
e485f50221 feat: support ai trace aggregate filtering in ai span list (#12122)
## Pull Request

---

### 📄 Summary
* Span list (raw) queries can now leverage trace-level `trace.` filter
conditions leading to the trace-level component being qualified with
`__trace_scope`.
* An error is now raised if the span list is ordered by the trace level
key.


The following constraint is known: in case of ordering by `timestamp`,
the querier processes the span list by time bucket thus performing
trace-level aggregates calculation per bucket not within a time window.
The records are kept separately.


#### Issues closed by this PR

Fixes https://github.com/SigNoz/engineering-pod/issues/5976

---

###  Change Type
_Select all that apply_

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

---

### 🧪 Testing Strategy
> How was this change validated?

- Tests added/updated: 
- Manual verification: 
- Edge cases covered:

---

### ⚠️ Risk & Impact Assessment
> What could break? How do we recover?

- Blast radius: None
- Potential regressions:
- Rollback plan:
2026-09-03 07:10:24 +00:00
Vikrant Gupta
b3b547f34a feat(gateway): add first-class ingestion limit APIs (#12625)
#### Description

- Adds first-class ingestion limit APIs under
`/api/v2/gateway/ingestion_limits`: create (`keyId` in body), get,
update, and delete by `{limitId}`. Get proxies the new upstream `GET
/v1/workspaces/me/limits/{limitID}`.
- Adds key read APIs: `GET /api/v2/gateway/ingestion_keys/{keyId}` (key
by id — upstream does not embed limits here) and `GET
/api/v2/gateway/ingestion_keys/{keyId}/limits` (limits for a key, with
current-period usage metrics).
- Marks the existing limit routes (`POST
/ingestion_keys/{keyId}/limits`, `PATCH/DELETE
/ingestion_keys/limits/{limitId}`) as deprecated; they keep working
unchanged.
- Renames the old create body to `DeprecatedPostableIngestionKeyLimit`;
`PostableIngestionKeyLimit` is now the first-class body carrying
`keyId`. Handlers decode via `binding.JSON` and the create response is
`types.Identifiable`.

Part of SigNoz/platform-pod#2651.

#### Additional Information

- OpenAPI spec and the generated frontend client are regenerated; the UI
stays on the deprecated routes for now.
- Requires the upstream get-by-id endpoints from
SigNoz/opentelemetry-gateway#96 (merged and deployed).
2026-09-03 07:07:10 +00:00
Naman Verma
be9a045455 chore: add internal name to data layer of notification channels (#12754)
<!--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

Convert the existing `name` to store an immutable DNS1123 internal name
of a notification_channel, and add a display name column where the data
from the existing `name` column will go.

This is just a database level change. The internal name is not being
used by any consumer, be it the API or rules or route policies. All that
will come in subsequent PRs

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR

Part of https://github.com/SigNoz/pulse-pod/issues/296

<!--Anything reviewers should keep in mind while reviewing -->
#### Additional Information

Eventually references (rules, routing policies) migrate onto the
internal name, freeing the display name to become a user-editable. But
that will happen post rules migration so that all rules are on v2.


<!--Please delete paragraphs that you did not use before submitting.-->
2026-09-03 06:52:58 +00:00
Nikhil Mantri
297d3dd44b feat(alert-channel-integrations): incidentio frontend (#12645)
## Description

Frontend for the **incident.io** alert channel (backend in #12644 — this
PR is stacked on it).

- Adds **incident.io** to the channel-type dropdown with a settings
form: alert source **URL** + **token** (both required), and
**title/description** template fields prefilled with the backend
defaults — same UX as Jira/JSM.
- A tip above the form links to the setup docs (create an HTTP alert
source in incident.io, copy URL + token).
- Client-side validation mirrors the backend for a nicer error
experience: both fields required, URL must be an alert events URL
(`…/v2/alert_events/http/<source_config_id>`).
- `send_resolved` is seeded **on** so incident.io alerts resolve with
the rule (backend can't default it — same reasoning as JSM).
- **Additional metadata** section: key-value rows merged into every
alert's metadata on top of the alert's labels (channel wins on clash);
values may use templates.
- Create, edit and test-channel flows all wired; editing prefills from
the stored `incidentio_configs`.

Notes for the reviewer:

- Follows the JSM Ops form/handler pattern file-for-file; no new
patterns introduced.
- Tests: 4 create-flow cases (fields render, required-field error, URL
validation error, payload shape with defaults) + 1 edit-flow payload
case.

## Issues closed by this PR

Closes SigNoz/pulse-pod#173

---------

Co-authored-by: Naman Verma <naman.verma@signoz.io>
2026-09-03 06:20:47 +00:00
Nikhil Mantri
e84a61d43f feat(alert-channel-integrations): incidentio channel integration (#12644)
## Description

Adds **incident.io** as a native alert notification channel, using
incident.io's HTTP alert source (Alert Events V2 API)

- A channel is configured with the alert source's **URL + token**; title
and description templates are prefilled with the same defaults as
Jira/JSM.
- Alerts fire and auto-resolve in incident.io; the description is
markdown (incident.io renders it natively) and carries the usual deep
links — **View in SigNoz, related logs, related traces**.
- All rule labels (severity, team, custom labels) are sent as
**metadata**, so users can map them to incident.io attributes and
route/escalate on them.

Notes and decisions for the reviewer (full details in the [discussion
ticket and doc](https://github.com/SigNoz/pulse-pod/issues/171)):

- **Dedup:** one incident.io alert per notification group, keyed by the
group key hash (same identity Jira uses). A resolve targets the same
key; re-fires after resolve correctly open a fresh alert — no key
rotation needed.
- **Repeat notifications are no-ops on incident.io** (it drops duplicate
firing events) — unlike Jira, we cannot append updated values to an open
alert; operators click through to SigNoz for current values.
- **Limits:** description capped client-side under incident.io's
documented 512 KB payload limit; retries only on 429/5xx (documented
limit: 120 events/min per source).
- **Channel-level metadata:** optional key-value pairs on the channel
config, merged into every event's metadata on top of the alert's labels
(channel wins on key clash — Opsgenie precedent). Values are
template-expanded; a value that fails to expand is sent raw with a
warning logged, so delivery never breaks on a bad template.
- Upstream alertmanager ships its own basic incident.io notifier — our
config **shadows it** so the SigNoz notifier (templates, dedup,
metadata) handles delivery.
- Frontend (channel form) follows in a stacked PR.

## Issues closed by this PR

Closes SigNoz/pulse-pod#172

---------

Co-authored-by: Naman Verma <naman.verma@signoz.io>
2026-09-03 05:32:51 +00:00
Vikrant Gupta
afb1eb4a41 feat(licensing): add v4 license endpoints with resource authz (#12731)
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
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

- Adds strongly typed `/api/v4/licenses` endpoints on the apiserver with
OpenAPI definitions: activate, list, get, refresh, delete, and `GET
/api/v4/licenses/active`.
- Wires resource authz (`license:create/list/read/update/delete`) via
`CheckResources`; `GET /active` is `OpenAccess` and never includes the
license key — the key is returned only by the FGA-gated get-by-id, so
orgs can grant `license:read` selectively. Migration 118 backfills
license tuples for existing orgs.
- Delete is allowed only for non-cloud licenses; licenses managed by
SigNoz Cloud are rejected.
- v4 responses are camelCase with lowercase enum values; v3 routes and
stored license data are unchanged.

#### Issues closed by this PR

Closes https://github.com/SigNoz/platform-pod/issues/3076
2026-09-02 18:49:35 +00:00
Aditya Singh
4f6414ef61 fix(logs): preserve active viewKey in log-details filter/group/replace (#12757)
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
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

- Fixes the issue where applying filter for value / filter out (also
group by and replace filter) from the log details drawer was removing
the added columns from the table.
- As part of this fix, we rename `id` and `name` fields in
explorerTabChange input type. Now it reads `viewName` and `viewKey` and
avoids confusion
- Now consumers of explorerTabChange need to send viewName and viewKey
only if needed. As now this is an optional field

#### Screenshots/Recording

Before


https://github.com/user-attachments/assets/4c6145e3-4979-4386-9230-f74eb00721c8



After


https://github.com/user-attachments/assets/52bc3083-40a2-4345-aebd-63770882b06c



#### Issues closed by this PR

Closes https://github.com/SigNoz/engineering-pod/issues/6012

#### Additional Information

- same issue was present in the old drawer (group by, replace filter)
and the metrics explorer detail (passed the metric name)..both fixed
here. metrics never surfaced as a bug since it opens in time series with
no columns to collapse
- filter for/out in the old drawer was never affected, it uses a
different add to query path
2026-09-02 12:13:37 +00:00
Nikhil Mantri
70335dc707 feat(alert-channel-integrations): jira + jsm ops channel frontend (#12488)
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
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

Adds **Jira** and **JSM Ops** as alert channel types in the existing
channel flow. No new pages or endpoints — both reuse the same create /
edit / list / test actions as every other channel.

**Jira**

- Required fields: Jira Cloud site URL, Atlassian email + API token,
project, and issue type. **Summary** and **Description** are prefilled,
editable templates (the rich issue body is built server-side).
- The form recommends using an Atlassian **service account**, with a
link to the docs.
- Advanced Options: priority, labels (chip input), resolve/reopen
transition-name overrides, and the reopen window.
- Client-side validation mirrors the backend (must be an
`https://….atlassian.net` URL; reopen window ≥ 1m) purely for a
friendlier error — the backend enforces the same rules.

**JSM Ops**

- The JSM **integration API key** is the only required field — there is
no site or region to configure.
- **Message**, **Description**, and **Priority** are prefilled, editable
templates; **Tags** is a chip input (defaults to `signoz`).

**Shared behaviour**

- `Send resolved alerts` is on by default; on resolve, Jira transitions
+ comments the issue and JSM Ops closes the alert.
- Optional fields left empty are omitted from the payload, so the
backend applies its own defaults.
- Jest tests cover both forms: rendering, prefilled defaults, validation
errors, and the exact save payloads (`jira_configs` / `jsmops_configs`).
- Generated API client types are regenerated to include
`jsmops_configs`; locale strings added for all new fields.

#### Issues closed by this PR

Stacked on top of #12478 · Discussion: SigNoz/pulse-pod#169 · Closes
SigNoz/pulse-pod#170

#### Screenshots / Screen Recordings

Jira Form : 

<img width="1512" height="823" alt="Screenshot 2026-08-18 at 1 16 49 PM"
src="https://github.com/user-attachments/assets/1a3a3419-de1b-4bf1-8a71-50d8341e5d47"
/>

Expanded Jira advanced options : 

<img width="1444" height="406" alt="Screenshot 2026-08-18 at 1 17 08 PM"
src="https://github.com/user-attachments/assets/d1298009-b0f3-4f3d-b960-b805b12f5026"
/>

JSM Ops Form: 

<img width="1479" height="631" alt="Screenshot 2026-08-18 at 1 19 11 PM"
src="https://github.com/user-attachments/assets/678b130b-8b1f-41c8-a78a-b6bd58ae2904"
/>

JSM Ops Advanced Options: 

<img width="1455" height="239" alt="Screenshot 2026-08-18 at 1 19 22 PM"
src="https://github.com/user-attachments/assets/a4425b39-b7e1-4f10-bd0a-83dbc678693a"
/>

#### Additional Information

Notes for reviewers:

- **This branch is stacked on #12478**, so the diff shows the backend
commits too — only the `frontend/` files are new here.
- Follows the pattern of the Google Chat channel frontend.
- **JSM Ops seeds `send_resolved: true` in its prefilled config on
purpose** — the backend cannot default it to on, so the UI carries the
default and sends it explicitly.
- **Tags is a chip input in the UI, but the backend takes a
comma-separated string** — joined on save, split back into chips on
edit-prefill.
- Jira's reopen window is sent as a duration string (`"72h"`) even
though the generated DTO types it as a number, so it's cast at that one
boundary. `custom_fields` stays API-only and is not surfaced in the
form.

---------

Co-authored-by: Naman Verma <naman.verma@signoz.io>
2026-09-02 03:52:24 +00:00
Ashwin Bhatkal
eb2d094b1f feat(infra-monitoring): Kubernetes Containers section (#12636)
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
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description

Adds the **Containers** section to the Kubernetes tab of Infrastructure
Monitoring, breaking each pod down into the app and sidecar containers
running inside it — which the Pods view rolls up into a single row — so
you can tell which container in a pod is throttling, leaking memory or
crash-looping.

- Backed by `POST /api/v2/infra_monitoring/kube_containers` through the
generated client. Filtering, grouping, time range, pagination, column
customization and the instrumentation checks callout all come from the
shared k8s entity framework, so this is mostly configuration rather than
new machinery.
- **List columns:** container name, pod, image:tag, kubectl-style
status, readiness, restarts, CPU and memory usage plus request/limit
utilization. Namespace, node, cluster and deployment sit behind the
column selector. Grouped rows show per-status and per-readiness counts.
- **Detail drawer:** ten `/v5/query_range` charts scoped to the single
container, plus the logs, traces and events tabs. Events are scoped to
the container's *pod*, since Kubernetes emits events per pod rather than
per container.
- A container's identity is the `(k8s.pod.uid, k8s.container.name)` pair
— a container name alone repeats across replicas, and a container ID
changes on every restart. Every other k8s entity is addressable by a
single name, so the first commit widens `SelectedItemParams` with an
optional container name, alongside the cluster and namespace slots that
already serve that purpose.

Columns and charts follow the descriptions in
https://github.com/SigNoz/signoz.io/pull/3644.

#### Issues closed by this PR

Closes https://github.com/SigNoz/engineering-pod/issues/5547

#### Additional Information

- Reviewed best commit by commit: identity foundation, then shared
constants/helpers, then the entity itself. Each stands on its own.
2026-09-01 14:07:03 +00:00
Nityananda Gohain
7c50fe3763 fix: quick filters old migration cleanup (#12746)
#### Description
This PR makes sure that the old migrations of quick filters are
decoupled from types as they should and not imported.

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
Part of https://github.com/SigNoz/engineering-pod/issues/5947
2026-09-01 13:54:36 +00:00
Nikhil Mantri
160a1b018c feat(alert-channel-integrations): jira + jsm ops channel backend (#12478)
#### Description

Adds two Atlassian alert channels. Backend only — frontend is #12488;
channels are created via the API.

**Jira issues — `jira_configs`**

- A firing alert creates a Jira Cloud issue; when the alert resolves,
the issue is transitioned to done. A re-fire within 3 days reopens the
same issue instead of creating a new one. 3 days is default but can be
edited via frontend form.
- The issue body is rich **Atlassian Document Format (ADF)**: a status
panel, the rendered alert description, and deep-links back to SigNoz.
- Re-fires keep the issue in sync (summary and description are
refreshed), and every notification after the first — re-fire, resolve,
reopen — also posts a **comment** carrying the same rich ADF snapshot,
so the issue holds a full lifecycle timeline.
- Per-rule custom notification templates (title/body) are honored, same
as every other channel; multi-alert custom bodies render as
divider-separated sections.
- Auth is Atlassian email + API token; Atlassian **service accounts**
also work (routed via the `api.atlassian.com` gateway automatically —
the cloud id is resolved server-side and client-supplied values are
ignored). Jira Cloud only.

**JSM Ops alerts — `jsmops_configs`**

- A firing alert opens a JSM Operations alert (the ex-Opsgenie alert
product); resolve **closes** it. A fire after close opens a fresh alert
— there is no reopen window.
- Re-fires dedupe into the same alert and increment its count. The alert
description keeps the first-fire snapshot; the value-over-time story
lives in the notes.
- Every fire and the resolve appends a **note** to the alert. JSM Ops
notes support **plain text only** (they render neither HTML nor
markdown), so notes use a new plain-text renderer with links flattened
to `text (url)`.
- The alert description supports JSM's **HTML subset**, rendered from
the same markdown templates.
- Auth is the JSM integration API key. No region/site config needed.

**Also in this PR**

- Unit tests for both config types, both notifiers, and the new ADF +
plain-text renderers.
- OpenAPI spec regenerated (adds `jsmops_configs`).

#### Issues closed by this PR

Closes SigNoz/pulse-pod#168 · Discussion: SigNoz/pulse-pod#169

#### Screenshots / Screen Recordings

Jira alert issue:

<img width="1171" height="739" alt="Screenshot 2026-08-18 at 12 49
31 PM"
src="https://github.com/user-attachments/assets/1ec74757-5d4d-4534-a5ca-13bed07cce72"
/>

Jira issue comments as a timeline:

<img width="1034" height="746" alt="Screenshot 2026-08-18 at 12 50
32 PM"
src="https://github.com/user-attachments/assets/09afb687-b62b-4eb3-86ab-39489e38513e"
/>

JSM Ops alerts page look: 

<img width="1317" height="460" alt="Screenshot 2026-08-18 at 12 52
29 PM"
src="https://github.com/user-attachments/assets/dd5a60b5-ea6c-49b7-afde-f3b2c72b178a"
/>

JSM Ops alerts main body + comment timeline ( comments only support
plain text today ) :

<img width="1323" height="784" alt="Screenshot 2026-08-18 at 12 53
10 PM"
src="https://github.com/user-attachments/assets/871fb91c-307a-4c67-b2c4-bc9548506cbc"
/>

#### Additional Information

Notes for reviewers:

- Jira shadows upstream Alertmanager's `jira_configs` so our notifier
handles it instead of upstream's; this needs a small dedupe in
`PostableChannel.JSONSchema()` and leaves every other channel type
untouched.
- JSM Ops reuses the existing Opsgenie notifier; all new behaviour sits
behind a single `advancedFeatures` flag, so plain Opsgenie is unchanged
when it's off.
- `send_resolved` defaults off for both channels, so resolve-time
behaviour (Jira transition, JSM close + resolved note) needs it set on
the channel; the frontend will send it on by default.
- Notes are best-effort: a permanently-failed note (e.g. the first-fire
note racing JSM's asynchronous alert create) is dropped with a warning
instead of failing the whole notification. Nothing is lost — that first
datapoint is already in the alert body; retryable failures (429) still
retry.

---------

Co-authored-by: Naman Verma <naman.verma@signoz.io>
2026-09-01 13:03:48 +00:00
Naman Verma
27acdbd970 fix: ensure that alerts created as disabled don't actually fire (#12743)
<!--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

Patch and Edit Rule APIs would call syncRuleStateWithTask instead of
adding the task blindly, which is what the create API was doing. This PR
fixes the incorrect call in the create API

<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR

Closes https://github.com/SigNoz/pulse-pod/issues/312
2026-09-01 11:26:43 +00:00
Nityananda Gohain
7eb610287e feat: system dashboards (#12620)
#### Description
Adding support for system dashboards.
* as of now updates are only through new versions in the file.
* user cannot update the dashboard
* For now kept the dashboard content empty and will raise it separately.


Closes https://github.com/SigNoz/engineering-pod/issues/4501
2026-09-01 10:17:10 +00:00
Tushar Vats
74c946fe79 fix(querier): decode JSON columns in table and graph queries (#12555)
#### Description

**What was broken:** on a stack using the new JSON log body, any query
asking for a **table** (`scalar`) or a **graph** (`time_series`) failed
with HTTP 500 if the result contained a JSON column. The logs list view
worked fine, which is why this went unnoticed. The smallest way to hit
it is a raw ClickHouse panel running `select * from
signoz_logs.logs_v2`.

**Why it happened:** we ask ClickHouse to send JSON columns as plain
text, but the driver reports that such a column needs a different Go
type — so the reader prepared the wrong kind of container and the read
failed. The driver only reports the correct type *after* the first row
has been read, which is too late for code that sets up its containers up
front. The original JSON work patched around this inside the logs-list
reader only; the connection setting that causes it is global, so the
other two readers stayed broken.

**The fix:** correct the reported type once, at the connection that sets
that option, so every reader gets a container that works and receives a
normal map. Concretely:

- `pkg/querier` no longer needs its own workaround — the three readers
are back to ordinary code.
- The older v3/v4 read paths had the same bug and are fixed without any
changes of their own.
- A JSON path value such as `body_v2.level` now comes back as `"error"`
or `7` instead of a driver wrapper object.
- Grouping a graph by the whole JSON body used to collapse every group
into a single unlabelled line; each document now labels its own series.

#### Issues closed by this PR

Fixes https://github.com/SigNoz/engineering-pod/issues/5911

#### Additional Information

Verified end to end against a local stack with 1,000,000 log rows and
200,002 distinct `trace_id`s:

| query | before | after |
| --- | --- | --- |
| table query over a JSON column | 500 | 200, body returned as an object
|
| graph grouped by the JSON body | 500 | 200, 22 series, one per
document |
| graph grouped by `trace_id` (200k groups) | 200 | 200, unchanged |

A follow-up PR stacked on this one reworks how the graph reader
classifies columns — fixing boolean and small-integer columns in raw SQL
panels and cutting the reader's allocations.

Known gaps, unchanged from `main` and out of scope here:

- Waterfall and flamegraph read rows into structs, which this fix does
not cover. Moving span attributes to JSON will need the same type on
those fields, and one helper there fails silently rather than erroring.
- Dashboard variable queries no longer crash on a JSON column but still
reject it as an unsupported value type.
- A `Map(String, JSON)` column **panics inside the driver**, which can
take the process down. Confirmed still unfixed on `clickhouse-go` main,
and not yet reported upstream.
2026-09-01 10:07:56 +00:00
Gaurav Tewari
23dd98bee7 chore: update sample json in test ai-o11y (#12741)
#### Description

- Update the sample span JSON in the LLM Observability attribute-mapping
Test tab to use OpenInference-style attribute keys (`llm.model_name`,
`llm.provider`, `llm.token_count.*`, `input.value`, `output.value`)
instead of the earlier mix of `gen_ai.*` and placeholder `my_company.*`
keys.
- The sample is what users see first when trying out attribute mapping,
so it should reflect the attribute shape they'll actually be mapping
from.

#### Issues closed by this PR

Closes -
https://github.com/orgs/SigNoz/projects/39/views/20?pane=issue&itemId=238442542&issue=SigNoz%7Cengineering-pod%7C5997

#### Screenshots / Screen Recordings

<img width="911" height="572" alt="image"
src="https://github.com/user-attachments/assets/88df20e9-0131-415c-9770-67ea8196075a"
/>


#### Additional Information

- Constant-only change (`SAMPLE_SPAN_JSON` in `spanInputStorage.ts`); no
parsing or mapping logic touched.
- Worth a sanity check that the new keys line up with the attribute
names the mapping UI is expected to suggest.

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-09-01 09:30:30 +00:00
Swapnil Nakade
aca35a1309 chore: bumping cloud integration agent version to v0.0.14 (#12737)
<!--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
Bumping cloud integration agent version from v0.0.13 to v0.0.14

#### Contributes to
https://github.com/SigNoz/platform-pod/issues/3038
2026-09-01 09:26:50 +00:00
Srikanth Chekuri
c65f6fa77d fix(instrumentation): stop recording promql engine spans (#12732)
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
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
## Description

- The promql engine traces every evaluation through the global tracer
provider under an unnamed scope: `promqlExec`, `promqlPrepare`,
`promqlExecQueue`, and one `promqlInnerEval eval *promql.<Node>` span
per AST node per query. These didn't find much useful as the bottleneck
is usually the CH so we remove them.
2026-08-31 15:38:11 +00:00
Srikanth Chekuri
4d015a927e perf(clickhouseprometheusv2): skip the series lookup for statically named transpiled units (#12728)
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
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
## Description

<img width="1420" height="516" alt="image"
src="https://github.com/user-attachments/assets/cc536314-71d2-4750-b394-d53c41ac7d91"
/>

the un-needed query contributed to this data read to query service,
which had a purpose in earlier dev cycle but not longer needed.
2026-08-31 14:13:25 +00:00
Srikanth Chekuri
abebea532b feat(prometheus): add the Prometheus query API under a /prometheus prefix (#12093)
#### Description

- Adds `GET|POST /prometheus/api/v1/query_range` and
`/prometheus/api/v1/query` (`pkg/prometheus/promapi`), following the
Prometheus HTTP API contract: float-unix or RFC3339 times, float-seconds
or duration-string durations, the `{status, data, errorType, error,
warnings, infos}` envelope with Prometheus' status codes, and the
11,000-point cap.
- The `/prometheus` prefix works as a drop-in Prometheus base URL:
Grafana's Prometheus data source, promtool, and the PromQL compliance
tester append `/api/v1/*` to a base URL, so they can point at SigNoz
unmodified. Same layout as Mimir/Cortex.
- Wired through `signoz.Handlers` (`prometheus.Handler` interface,
constructed in `NewHandlers`) like the other domain handlers.
- Range queries serve through the `RangeExecutor` capability when the
provider has it, so a clickhousev2-serving deployment transpiles through
these endpoints too.
- New `promapiconformance` integration suite: the frozen promqltest
corpus replayed against these endpoints with `prometheus::provider:
clickhousev2` — the two paths nothing else exercises (v2 as serving
provider, and this API surface). Instant cases go through `/query` with
a real `time` parameter. The `instant-coarse` corpus variants are
skipped — they exist only to encode instant evals as coarse ranges for
the v5 API, and their transpiled coarse-step serving is already covered
and ledgered by promqlconformance's clickhousev2 leg — so this suite
asserts zero divergences with no ledger of its own.
- Purely additive: the existing `GET /api/v1/query_range` and `GET
/api/v1/query` handlers are untouched. `openapi.yml` is generated and
these mux-registered routes are outside the generator, so their
documentation is the upstream Prometheus API contract they follow.

#### Additional Information

Final slice of the clickhouseprometheusv2 stack (#12323, #12324, #12325
— merged). Legacy endpoint removal, if ever, is a separate change after
usage drains.
2026-08-31 12:10:05 +00:00
Nikhil Soni
764fe8ec69 feat(logs): add pinned attributes preference support (#12687)
#### Description

Registers a new per-user preference `log_details_pinned_attributes` in
`pkg/types/preferencetypes`, following the same shape as the existing
`span_details_pinned_attributes` (trace-details pin feature, #11092).
2026-08-31 10:31:59 +00:00
1705 changed files with 37919 additions and 47257 deletions

View File

@@ -0,0 +1,95 @@
---
name: signoz-page-story
description: Explore a SigNoz page, map the endpoints, states and query params it has, then write its Storybook page story with control-driven msw mocks that reach every state. Use when asked to create, extend or review a Storybook story for a page under frontend/src/pages, to add controls to an existing page story, or to write defineStoryMocks handlers and mock data for a page.
---
# SigNoz page stories
A page story renders the real page inside the real app shell against msw, and its
controls panel can reach every state the page has. The panel is the deliverable,
not the story list.
`frontend/src/storybook/README.md` is the API surface (providers, parameters,
control builders, module mocks, navigation). Read it first; this skill is the
process on top of it.
## Workflow
1. **Map the page**: [references/discovery.md](references/discovery.md). Produce
the inventory (endpoints, states, params, permissions, caps) before writing
code. No inventory, no story.
2. **Skeleton first**: story + empty `defineStoryMocks`, then run it. The console
names the endpoints step 1 missed.
3. **Inventory to controls**: [references/controls.md](references/controls.md).
4. **Mock data and handlers**: builders in `__story_mockdata__`, handlers in the page's
mocks module.
5. **Verify in the browser**: [references/verify.md](references/verify.md). Never
report the story as done without it.
## Rules
- **Default is the loaded page.** `export const Default: Story = {}` with no args,
every widget carrying data. Empty, loading and failed are variants or control
values, never the default.
- **A control is a knob on a response**, resolved through `handlers`, `config` or
`effect`. Never a component prop, never a module mock added for one story.
- **Every branch in the inventory is reachable from the panel.** A state that
needs a code edit to see is a missing control.
- **Never re-declare what every story already has**: banner, side nav, data state
(loaded/loading/error), access preset, permissions, check state.
- **A variant earns a story only when it is worth linking to**: an empty
workspace, a viewer, a page mid-load. Everything else stays a control.
- **Endpoints the page owns go through `response.json`**, so the Data control
covers loaded, loading and failed in one declaration. Endpoints the page cannot
render without (ingestion detection, preferences, feature payloads) take a
plain resolver so the shell survives the loading and error states.
- **Query-param state starts from `route`** (`/logs?tab=explorer`). In-page param
navigation works inside a story; a different pathname is blocked and reported
by the overlay. A control for a param is worth it only when the param is a page
mode someone would want to flip.
- **File layout**: every story file for a page lives under
`src/pages/<Page>/stories/`: `<Page>.stories.tsx`, `<Page>.stories.mocks.tsx`,
payload builders in `stories/__story_mockdata__/<page>.ts`. Nothing
page-specific in `src/storybook/controls/`.
- **The mocks are AI-owned and say so.** `<Page>.stories.mocks.tsx` and every file
under a `__story_mockdata__/` open with this banner, above the imports:
```ts
/**
* AI-owned. Generated and maintained by the `signoz-page-story` skill.
* Do not hand-edit: regenerate instead.
*/
```
The root `.gitattributes` marks both paths `linguist-generated=true`, so the
reviewer gets them collapsed and spends the attention on the rendered page. The
story file is the human surface and never carries the banner. A file with the
banner has to stay regenerable from the page alone: no page knowledge in
`src/storybook/`, and builders typed from `src/api/generated` where the endpoint
has types, so a contract change is a compile error instead of a mock that lies.
- **Reuse fixtures** from `src/mocks-server/` and `src/tests/fixtures/` where they
exist. An endpoint jest needs too belongs in `src/mocks-server/handlers.ts`.
- **Shared response builders live in `src/storybook/msw/__story_mockdata__/`**: typed
helpers like `queryRangeV5ScalarResponse` that multiple pages need. Before
writing a response shape inline, check if a builder exists; if not and the
shape will repeat, add it there. Page-specific builders stay in the page's
`__story_mockdata__/`.
- **No comment is the default.** Write one only for what the code cannot show:
a shape the backend dictates, an app bug the mock reproduces, an ordering or
cap the page depends on, a workaround and the reason for it. Never restate a
name, a type, or what a builder plainly builds; if the sentence reads as the
signature in prose, delete it. Nothing addressed to a reviewer. The one
comment a story always gets is its own doc comment: what it shows, in the
page's own terms.
## Done means
- [ ] `Default` shows the page with data, checked in dark and light
- [ ] the mocks module and every `__story_mockdata__` file carry the AI-owned banner
- [ ] every control flipped once, its effect seen on screen
- [ ] console clean: no `[storybook] no msw handler`, no 501, no msw unhandled
request, no React warning
- [ ] no navigation overlay on mount
- [ ] `pnpm tsgo --noEmit`, `pnpm exec oxlint <files>`,
`pnpm exec oxfmt --check <files>` all clean. The repo has no
prettier: `pnpm exec prettier` prints a pass while exiting 254

View File

@@ -0,0 +1,165 @@
# Turning the inventory into controls
Every row of the inventory becomes a control, a global control that already
exists, or a documented reason it cannot be one.
## Imports
Paths written as `src/storybook/...` in prose are repo paths, not import
specifiers. Stories import through the `@/` alias (`@/*``./src/*`); modules
inside `src/storybook/` import each other relatively.
| Import | From |
| --- | --- |
| `toggleControl`, `countControl`, `choiceControl`, `multiChoiceControl` | `../controls/controls` |
| `defineStoryMocks`, `storyMocks` | `../controls/defineStoryMocks` |
| `PageStoryArgs` | `../controls/resolveStoryMocks` |
| `MockRequest`, `MockResponse` | `../controls/types` |
| the page's mocks, from the story | `./<Page>.stories.mocks` |
| `queryRangeV5ScalarResponse`, `queryRangeV5RawResponse`, etc. | `@/storybook/msw/__story_mockdata__/queryRange` |
## Which builder
`src/storybook/controls/controls.ts`:
| The state is | Builder |
| --- | --- |
| on or off (a signal ingesting, a feature present) | `toggleControl` |
| how many rows a list has | `countControl` |
| one of several modes (tab, visibility, plan, severity filter) | `choiceControl` |
| a subset (steps skipped, columns shown, signals selected) | `multiChoiceControl` |
Rules that come with them:
- `countControl` `max` goes past what the page renders, so a story can show the
cap being hit. `0` is the empty state, which is why an empty list rarely needs
its own story. When the cap is in the *request* (`?limit=5`) rather than the
renderer, stop `max` at the limit: a longer response is a body the backend
cannot send.
- `choiceControl` options come from a `const` array typed with
`(typeof X)[number]`, not from string literals scattered in the handlers.
- Defaults describe the fully-populated page. The panel starts where `Default`
starts.
- `group` is `'<Page> · <facet>'`, such as `'Services · lists'` or
`'Alerts · rules'`. Keep a page's knobs in two or three groups, not one per
control.
- `description` only when the name does not carry the effect (what dismissing
does, what the cap is, which widget it feeds).
## Which hook
`defineStoryMocks` takes three, all optional:
- `handlers(values, response)`: the page's endpoints. Everything the page owns
goes through `response.json`, so the global Data control turns the whole page
into loading or failed without a second declaration. An endpoint the page
cannot render at all without (ingestion detection, preferences, license
payloads) takes a plain `rest.get(...)` resolver instead, so the shell stays
visible while the rest hangs or fails.
- `config(values)`: `SignozStoryConfig` for knobs no endpoint covers: `route`,
`appContext`, `reduxState`, `queryBuilder`, `theme`.
- `effect(values)`: module-level state no provider exposes.
One endpoint feeding several widgets stays one handler that reads the request.
`response.json` hands the request to the builder and awaits it, so reading a
query param, or a POST body, does not cost the Data control:
```ts
rest.get(
'http://localhost/api/v1/explorer/views',
response.json((req) =>
savedViews(values.savedViews, req.url.searchParams.get('sourcePage') ?? 'logs'),
),
),
```
```ts
rest.post(
'http://localhost/api/v5/query_range',
response.json(async (req) => {
const body = (await req.json()) as QueryRangeRequestV5;
const signal = body.compositeQuery?.queries?.[0]?.spec?.signal;
return countResponse(values[`${signal}Ingestion`] ? 4213 : 0);
}),
),
```
Reach for a plain `rest.post(url, async (req, res, ctx) => …)` only when the
endpoint has to keep answering while the Data control is on `loading` or
`error`: detection calls the page cannot render without.
## Mutations
A control drives the response, so a write the page makes against state a control
owns does not stick: the refetch answers with the control's value and the button
appears to do nothing. Two honest options: leave it declarative and say so in
the PR, or move the state into `effect` so the handler can read what the page
wrote. Never fake the write by mutating a builder's module state without saying
where the state lives.
## Wiring it up
```ts
// src/pages/Services/stories/Services.stories.mocks.tsx
/**
* AI-owned. Generated and maintained by the `signoz-page-story` skill.
* Do not hand-edit: regenerate instead.
*/
export const servicesMocks = defineStoryMocks({
controls: {
services: countControl('Services', { group: LISTS, value: 8, max: 12 }),
apdex: choiceControl<ApdexState>('Apdex', {
group: HEALTH,
options: APDEX_STATES,
value: 'mixed',
}),
},
handlers: (values, response) => [
rest.post(
'http://localhost/api/v2/services',
response.json(() => buildServices(values.services, values.apdex)),
),
],
});
```
```tsx
// src/pages/Services/stories/Services.stories.tsx
type ServicesArgs = PageStoryArgs<typeof servicesMocks>;
const meta = {
title: 'Pages/Services',
component: Services,
...storyMocks(servicesMocks, { route: ROUTES.APPLICATION, layout: 'app' }),
} satisfies Meta<ServicesArgs>;
```
`PageStoryArgs` folds in the global controls, so a story's `args` can set
`access`, `dataState` or `banner` next to the page's own knobs and stay typed.
## Not a control
- Anything the global controls already cover: banner, side nav, data state,
access preset, permissions, check state.
- A knob whose effect nobody can see on the page. Delete it or find the widget it
was supposed to drive.
- A raw payload as an object control. Controls carry intent (`5 dashboards`,
`viewer`), and the builder turns intent into the payload.
- Anything that needs a module mock or a component prop to work. If the state
cannot be produced from a response, config or module state, say so in the PR
instead of faking it.
## Control or story
Default to a control. Write a story when the state is worth a link:
- the fresh workspace, because that is what a new user sees
- the restricted user, when permissions visibly change the page
- a page-defining mode (a tab, a category) that has its own layout
Combinations of controls do not need stories, which is what the panel is for.
Each story gets one prose doc comment: what it shows, in the page's own terms.
Everywhere else the comment rule in SKILL.md applies: write one only for what
the code cannot show.

View File

@@ -0,0 +1,64 @@
# Mapping a page
Two passes: read the code, then let the running story correct you. Write the
inventory down: it is what the controls are derived from, and the only
protection against a story that renders one state and calls it a page.
## Pass 1: read the page
Start at `src/pages/<Page>/` and follow it outward: the containers it mounts
(`src/container/<Feature>/`), the hooks those use, the components with their own
fetches. Stop at leaf components that take props only.
Grep recipes, run against the page's directories:
| Looking for | Grep |
| --- | --- |
| endpoints | `useQuery\|useMutation\|useInfiniteQuery`, then the `api/` module it calls |
| endpoint URLs | the api module's `axios.get\|post` |
| endpoint URLs behind a generated hook | the hook lives in `src/api/generated/services/<name>/index.ts` and the URL only appears in the fetcher body: `rg 'url: \`' src/api/generated/services/<name>/` |
| url state | `useUrlQuery\|useUrlQueryData\|useUrlSearchState\|useQueryState\|QueryParams\.` |
| navigation | `useSafeNavigate\|history.push\|<Link` |
| permissions | `useAuthZ\|AuthZGuard\|AuthZButton\|hasEditPermission\|routePermission` |
| flags and prefs | `useFeatureFlag\|FeatureKeys\.\|USER_PREFERENCES\.\|userPreferences` |
| empty and error branches | `isLoading\|isError\|isFetching\|length === 0\|!data` |
| render caps | `slice(0,\|PAGE_SIZE\|pageSize\|limit` |
`src/constants/routes.ts` has the route, `src/constants/query.ts` the param names,
`src/lib/authz/README.md` how a permission check resolves.
## The inventory
One table, in the story's PR or scratch notes:
| Endpoint | Feeds | States it can be in |
| --- | --- | --- |
| `GET /api/v1/x` | the header count | populated, zero, error |
Plus four short lists:
- **Query params** the page reads, and what each one switches.
- **Permission checks** the page makes, and what disappears when each is denied.
- **Preferences and flags** that change layout (dismissed banners, onboarding
checklists, opt-in views).
- **Caps**: how many rows each list renders before it truncates or paginates.
A state that appears in this inventory and not in the controls panel is a bug in
the story.
## Pass 2: let it run
Write the story and an empty `defineStoryMocks({ controls: {} })`, point it at the
route with `layout: 'app'`, then open it (see verify.md). The console is the
oracle:
- `[storybook] no msw handler` or a 501 from the catch-all: an endpoint pass 1
missed. Add it to the inventory.
- an msw unhandled-request warning: a request going to an origin the handlers do
not answer on. handlers are declared against `http://localhost`.
- a spinner that never resolves with the Data control on `loaded`: a handler
whose URL does not match what the page calls.
- the navigation overlay on mount: the page redirects, usually because `route`
is wrong or a guard is failing on a permission the controls have not granted.
Repeat until the console is silent. Only then start declaring controls.

View File

@@ -0,0 +1,110 @@
# Verifying a page story
A story is not done because it compiles. It is done when each control has been
seen changing the page and the console is silent.
## Run it
```bash
cd frontend && pnpm storybook --ci --quiet # :6006, background it
```
A newly added `.stories.tsx` takes a few seconds to appear in `index.json` on an
already-running server; an empty first poll is not a broken `stories` glob.
Story ids come from the meta title: `Pages/Services``pages-services`, plus the
story export in kebab-case. Render one story on its own:
```
http://localhost:6006/iframe.html?id=pages-services--default&viewMode=story
```
## Flip controls from the URL
Args are settable in the iframe URL, so a whole sweep runs headless without
touching the panel. Booleans go as `!true` / `!false`, numbers bare, arrays
indexed, several separated by `;`, and the theme through `globals`:
```
&args=services:0;apdex:poor;access:viewer;dataState:loading
&args=signals[0]:logs;signals[1]:traces
&globals=theme:light
```
That is the cheap way to check a control does something: load with and without
it, diff the page text.
## Drive it
Playwright lives in the repo's e2e workspace, so a scratch script can use it
directly:
```js
import pw from '<repo>/tests/e2e/node_modules/playwright/index.js';
const { chromium } = pw;
const browser = await chromium.launch();
const page = await browser.newPage();
const problems = [];
page.on('console', (m) => {
if (m.type() === 'error' || m.type() === 'warning') problems.push(m.text());
});
page.on('pageerror', (e) => problems.push(e.message));
await page.goto(`${story}&args=services:0`, { waitUntil: 'networkidle' });
await page.locator('body').waitFor();
console.log((await page.locator('body').innerText()).slice(0, 1500), problems);
await browser.close();
```
Screenshots are worth taking for `Default` in both themes
(`&globals=theme:light`): text extraction does not catch an unstyled page.
## Gates
- **Console silent.** `[storybook] no msw handler`, a 501 from the catch-all, an
msw unhandled-request warning, a React key or state warning: assume the story
is wrong first. A warning that survives is sometimes the app's. Prove it by
turning off the control that renders the widget and watching the warning go
with it, and by finding the same component elsewhere doing it right. Then
report the app bug in the PR. Never invent a field the API does not return to
silence a warning.
- **No navigation overlay on mount.** "Navigation blocked in Storybook" on load
means the page is trying to leave: wrong `route`, or a guard denying on a
permission the controls did not grant.
- **Every control moves something.** Sweep them one at a time from the URL and
diff the page text. A control with no diff is either wired to nothing or aimed
at a widget that is not rendering. Some only show their effect after an
interaction, such as a tab that has to be clicked or a select that has to be
opened. Drive that interaction rather than calling the control unobservable.
- **Both themes render styled.** An unstyled page means the story is not inside
the provider decorator, or `<body data-theme>` was lost.
- **Roles agree.** `<body data-signoz-story-role>` and
`<body data-signoz-context-role>` disagreeing means the page reads a different
`AppContext` than the story config fills.
- **The page's own navigation works.** Tabs, filters and pagination that write
query params should re-render the page in place; only leaving the page belongs
in the overlay.
## Then the usual
```bash
pnpm tsgo --noEmit
pnpm exec oxlint <changed files>
pnpm exec oxfmt --check <changed files>
```
There is no prettier in this repo. `pnpm exec prettier --check` fetches something
else, prints `Prettier: All files formatted correctly` and exits 254: a pass that is
not one.
## Common failures
| Symptom | Cause |
| --- | --- |
| endless spinner with Data on `loaded` | handler URL does not match the call; handlers answer on `http://localhost` |
| page renders but empty | response shape wrong; compare against the api module's type, not a guess |
| 501 in the console | endpoint nobody mocked; the catch-all is answering |
| new control missing from the panel | project-level control added; the tab needs a reload |
| control flips but nothing changes | the widget is gated by something else: a permission, a flag, a preference |
| shell disappears in `loading` | an endpoint the shell needs went through `response.json`; give it a plain resolver |

View File

@@ -0,0 +1,294 @@
---
name: storybook-visual-diff
description: Screenshot a set of SigNoz Storybook stories, then pixel-diff two runs to see what a CSS or component change did, with the changes tinted over the new shot. Use when asked to take story screenshots, capture a visual baseline, compare before/after of a style change, or find which pages a change affects.
---
# Storybook visual diff
Two scripts under `frontend/scripts`:
- `story-shots.mjs` — screenshots stories off a running Storybook dev server.
- `story-shots-diff.mjs` — pixel-diffs two runs and paints what moved.
Output goes to `frontend/.story-shots/` (gitignored), one directory per run.
## 0. Settle what is being compared, first
A diff is only worth taking when the two runs straddle something. Run twice over
the same tree and the answer is zero, or the noise floor: true, and useless.
So before starting a server, pin down four things. Whatever the prompt already
says, take it and do not ask again; ask only for what is genuinely missing, in
**one** `AskUserQuestion` call.
| To settle | Ask | Options |
| --- | --- | --- |
| Job | "What should this run produce?" | shoot only · baseline for a change you are about to make · compare against a change already in the working tree · compare this branch against another (`main` by default, or one the user names) · compare two configurations of the same story (`--args`, clock, width) · noise floor (same tree twice) |
| Scope | "Which stories?" | offer 2-3 concrete selections read off `index.json` (a page, a `--title` prefix, everything), never open-ended |
| Themes | "Which themes?" | dark · dark + light |
| Read-out | "How should the diff read?" | `green` (changed pixels over the after shot) · `green-parallel` (before \| after \| diff, side by side) · `red` · `red-parallel` · `none` (keep both runs, do not diff) |
Skip a row when the prompt answers it, and skip the whole call when the prompt
answers all of it ("shoot the pods tooltips in both themes" needs no question).
Skip Read-out too whenever the job is *shoot only*, and take `none` for what it
says: shoot both sides, report both paths, run no comparison. When the prompt
says nothing at all, ask; a silent guess here burns ~6 min per sweep on the
wrong stories.
The job decides which loop below to run:
| Job | Loop |
| --- | --- |
| **shoot only** | §1, §2, stop. Report the paths. No diff, no second run. |
| **baseline first** | the full loop, stopping after step 2 to hand the change back. The user makes it, then continue at step 4. |
| **change already in the tree** | the tree *is* the after state. `git stash` (or check out the base commit) to shoot the before, restore, shoot the after. Confirm the working tree is clean enough to stash before touching it, and restore it even if a capture fails. |
| **branch vs branch** | shoot the current branch, then `git switch <base>` in place (stash first if the tree is dirty), restart the dev server, shoot again, switch back and unstash. Restart matters: HMR does not survive a whole-branch swap cleanly. Get the tree back to where it started even if a capture fails. |
| **noise floor** | two runs, same tree, diff. The number is the harness's floor, not a finding. |
| **config vs config** | same tree, two runs that differ only in flags: `--args`, `--clock`, `--width`, `--theme`, `--motion`. Filenames stay identical, so the pairs line up and the caption names what changed. |
## The loop
1. Capture the baseline **before touching anything**.
2. Capture it a second time and diff the two. That is the noise floor: anything
it reports is what the harness cannot hold still, and no conclusion about the
change may rest on those stories. Cheap on a handful of stories, ~6 min per
32, so on a wide sweep run it over the two or three stories the change is
aimed at instead of all of them.
3. Make the change.
4. Capture again into a third directory.
5. Diff, then read the tinted shot of the largest movers to judge the change.
## 1. One dev server, on a free port
`storybook dev` keys its Vite dep cache off the config dir, so two servers on the
same `-c` serve mismatched prebundles and every story dies with `Invalid hook
call`. Check what is already up first — port 6006 is often another repo's
Storybook, and its `index.json` then indexes the wrong stories:
```bash
for port in 6006 6007; do
curl -s -m 2 "http://localhost:$port/index.json" | head -c 60 && echo " <- $port"
done
```
Start the SigNoz one on a free port, from the repo's own binary so no package
manager shim is in the way:
```bash
cd frontend
nohup ./node_modules/.bin/storybook dev -p 6007 --no-open --quiet \
> "${TMPDIR:-/tmp}/signoz-storybook.log" 2>&1 &
```
It is ready when `curl -s localhost:6007/index.json` returns JSON whose
`entries` hold SigNoz story ids.
## 2. Capture
Playwright is not a frontend dependency. The script finds it in `tests/e2e`
(`pnpm -C tests/e2e install`, `@playwright/test` is enough) or in a global
install, and launches Playwright's own chromium, falling back to an installed
Chrome. Two escape hatches when that is not what a machine has:
```bash
export PLAYWRIGHT_MODULE=/path/to/playwright # a different install
export CHROME_PATH=/path/to/chrome # a specific browser binary
```
Then pick the stories. `--list` prints the selection without shooting anything:
```bash
# every tooltip story of every page
node scripts/story-shots.mjs .story-shots/baseline \
--port 6007 --title Pages/ --name tooltip --theme dark
# a handful of stories by id or by title/name substring, both themes
node scripts/story-shots.mjs .story-shots/baseline \
--port 6007 --stories pages-noz,dashboards/detail --theme dark,light
```
| Flag | Meaning |
| --- | --- |
| `--stories <match>` | id or `Title/Name` substring, repeatable or comma-separated. Omit for every story. |
| `--title <prefix>` | only titles starting with the prefix (`Pages/`, `Components/`) |
| `--name <match>` | only story names containing the match |
| `--theme dark,light` | one pass per theme; omit for the story's own default (dark) |
| `--args <k:v;k2:v2>` | arg overrides, Storybook's own `?args=` syntax, repeatable. A dotted value is dropped by Storybook itself, so map it to a slug inside the story's mocks |
| `--port` | dev server port, or `$SB_PORT` |
| `--width <px>` | the only fixed dimension, default 1680 |
| `--height <px>` | shortest the viewport may be, default 1200 |
| `--max-height <px>` | tallest it may grow to, default 8000 |
| `--grow <what>` | `scrollers` (default) grows the viewport until the page's own scrollers fit, `document` only follows the document height, `none` keeps `--height` |
| `--settle <ms>` | wait after the page goes quiet, default 1500 |
| `--clock <iso\|live>` | wall clock the page reads, passed to the preview as `?storyClock`; `live` unfreezes it |
| `--motion` | keep animations and transitions running (sets the `motion` global to `live`) |
| `--ignore <selector>` | hide matching elements, on top of `[data-shot-ignore]` and `[data-chromatic="ignore"]` |
| `--flat` | write `<out>/<id>.png`, no theme directory |
| `--no-caption` | leave the caption band off the shots |
| `--list` | print the matched stories and exit |
Files land at `<out>/<theme>/<story-id>.png`, next to a `shots.json` recording
what each shot is (id, title, name, theme, `ok`/`busy`, the caption's height in
rows) and how the run was configured (args, clock, width, height, grow, motion,
settle, ignore). Keep the flags identical between the two runs or the diff pairs
nothing.
Every shot carries the caption band described below, so a single screenshot says
what it is on its own. `--no-caption` leaves it off, and so does a machine
without ImageMagick (with a warning). The band never changes the shot's width
(long text wraps rather than widening the canvas) and its height is recorded, so
the diff crops it back off and never reports one caption against another. Two
runs whose captions are different heights still diff to zero. A story that never held still for two
identical frames is logged `busy` instead of `ok` — treat its diff as suspect.
Dark alone is enough while iterating on the harness; add `light` for the run you
report.
## 3. Diff
```bash
node scripts/story-shots-diff.mjs .story-shots/baseline .story-shots/capped .story-shots/diff
```
Prints `<changed pixels> <theme>/<story>.png`, largest first, and writes one
image per pair. Needs ImageMagick for PNG encode/decode (7's `magick`, or 6's
`convert`/`identify`/`montage`); the comparison itself is in the script.
| Flag | Meaning |
| --- | --- |
| `--mode green` | default. The after shot with the changed pixels painted over it, exactly the pixels that changed. What Chromatic shows. |
| `--mode green-parallel` | `previous \| current \| diff` in one image, each tile labelled above it, on a gutter inverted from the theme. The diff tile is the `green` one, so the after shot stays readable underneath. |
| `--mode red` | the after shot faded to 10%, changed pixels in red. A pixelmatch-style diff, easiest to read when the change is a thin edge. |
| `--mode red-parallel` | the same three tiles, with the `red` diff. Best when the change is a thin edge that the unfaded shot would swallow. |
| `--threshold <0..1>` | how far a pixel must move to count. Default 0.063, Chromatic's `diffThreshold`. |
| `--include-aa` | count antialiasing changes too. Off by default, as in Chromatic. |
| `--tint <#rrggbb>` | override the mode's colour. |
| `--no-caption` | drop the caption band. |
### The caption
Both scripts stamp a band on top of what they write: `story-shots.mjs` on each
shot, from the story and the run's own settings; `story-shots-diff.mjs` on each
diff, read out of the two runs' `shots.json`. It carries the story's
`Title/Name`, then its id, theme and `busy` flag, then the settings both runs
shared, each reading `key:value`. Whatever the two runs did **differently** goes
on the side it belongs to: under `previous` and `current` on the parallel tiles,
on two lines of the band otherwise. So a pair that differs only in `--args` says
so on its face, which is what makes several shots of one story tellable apart.
The shots' own bands are cropped off before comparing and before going into the
tiles, so nothing in the output is a diff of a caption. Type size follows the
image width, so it stays readable with the whole image viewed at fit-to-width;
the heading is set in an installed sans and the detail lines in a mono, falling
back to ImageMagick's default when neither is on the machine. Without a manifest
the band falls back to the file path, and a directory of captioned shots whose
`shots.json` is missing has nothing to crop by, so its captions do land in the
diff. Keep `shots.json` next to the shots.
### How the comparison works
Chromatic's own capture and diff run server-side — `chromatic-cli` uploads a
built Storybook and contains no capture or comparison code at all. What is public
is the parameter contract, and the numbers in it say what the comparison is:
`diffThreshold` defaults to `0.063` on a 0-1 scale, which is pixelmatch's
`threshold`, and `diffIncludeAntiAliasing` defaults to false, which is
pixelmatch's `includeAA: false`. So the script implements that comparison:
1. Both PNGs are read as raw RGBA through `magick … RGBA:-`.
2. Per pixel, the squared YIQ distance between the two colours (weights
`0.5053 / 0.299 / 0.1957`), compared against `35215 * threshold²` — 35215 is
the largest distance two 8-bit colours can have. Chroma is included, so a
colour swap at equal brightness still counts.
3. A pixel over the threshold is dropped when it is only antialiasing: it is the
darkest or lightest of its eight neighbours, and the other image has a pixel
around there doing the same job. This is what keeps a subpixel glyph edge from
reading as a change.
4. What survives is painted at full opacity, one output pixel per changed input
pixel. No dilation, no blobs — a one-pixel shift shows as a one-pixel line.
A pair whose shots are different sizes is compared over the overlap, and every
row and column that exists in only one of them counts as changed.
Pairing is by `<theme>/<story-id>.png`, so a story that exists on only one side
(new on the feature branch, renamed, retitled) has nothing to pair with and is
skipped silently. On a branch-vs-branch run, compare the two runs' file lists
before reading the numbers.
## What makes a shot reproducible
Most of it is in the preview, not in the script, so a Chromatic build in the
cloud shoots the same page: `.storybook/preview-head.html` freezes the clock,
and `settleForCapture` (the preview's `afterEach`, which runs after `play`)
parks the animations and snaps the bottom-pinned lists. The script drives the
rest:
- **Storybook's own render phase is the readiness signal.** It waits for
`window.__STORYBOOK_PREVIEW__.storyRenders[].phase === 'finished'`, which is
reached only after the loaders, the decorators and the story's `play` are done.
A DOM check cannot see a `play` still running. (Storybook 10 spells the final
phase `finished`, not `completed`.)
- **Network quiescence, not `networkidle`.** react-query retries and msw keep
requests going after load, and a few stories hang a request by design, so the
wait is "no request for 600ms", capped at 15s.
- **The clock is frozen** (`2026-06-15T12:00:00Z`), by the preview itself. Chart windows, `4 mins ago`
labels and trial countdowns all derive from `now`; a live clock alone moved
8000 pixels on the dashboards list and redrew every chart axis.
- **Animations are parked on their last frame** by `html.sb-still`, a
zero-length single iteration with `forwards` fill, plus `prefers-reduced-
motion`. The Motion toolbar item (`still` by default) turns it off. An infinite
spinner is otherwise caught at a random angle.
- **`document.fonts.ready`**, because text reflows when a face lands late.
- **Lists pinned to their bottom are snapped onto it**, once by the preview and
again by the script after the page goes quiet. A virtuoso list settles a
few pixels short of the end depending on the order its items were measured in.
- **Two identical frames in a row**, because what a page is still waiting on is
often not observable from outside it.
- **`[data-shot-ignore]`, `[data-chromatic="ignore"]` and `--ignore <selector>`**
hide a region that cannot be held still; Chromatic excludes the same attribute
from its comparison.
- **The width is the only fixed dimension.** Chromatic's `viewports` are widths;
the height follows the page. `src/styles.scss` pins `html, body, #root` to
`height: 100%; overflow: hidden`, so the document never outgrows the viewport
and its height says nothing: what overflows are the shell's inner scrollers.
`--grow scrollers`, the default, grows the viewport until the tallest in-flow
scroller fits, so nothing is cut off and no scrollbar is left in the shot (the
dashboards list goes to 2226px in one round). Popups are skipped — they are out
of the flow, and a tall dropdown would otherwise drag the shot to a height
nothing on the page needs. A page that sizes a panel in `vh` grows its own
content as the viewport grows, so no height ever fits it and the rounds only
chase — `.alert-chart-container` is `57vh`, which puts Create Alert's fixed
point at 4344px with an empty band on top. Those pages are shot at `--height`
with their own scrollbar, which is what they look like in a browser, and the
log says `(viewport-sized content, stopped chasing Npx)`.
With all of that, 29 of the 32 page tooltip stories are byte-identical across
runs. The three that are not, and why:
| Story | Residual | Cause |
| --- | --- | --- |
| `kubernetes-pods--tooltips-in-options-panel` | ~13k px | 24 tooltips held open in an overlapping cluster; they portal to `body` in mount order, and the drawer's own tooltips mount before or after the list's depending on when their data lands, so overlapping tooltips stack differently. Panel geometry itself is stable. |
| `settings-role-editor--tooltips-in-json-editor` | ~2.5k px | monaco re-measures and lands one pixel off. |
| `traces-trace-details--tooltips` | ~800 px | same class, one row of the waterfall. |
Each is bimodal — two stable arrangements — so the same number reappears run
after run. Diff a story against itself before believing its number, and reach
for `--ignore` when a region cannot be settled.
## Gotchas
- **Zero pixels is a real answer.** A story whose tooltips are all short is
unaffected by a tooltip rule; it is not a broken capture.
- **The selector matters more than the rule.** A global rule on
`[data-slot='…']` only reaches design-system components. antd's own tooltips
(`.ant-tooltip-inner`, e.g. the Create Alert help popups) are untouched, which
is why some stories show no diff at all.
- **Global style overrides need `!important`.** `src/styles.scss` loads before
the design system injects its CSS-module styles at runtime, so a plain rule on
a `[data-slot='…']` element loses. A component-level `!important` of the same
specificity still wins over it — `PanelStatusPopover.module.scss` keeps its own
`max-width: 520px !important`.
- **A fresh context per story** is why a full sweep takes ~6 min for 32 stories.
Reusing one page loses the msw service worker re-registration race and stories
start failing after a few navigations.
- **Stories behind a hover, drawer or modal** only render what their `play`
reaches. If a state is missing from the shot, the story needs the `play`, not
the script.

4
.gitattributes vendored
View File

@@ -1 +1,3 @@
*.css linguist-detectable=false
*.css linguist-detectable=false
*.stories.mocks.tsx linguist-generated=true
**/__story_mockdata__/** linguist-generated=true

28
.github/CODEOWNERS vendored
View File

@@ -152,39 +152,29 @@ go.mod @therealpandey
## Dashboard Types
/frontend/src/api/types/dashboard/ @SigNoz/pulse-frontend
/frontend/src/types/api/dashboard/ @SigNoz/pulse-frontend
/frontend/src/types/api/widgets/ @SigNoz/pulse-frontend
## Dashboard List
## Widget Card
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
# Dashboard Widget Page
/frontend/src/pages/DashboardWidget/ @SigNoz/pulse-frontend
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
## Dashboard Page
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
/frontend/src/container/WidgetCard/ @SigNoz/pulse-frontend
## Public Dashboard Page
/frontend/src/pages/PublicDashboard/ @SigNoz/pulse-frontend
/frontend/src/container/PublicDashboardContainer/ @SigNoz/pulse-frontend
## Dashboard Libs + Components
/frontend/src/lib/uPlotV2/ @SigNoz/pulse-frontend
/frontend/src/lib/visualization/ @SigNoz/pulse-frontend
/frontend/src/lib/dashboard/ @SigNoz/pulse-frontend
/frontend/src/lib/dashboardVariables/ @SigNoz/pulse-frontend
/frontend/src/components/NewSelect/ @SigNoz/pulse-frontend
## Dashboard V2
/frontend/src/pages/DashboardPageV2/ @SigNoz/pulse-frontend
/frontend/src/pages/DashboardsListPageV2/ @SigNoz/pulse-frontend
## Dashboard Pages
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
## Infrastructure Monitoring
/frontend/src/pages/InfrastructureMonitoring/ @SigNoz/pulse-frontend

View File

@@ -103,7 +103,6 @@ jobs:
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}
-X github.com/SigNoz/signoz/ee/zeus.url=https://api.signoz.cloud
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.signoz.io
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
DOCKER_DOCKERFILE_PATH: ./cmd/enterprise/Dockerfile.multi-arch

View File

@@ -109,7 +109,6 @@ jobs:
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}
-X github.com/SigNoz/signoz/ee/zeus.url=https://api.staging.signoz.cloud
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.staging.signoz.cloud
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.staging.signoz.cloud/api/v1
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
DOCKER_DOCKERFILE_PATH: ./cmd/enterprise/Dockerfile.multi-arch

View File

@@ -50,6 +50,7 @@ jobs:
- logspipelines
- passwordauthn
- preference
- quickfilter
- querierlogs
- queriertraces
- queriermetrics
@@ -58,6 +59,7 @@ jobs:
- querierai
- rawexportdata
- promqlconformance
- promapiconformance
- querierauthz
- role
- rootuser

View File

@@ -96,6 +96,8 @@ func runGenerateAuthz(_ context.Context) error {
coretypes.NewResourceRef(coretypes.ResourceServiceAccount).String(): true,
coretypes.NewResourceRef(coretypes.ResourceRole).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourceFactorAPIKey).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourceLicense).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourceSubscription).String(): true,
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceLogs).String(): true,
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceTraces).String(): true,
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceMetrics).String(): true,

View File

@@ -44,8 +44,11 @@ import (
"github.com/SigNoz/signoz/pkg/ruler/signozruler"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/subscription"
"github.com/SigNoz/signoz/pkg/subscription/noopsubscription"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/version"
"github.com/SigNoz/signoz/pkg/zeus"
@@ -86,6 +89,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(_ sqlstore.SQLStore, _ zeus.Zeus, _ organization.Getter, _ analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
return nooplicensing.NewFactory()
},
func(_ zeus.Zeus, _ licensing.Licensing) subscription.Subscription {
return noopsubscription.New()
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),
signoz.NewWebProviderFactories(config.Global),
@@ -103,8 +109,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, authtypes.NewRegistry()), nil
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing, tagModule tag.Module) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, tagModule)
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing, tagModule tag.Module, systemDashboardRegistry dashboardtypes.SystemDashboardRegistry) dashboard.Module {
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, tagModule, systemDashboardRegistry)
},
func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return noopgateway.NewProviderFactory()

View File

@@ -31,7 +31,6 @@ builds:
- -X github.com/SigNoz/signoz/pkg/version.branch={{ .Branch }}
- -X github.com/SigNoz/signoz/ee/zeus.url=https://api.signoz.cloud
- -X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.signoz.io
- -X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1
- -X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr
mod_timestamp: "{{ .CommitTimestamp }}"
tags:

View File

@@ -28,6 +28,7 @@ import (
eequerier "github.com/SigNoz/signoz/ee/querier"
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
eerules "github.com/SigNoz/signoz/ee/query-service/rules"
"github.com/SigNoz/signoz/ee/subscription/httpsubscription"
enterprisezeus "github.com/SigNoz/signoz/ee/zeus"
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
"github.com/SigNoz/signoz/pkg/alertmanager"
@@ -60,9 +61,11 @@ import (
"github.com/SigNoz/signoz/pkg/ruler/signozruler"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/subscription"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/version"
"github.com/SigNoz/signoz/pkg/zeus"
@@ -102,6 +105,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter, analytics)
},
func(zeus zeus.Zeus, licensing licensing.Licensing) subscription.Subscription {
return httpsubscription.New(zeus, licensing)
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),
signoz.NewWebProviderFactories(config.Global),
@@ -136,8 +142,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
}
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, onBeforeRoleDelete, authtypes.NewRegistry()), nil
},
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing, tagModule)
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module, systemDashboardRegistry dashboardtypes.SystemDashboardRegistry) dashboard.Module {
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing, tagModule, systemDashboardRegistry)
},
func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] {
return httpgateway.NewProviderFactory(licensing)

View File

@@ -397,7 +397,6 @@ identn:
# headers to use for tokenizer identN resolver
headers:
- Authorization
- Sec-WebSocket-Protocol
apikey:
# toggle apikey identN
enabled: true

File diff suppressed because it is too large Load Diff

View File

@@ -299,8 +299,11 @@ substituted. One subtlety makes it exact: we write stale markers at absent
grid points. Without them, the engine's lookback would resurrect a point
from up to `lookback` earlier. The marker encodes "absent here" the way the
engine itself encodes it. Units evaluate concurrently. Each unit is one
series lookup plus one grid statement. A step of 0 is an instant query: a
single evaluation at `end`.
grid statement: the group-key join resolves the matchers, and the samples
primary key takes the metric name straight from the selector. Only a
selector without a static `__name__` runs the series lookup first, to learn
the concrete metric names. A step of 0 is an instant query: a single
evaluation at `end`.
A note on the window sliver: when the window is narrower than the step, the
grid windows cover only `window/step` of the timeline. A sample in a gap
@@ -315,8 +318,9 @@ selectors and `last_over_time` transpile at window < step too.
## Series lookup
Both paths resolve matchers the same way, once per selector
(`selectSeries`). The series tables hold one row per (fingerprint, bucket)
The engine path resolves matchers once per selector (`selectSeries`); the
transpiled path builds the same conditions into its group-key join. Both
read the same tables. The series tables hold one row per (fingerprint, bucket)
at 1h/6h/1d/1w granularities. The shared schema package
(`pkg/telemetryschema/metricstelemetryschema`) picks the table whose bucket
fits the window. It rounds the window start down to the bucket boundary, so

View File

@@ -108,6 +108,20 @@ func (provider *Provider) SearchIngestionKeysByName(ctx context.Context, orgID v
}, nil
}
func (provider *Provider) GetIngestionKey(ctx context.Context, orgID valuer.UUID, keyID string) (*gatewaytypes.IngestionKey, error) {
responseBody, err := provider.do(ctx, orgID, http.MethodGet, "/v1/workspaces/me/keys/"+keyID, nil, nil)
if err != nil {
return nil, err
}
var ingestionKey gatewaytypes.IngestionKey
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "data").String()), &ingestionKey); err != nil {
return nil, err
}
return &ingestionKey, nil
}
func (provider *Provider) CreateIngestionKey(ctx context.Context, orgID valuer.UUID, name string, tags []string, expiresAt time.Time) (*gatewaytypes.GettableCreatedIngestionKey, error) {
requestBody := gatewaytypes.PostableIngestionKey{
Name: name,
@@ -161,7 +175,7 @@ func (provider *Provider) DeleteIngestionKey(ctx context.Context, orgID valuer.U
}
func (provider *Provider) CreateIngestionKeyLimit(ctx context.Context, orgID valuer.UUID, keyID string, signal string, limitConfig gatewaytypes.LimitConfig, tags []string) (*gatewaytypes.GettableCreatedIngestionKeyLimit, error) {
requestBody := gatewaytypes.PostableIngestionKeyLimit{
requestBody := gatewaytypes.DeprecatedPostableIngestionKeyLimit{
Signal: signal,
Config: limitConfig,
Tags: tags,
@@ -184,6 +198,34 @@ func (provider *Provider) CreateIngestionKeyLimit(ctx context.Context, orgID val
return &createdIngestionKeyLimitResponse, nil
}
func (provider *Provider) GetIngestionKeyLimit(ctx context.Context, orgID valuer.UUID, limitID string) (*gatewaytypes.Limit, error) {
responseBody, err := provider.do(ctx, orgID, http.MethodGet, "/v1/workspaces/me/limits/"+limitID, nil, nil)
if err != nil {
return nil, err
}
var limit gatewaytypes.Limit
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "data").String()), &limit); err != nil {
return nil, err
}
return &limit, nil
}
func (provider *Provider) GetIngestionKeyLimits(ctx context.Context, orgID valuer.UUID, keyID string) ([]gatewaytypes.Limit, error) {
responseBody, err := provider.do(ctx, orgID, http.MethodGet, "/v1/workspaces/me/keys/"+keyID+"/limits", nil, nil)
if err != nil {
return nil, err
}
var limits []gatewaytypes.Limit
if err := json.Unmarshal([]byte(gjson.GetBytes(responseBody, "data").String()), &limits); err != nil {
return nil, err
}
return limits, nil
}
func (provider *Provider) UpdateIngestionKeyLimit(ctx context.Context, orgID valuer.UUID, limitID string, limitConfig gatewaytypes.LimitConfig, tags []string) error {
requestBody := gatewaytypes.UpdatableIngestionKeyLimit{
Config: limitConfig,

View File

@@ -1,168 +0,0 @@
package httplicensing
import (
"context"
"encoding/json"
"net/http"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type licensingAPI struct {
licensing licensing.Licensing
}
func NewLicensingAPI(licensing licensing.Licensing) licensing.API {
return &licensingAPI{licensing: licensing}
}
func (api *licensingAPI) Activate(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
req := new(licensetypes.PostableLicense)
err = json.NewDecoder(r.Body).Decode(&req)
if err != nil {
render.Error(rw, err)
return
}
err = api.licensing.Activate(r.Context(), orgID, req.Key)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusAccepted, nil)
}
func (api *licensingAPI) GetActive(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
license, err := api.licensing.GetActive(r.Context(), orgID)
if err != nil {
render.Error(rw, err)
return
}
gettableLicense := licensetypes.NewGettableLicense(license.Data, license.Key)
render.Success(rw, http.StatusOK, gettableLicense)
}
func (api *licensingAPI) Refresh(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
err = api.licensing.Refresh(r.Context(), orgID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (api *licensingAPI) Checkout(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
req := new(licensetypes.PostableSubscription)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
render.Error(rw, err)
return
}
gettableSubscription, err := api.licensing.Checkout(ctx, orgID, req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, gettableSubscription)
}
func (api *licensingAPI) Portal(rw http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
return
}
req := new(licensetypes.PostableSubscription)
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
render.Error(rw, err)
return
}
gettableSubscription, err := api.licensing.Portal(ctx, orgID, req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, gettableSubscription)
}

View File

@@ -2,12 +2,9 @@ package httplicensing
import (
"context"
"encoding/json"
"log/slog"
"time"
"github.com/tidwall/gjson"
"github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/errors"
@@ -95,24 +92,65 @@ func (provider *provider) Validate(ctx context.Context) error {
return nil
}
func (provider *provider) Activate(ctx context.Context, organizationID valuer.UUID, key string) error {
data, err := provider.zeus.GetLicense(ctx, key)
func (provider *provider) Activate(ctx context.Context, organizationID valuer.UUID, key string) (*licensetypes.License, error) {
zeusLicense, err := provider.zeus.GetLicense(ctx, key)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to fetch license data with upstream server")
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to fetch license data with upstream server")
}
license, err := licensetypes.NewLicense(data, organizationID)
license, err := licensetypes.NewLicense(zeusLicense, organizationID)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity")
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity")
}
storableLicense := licensetypes.NewStorableLicenseFromLicense(license)
err = provider.store.Create(ctx, storableLicense)
if err != nil {
return nil, err
}
return license, nil
}
func (provider *provider) Get(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) (*licensetypes.License, error) {
storableLicense, err := provider.store.Get(ctx, organizationID, licenseID)
if err != nil {
return nil, err
}
return licensetypes.NewLicenseFromStorableLicense(storableLicense)
}
func (provider *provider) List(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.License, error) {
storableLicenses, err := provider.store.GetAll(ctx, organizationID)
if err != nil {
return nil, err
}
licenses := make([]*licensetypes.License, 0, len(storableLicenses))
for _, storableLicense := range storableLicenses {
license, err := licensetypes.NewLicenseFromStorableLicense(storableLicense)
if err != nil {
return nil, err
}
licenses = append(licenses, license)
}
return licenses, nil
}
func (provider *provider) Delete(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) error {
license, err := provider.Get(ctx, organizationID, licenseID)
if err != nil {
return err
}
return nil
if err := license.ErrIfCloud(); err != nil {
return errors.WithAdditionalf(err, "license %s cannot be deleted", licenseID.StringValue())
}
return provider.store.Delete(ctx, organizationID, licenseID)
}
func (provider *provider) GetActive(ctx context.Context, organizationID valuer.UUID) (*licensetypes.License, error) {
@@ -139,7 +177,7 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
return err
}
data, err := provider.zeus.GetLicense(ctx, activeLicense.Key)
zeusLicense, err := provider.zeus.GetLicense(ctx, activeLicense.Key)
if err != nil {
if time.Since(activeLicense.LastValidatedAt) > time.Duration(provider.config.FailureThreshold)*provider.config.PollInterval {
activeLicense.UpdateFeatures(licensetypes.BasicPlan)
@@ -154,7 +192,7 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
return err
}
err = activeLicense.Update(data)
err = activeLicense.Update(zeusLicense)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity from license data")
}
@@ -187,47 +225,6 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
return nil
}
func (provider *provider) Checkout(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) {
activeLicense, err := provider.GetActive(ctx, organizationID)
if err != nil {
return nil, err
}
body, err := json.Marshal(postableSubscription)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal checkout payload")
}
response, err := provider.zeus.GetCheckoutURL(ctx, activeLicense.Key, body)
if err != nil {
if errors.Ast(err, errors.TypeAlreadyExists) {
return nil, errors.WithAdditionalf(err, "checkout has already been completed for this account. Please click 'Refresh Status' to sync your subscription")
}
return nil, err
}
return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
}
func (provider *provider) Portal(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) {
activeLicense, err := provider.GetActive(ctx, organizationID)
if err != nil {
return nil, err
}
body, err := json.Marshal(postableSubscription)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal portal payload")
}
response, err := provider.zeus.GetPortalURL(ctx, activeLicense.Key, body)
if err != nil {
return nil, err
}
return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
}
func (provider *provider) GetFeatureFlags(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.Feature, error) {
license, err := provider.GetActive(ctx, organizationID)
if err != nil {

View File

@@ -64,6 +64,22 @@ func (store *store) GetAll(ctx context.Context, organizationID valuer.UUID) ([]*
return storableLicenses, nil
}
func (store *store) Delete(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) error {
_, err := store.
sqlstore.
BunDB().
NewDelete().
Model(new(licensetypes.StorableLicense)).
Where("org_id = ?", organizationID).
Where("id = ?", licenseID).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to delete license with ID: %s", licenseID)
}
return nil
}
func (store *store) Update(ctx context.Context, organizationID valuer.UUID, storableLicense *licensetypes.StorableLicense) error {
_, err := store.
sqlstore.

View File

@@ -32,9 +32,9 @@ type module struct {
tagModule tag.Module
}
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module, systemDashboardRegistry dashboardtypes.SystemDashboardRegistry) dashboard.Module {
scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard")
pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser, tagModule)
pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser, tagModule, systemDashboardRegistry)
return &module{
pkgDashboardModule: pkgDashboardModule,
@@ -361,6 +361,14 @@ func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valu
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, isAdmin, lock)
}
func (module *module) ReconcileSystemDashboards(ctx context.Context, orgID valuer.UUID) error {
return module.pkgDashboardModule.ReconcileSystemDashboards(ctx, orgID)
}
func (module *module) GetSystemDashboard(ctx context.Context, orgID valuer.UUID, name string) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.GetSystemDashboard(ctx, orgID, name)
}
func (module *module) delete(ctx context.Context, orgID, id valuer.UUID) error {
return module.store.RunInTx(ctx, func(ctx context.Context) error {
if err := module.store.DeletePublic(ctx, id.String()); err != nil && !errors.Ast(err, errors.TypeNotFound) {

View File

@@ -4,7 +4,6 @@ import (
"net/http"
"time"
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/middleware"
@@ -42,7 +41,6 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz, config signoz.
IntegrationsController: opts.IntegrationsController,
LogsParsingPipelineController: opts.LogsParsingPipelineController,
FluxInterval: opts.FluxInterval,
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
Signoz: signoz,
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),
}, config)
@@ -72,15 +70,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
// base overrides
router.HandleFunc("/api/v1/version", am.OpenAccess(ah.getVersion)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.LicensingAPI.Checkout)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingAPI.Portal)).Methods(http.MethodPost)
// v3
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Activate)).Methods(http.MethodPost)
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Refresh)).Methods(http.MethodPut)
router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(ah.LicensingAPI.GetActive)).Methods(http.MethodGet)
// v4
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)

View File

@@ -1,122 +0,0 @@
package api
import (
"encoding/json"
"fmt"
"net/http"
"github.com/SigNoz/signoz/ee/query-service/constants"
"github.com/SigNoz/signoz/ee/query-service/model"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type DayWiseBreakdown struct {
Type string `json:"type"`
Breakdown []DayWiseData `json:"breakdown"`
}
type DayWiseData struct {
Timestamp int64 `json:"timestamp"`
Count float64 `json:"count"`
Size float64 `json:"size"`
UnitPrice float64 `json:"unitPrice"`
Quantity float64 `json:"quantity"`
Total float64 `json:"total"`
}
type tierBreakdown struct {
UnitPrice float64 `json:"unitPrice"`
Quantity float64 `json:"quantity"`
TierStart int64 `json:"tierStart"`
TierEnd int64 `json:"tierEnd"`
TierCost float64 `json:"tierCost"`
}
type usageResponse struct {
Type string `json:"type"`
Unit string `json:"unit"`
Tiers []tierBreakdown `json:"tiers"`
DayWiseBreakdown DayWiseBreakdown `json:"dayWiseBreakdown"`
}
type details struct {
Total float64 `json:"total"`
Breakdown []usageResponse `json:"breakdown"`
BaseFee float64 `json:"baseFee"`
BillTotal float64 `json:"billTotal"`
}
type billingData struct {
BillingPeriodStart int64 `json:"billingPeriodStart"`
BillingPeriodEnd int64 `json:"billingPeriodEnd"`
Details details `json:"details"`
Discount float64 `json:"discount"`
SubscriptionStatus string `json:"subscriptionStatus"`
}
type billingDetails struct {
Status string `json:"status"`
Data billingData `json:"data"`
}
func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
licenseKey := r.URL.Query().Get("licenseKey")
if licenseKey == "" {
RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
return
}
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
RespondError(w, model.InternalError(err), nil)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
evalCtx := featuretypes.NewFlaggerEvaluationContext(orgID)
useZeus := ah.Signoz.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureGetMetersFromZeus, evalCtx)
if useZeus {
data, err := ah.Signoz.Zeus.GetMeters(r.Context(), licenseKey)
if err != nil {
RespondError(w, model.InternalError(err), nil)
return
}
var billing billingData
if err := json.Unmarshal(data, &billing); err != nil {
RespondError(w, model.InternalError(err), nil)
return
}
ah.Respond(w, billing)
return
}
billingURL := fmt.Sprintf("%s/usage?licenseKey=%s", constants.LicenseSignozIo, licenseKey)
hClient := &http.Client{}
req, err := http.NewRequest("GET", billingURL, nil)
if err != nil {
RespondError(w, model.InternalError(err), nil)
return
}
req.Header.Add("X-SigNoz-SecretKey", constants.LicenseAPIKey)
billingResp, err := hClient.Do(req)
if err != nil {
RespondError(w, model.InternalError(err), nil)
return
}
var billingResponse billingDetails
if err := json.NewDecoder(billingResp.Body).Decode(&billingResponse); err != nil {
RespondError(w, model.InternalError(err), nil)
return
}
ah.Respond(w, billingResponse.Data)
}

View File

@@ -184,7 +184,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
apiHandler.RegisterIntegrationRoutes(r, am)
apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am)
apiHandler.RegisterWebSocketPaths(r, am)
apiHandler.RegisterMessagingQueuesRoutes(r, am)
apiHandler.RegisterThirdPartyApiRoutes(r, am)
apiHandler.RegisterTraceFunnelsRoutes(r, am)
@@ -197,7 +196,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "DELETE", "POST", "PUT", "PATCH", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "cache-control", "X-SIGNOZ-QUERY-ID", "Sec-WebSocket-Protocol"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "cache-control"},
})
handler := c.Handler(r)

View File

@@ -4,8 +4,6 @@ import (
"os"
)
var LicenseSignozIo = "https://license.signoz.io/api/v1"
var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "")
var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
var FetchFeatures = GetOrDefaultEnv("FETCH_FEATURES", "false")
var ZeusFeaturesURL = GetOrDefaultEnv("ZEUS_FEATURES_URL", "ZeusFeaturesURL")

View File

@@ -0,0 +1,95 @@
package httpsubscription
import (
"context"
"encoding/json"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/subscription"
"github.com/SigNoz/signoz/pkg/types/subscriptiontypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/zeus"
"github.com/tidwall/gjson"
)
const upstreamTimeout = 10 * time.Second
type provider struct {
zeus zeus.Zeus
licensing licensing.Licensing
}
func New(zeus zeus.Zeus, licensing licensing.Licensing) subscription.Subscription {
return &provider{
zeus: zeus,
licensing: licensing,
}
}
func (provider *provider) Create(ctx context.Context, organizationID valuer.UUID, postableSubscription *subscriptiontypes.PostableSubscription) (*subscriptiontypes.GettableSubscription, error) {
ctx, cancel := context.WithTimeout(ctx, upstreamTimeout)
defer cancel()
license, err := provider.licensing.GetActive(ctx, organizationID)
if err != nil {
return nil, err
}
body, err := json.Marshal(postableSubscription)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal subscription payload")
}
response, err := provider.zeus.GetCheckoutURL(ctx, license.Key, body)
if err != nil {
if errors.Ast(err, errors.TypeAlreadyExists) {
return nil, errors.WithAdditionalf(err, "checkout has already been completed for this account. Please click 'Refresh Status' to sync your subscription")
}
return nil, err
}
return &subscriptiontypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
}
func (provider *provider) Update(ctx context.Context, organizationID valuer.UUID, postableSubscription *subscriptiontypes.PostableSubscription) (*subscriptiontypes.GettableSubscription, error) {
ctx, cancel := context.WithTimeout(ctx, upstreamTimeout)
defer cancel()
license, err := provider.licensing.GetActive(ctx, organizationID)
if err != nil {
return nil, err
}
body, err := json.Marshal(postableSubscription)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal subscription payload")
}
response, err := provider.zeus.GetPortalURL(ctx, license.Key, body)
if err != nil {
return nil, err
}
return &subscriptiontypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
}
func (provider *provider) Get(ctx context.Context, organizationID valuer.UUID) (*subscriptiontypes.GettableSubscriptionUsage, error) {
license, err := provider.licensing.GetActive(ctx, organizationID)
if err != nil {
return nil, err
}
data, err := provider.zeus.GetMeters(ctx, license.Key)
if err != nil {
return nil, err
}
usage, err := subscriptiontypes.NewGettableSubscriptionUsage(data)
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, zeus.ErrCodeResponseMalformed, "failed to unmarshal subscription usage")
}
return usage, nil
}

View File

@@ -51,7 +51,7 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
}, nil
}
func (provider *Provider) GetLicense(ctx context.Context, key string) ([]byte, error) {
func (provider *Provider) GetLicense(ctx context.Context, key string) (*zeustypes.License, error) {
response, err := provider.do(
ctx,
provider.config.URL.JoinPath("/v2/licenses/me"),
@@ -63,7 +63,12 @@ func (provider *Provider) GetLicense(ctx context.Context, key string) ([]byte, e
return nil, err
}
return []byte(gjson.GetBytes(response, "data").String()), nil
license := new(zeustypes.License)
if err := json.Unmarshal([]byte(gjson.GetBytes(response, "data").String()), license); err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, zeus.ErrCodeResponseMalformed, "failed to unmarshal license data")
}
return license, nil
}
func (provider *Provider) GetCheckoutURL(ctx context.Context, key string, body []byte) ([]byte, error) {

9
frontend/.gitignore vendored
View File

@@ -28,4 +28,11 @@ e2e/test-plan/saved-views/
e2e/test-plan/service-map/
e2e/test-plan/services/
e2e/test-plan/traces/
e2e/test-plan/user-preferences/
e2e/test-plan/user-preferences/
# Storybook
/storybook-static/
debug-storybook.log
# Storybook screenshot sweeps (scripts/story-shots.mjs)
/.story-shots/

View File

@@ -323,9 +323,10 @@
"name": "react",
"importNames": [
"createContext",
"useContext"
"useContext",
"useSyncExternalStore"
],
"message": "[State mgmt] React Context is deprecated. Migrate shared state to Zustand."
"message": "[State mgmt] React Context and hand-rolled external stores are deprecated. Migrate shared state to Zustand."
},
{
"name": "immer",
@@ -565,16 +566,27 @@
}
},
{
// Root V2 pages own the dashboard fetch lifecycle; useDashboardFetchRequired wraps it.
// Root dashboard pages own the fetch lifecycle; useDashboardFetchRequired wraps it.
// Everywhere else must use useDashboardFetchRequired().
"files": [
"src/pages/DashboardPageV2/DashboardPageV2.tsx",
"src/pages/DashboardPageV2/PanelEditorPage/PanelEditorPage.tsx",
"src/pages/DashboardPageV2/DashboardContainer/hooks/useDashboardFetchRequired.ts"
"src/pages/DashboardPage/DashboardPage.tsx",
"src/pages/DashboardPage/PanelEditorPage/PanelEditorPage.tsx",
"src/pages/DashboardPage/DashboardContainer/hooks/useDashboardFetchRequired.ts"
],
"rules": {
"signoz/no-dashboard-fetch-outside-root": "off"
}
},
{
// Dev-tooling CLIs: stdout is their output, and they carry ported pixel/heap
// algorithms that read worse when split up.
"files": [
"scripts/**"
],
"rules": {
"no-console": "off",
"sonarjs/cognitive-complexity": "off"
}
}
]
}

View File

@@ -0,0 +1,89 @@
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
import type { StorybookConfig } from '@storybook/react-vite';
import type { Plugin, PluginOption } from 'vite';
const srcPath = resolve(dirname(fileURLToPath(import.meta.url)), '../src');
/**
* Modules replaced for every story. Same idea as `moduleNameMapper` in
* `jest.config.ts`: the app keeps importing its own paths, Storybook resolves
* them to a mock. Regexes so only exact specifiers match: `lib/history` must
* not catch `lib/historyUtils`.
*
* Each replacement is typed as the module it stands in for, so drift is a
* compile error rather than a story that fails at render. The `jest` note on
* each entry is where the same import lands under the other runner. The two
* only diverge where the runner needs them to.
*/
const mockAliases = [
{
// jest: not replaced, jsdom drives a real browser history.
find: /^(?:src\/)?lib\/history$/,
replacement: `${srcPath}/storybook/navigation/history.alias.ts`,
},
{
// jest: src/__tests__/logEventMock.ts
find: /^(?:src\/)?api\/common\/logEvent$/,
replacement: `${srcPath}/storybook/mocks/logEvent.mock.ts`,
},
{
// jest: __mocks__/env.ts, which leaves `baseURL` empty because jsdom already
// resolves a relative `/api/...` against `http://localhost`.
find: /^(?:src\/)?constants\/env$/,
replacement: `${srcPath}/storybook/mocks/env.mock.ts`,
},
];
/**
* Plugins from `vite.config.ts` that either target the app's `index.html` or
* only pay off in a production build.
*/
const EXCLUDED_PLUGINS = [
'vite-plugin-checker',
'dev-base-path',
'dev-boot-data',
'vite-plugin-image-optimizer',
'vite-plugin-compression',
];
const isExcluded = (plugin: PluginOption): boolean =>
!!plugin &&
typeof plugin === 'object' &&
'name' in plugin &&
EXCLUDED_PLUGINS.includes((plugin as Plugin).name);
const config: StorybookConfig = {
framework: '@storybook/react-vite',
stories: ['../src/**/*.stories.@(ts|tsx)'],
// `../public` carries the fonts, icons and i18n bundles the app expects at
// the root; `./public` carries the msw worker, which must not ship in a
// production build.
staticDirs: ['../public', './public'],
addons: ['@storybook/addon-a11y'],
core: { disableTelemetry: true },
viteFinal: async (viteConfig) => {
const plugins = (viteConfig.plugins ?? [])
.flat(Infinity as 1)
.filter((plugin) => !isExcluded(plugin as PluginOption));
const existingAlias = viteConfig.resolve?.alias;
const normalizedAlias = Array.isArray(existingAlias)
? existingAlias
: Object.entries(existingAlias ?? {}).map(([find, replacement]) => ({
find,
replacement: replacement as string,
}));
return {
...viteConfig,
plugins,
resolve: {
...viteConfig.resolve,
alias: [...mockAliases, ...normalizedAlias],
},
};
},
};
export default config;

View File

@@ -0,0 +1,10 @@
/**
* Chromatic modes: one snapshot per entry, per story. The globals in a mode are
* Storybook's own, so `theme` is the toolbar's theme and the story renders the
* way it does locally. The width matches `scripts/story-shots.mjs` (`--width`),
* so a cloud snapshot and a local shot frame the same page.
*/
export const allModes = {
dark: { theme: 'dark', viewport: { width: 1680, height: 1200 } },
light: { theme: 'light', viewport: { width: 1680, height: 1200 } },
} as const;

View File

@@ -0,0 +1,24 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="css/uPlot.min.css" />
<script>
// i18n's language detector would pick the browser locale (e.g. `en-US`), which
// /public/locales does not ship; pin it to the bundled language instead.
window.localStorage.setItem('i18nextLng', 'en');
// The Go backend injects this at boot; every integration it enables is off in
// Storybook so no third-party script loads inside the iframe.
window.signozBootData = {
settings: {
posthog: { enabled: false, apiHost: '', key: '', uiHost: '' },
appcues: { enabled: false, appId: '' },
sentry: { enabled: false, dsn: '', tunnel: '' },
pylon: { enabled: false, appId: '', identitySecret: '' },
},
};
</script>

View File

@@ -0,0 +1,133 @@
import type { Preview } from '@storybook/react-vite';
import type { SetupWorker } from 'msw';
import { setupWorker } from 'msw';
import { settleForCapture } from '../src/storybook/visual/settleForCapture';
import { withProviders } from '../src/storybook/decorators/withProviders';
import { globalMocks } from '../src/storybook/globals';
import { resetStoryHistory } from '../src/storybook/navigation/containment';
import { clearBlockedNavigations } from '../src/storybook/navigation/blockedNavigationStore';
import {
resolveStory,
type StoryRuntimeContext,
} from '../src/storybook/runtime/resolveStory';
import { allModes } from './modes';
import '../src/ReactI18';
import '../src/styles.scss';
import '../src/storybook/storybook-root.scss';
interface StorybookWorkerHolder {
__signozStorybookWorker?: StorybookWorker;
}
const holder = window as unknown as StorybookWorkerHolder;
/**
* One worker per page, even if this module is re-executed by HMR. Two live
* workers both answer the service worker and the story gets whichever replies
* first.
*/
interface StorybookWorker {
worker: SetupWorker;
ready: Promise<unknown>;
}
const { worker, ready } = (holder.__signozStorybookWorker ??=
((): StorybookWorker => {
const instance = setupWorker();
return {
worker: instance,
ready: instance.start({
serviceWorker: { url: './mockServiceWorker.js' },
// Storybook's own traffic (index.json, HMR, telemetry) goes unhandled by
// design; only flag the app's API calls so a missing handler is obvious.
onUnhandledRequest: (request, print): void => {
const url = new URL(request.url.href);
const isStaticAsset =
/\.(?:woff2?|ttf|otf|css|js|map|png|jpe?g|svg|webp|ico)$/.test(
url.pathname,
);
const isAppRequest =
!isStaticAsset &&
(url.pathname.startsWith('/api/') || url.host !== window.location.host);
if (isAppRequest) {
print.warning();
}
},
}),
};
})());
const preview: Preview = {
parameters: {
layout: 'fullscreen',
controls: { expanded: true },
// One cloud snapshot per theme, for every story. A mode carries Storybook
// globals, so `theme` here is the same toolbar global the app reads out of
// localStorage. Widths are Chromatic's only real dimension, as they are
// locally: the app shell sizes itself to the viewport, so the height is the
// one it is given.
chromatic: { modes: allModes },
},
globalTypes: {
theme: {
description: 'SigNoz color scheme',
toolbar: {
title: 'Theme',
icon: 'paintbrush',
items: [
{ value: 'dark', title: 'Dark' },
{ value: 'light', title: 'Light' },
],
dynamicTitle: true,
},
},
motion: {
description:
'Park every animation on its last frame once the story has settled. Still is what both capture stacks shoot; Live is for watching a transition.',
toolbar: {
title: 'Motion',
icon: 'play',
items: [
{ value: 'still', title: 'Still' },
{ value: 'live', title: 'Live' },
],
dynamicTitle: true,
},
},
},
initialGlobals: { theme: 'dark', motion: 'still' },
// Controls every story carries: permissions, banners, and whether the page's
// own endpoints answer, hang or fail.
args: globalMocks.args,
argTypes: globalMocks.argTypes,
decorators: [withProviders],
loaders: [
// Runs on every render, args changes included, and ahead of the decorators:
// the whole story world is put in place here, so the provider tree only has
// to read it. Re-registering the handlers per render also means an edit to a
// handler module takes effect on the next render instead of leaving the
// worker on the set it was created with.
async (context): Promise<void> => {
const world = resolveStory(context as unknown as StoryRuntimeContext);
world.apply();
world.install(worker);
await ready;
},
],
beforeEach: () => {
clearBlockedNavigations();
resetStoryHistory();
},
// After `play`, which is the moment both capture stacks shoot at.
afterEach: settleForCapture,
};
export default preview;

View File

@@ -0,0 +1,303 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker (1.3.2).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70'
const activeClientIds = new Set()
self.addEventListener('install', function () {
self.skipWaiting()
})
self.addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim())
})
self.addEventListener('message', async function (event) {
const clientId = event.source.id
if (!clientId || !self.clients) {
return
}
const client = await self.clients.get(clientId)
if (!client) {
return
}
const allClients = await self.clients.matchAll({
type: 'window',
})
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: INTEGRITY_CHECKSUM,
})
break
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId)
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: true,
})
break
}
case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId)
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}
break
}
}
})
self.addEventListener('fetch', function (event) {
const { request } = event
const accept = request.headers.get('accept') || ''
// Bypass server-sent events.
if (accept.includes('text/event-stream')) {
return
}
// Bypass navigation requests.
if (request.mode === 'navigate') {
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}
// Generate unique request ID.
const requestId = Math.random().toString(16).slice(2)
event.respondWith(
handleRequest(event, requestId).catch((error) => {
if (error.name === 'NetworkError') {
console.warn(
'[MSW] Successfully emulated a network error for the "%s %s" request.',
request.method,
request.url,
)
return
}
// At this point, any exception indicates an issue with the original request/response.
console.error(
`\
[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
request.method,
request.url,
`${error.name}: ${error.message}`,
)
}),
)
})
async function handleRequest(event, requestId) {
const client = await resolveMainClient(event)
const response = await getResponse(event, client, requestId)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
;(async function () {
const clonedResponse = response.clone()
sendToClient(client, {
type: 'RESPONSE',
payload: {
requestId,
type: clonedResponse.type,
ok: clonedResponse.ok,
status: clonedResponse.status,
statusText: clonedResponse.statusText,
body:
clonedResponse.body === null ? null : await clonedResponse.text(),
headers: Object.fromEntries(clonedResponse.headers.entries()),
redirected: clonedResponse.redirected,
},
})
})()
}
return response
}
// Resolve the main client for the given event.
// Client that issues a request doesn't necessarily equal the client
// that registered the worker. It's with the latter the worker should
// communicate with during the response resolving phase.
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)
if (client?.frameType === 'top-level') {
return client
}
const allClients = await self.clients.matchAll({
type: 'window',
})
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id)
})
}
async function getResponse(event, client, requestId) {
const { request } = event
const clonedRequest = request.clone()
function passthrough() {
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const headers = Object.fromEntries(clonedRequest.headers.entries())
// Remove MSW-specific request headers so the bypassed requests
// comply with the server's CORS preflight check.
// Operate with the headers as an object because request "Headers"
// are immutable.
delete headers['x-msw-bypass']
return fetch(clonedRequest, { headers })
}
// Bypass mocking when the client is not active.
if (!client) {
return passthrough()
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return passthrough()
}
// Bypass requests with the explicit bypass header.
// Such requests can be issued by "ctx.fetch()".
if (request.headers.get('x-msw-bypass') === 'true') {
return passthrough()
}
// Notify the client that a request has been intercepted.
const clientMessage = await sendToClient(client, {
type: 'REQUEST',
payload: {
id: requestId,
url: request.url,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
mode: request.mode,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: await request.text(),
bodyUsed: request.bodyUsed,
keepalive: request.keepalive,
},
})
switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data)
}
case 'MOCK_NOT_FOUND': {
return passthrough()
}
case 'NETWORK_ERROR': {
const { name, message } = clientMessage.data
const networkError = new Error(message)
networkError.name = name
// Rejecting a "respondWith" promise emulates a network error.
throw networkError
}
}
return passthrough()
}
function sendToClient(client, message) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error)
}
resolve(event.data)
}
client.postMessage(message, [channel.port2])
})
}
function sleep(timeMs) {
return new Promise((resolve) => {
setTimeout(resolve, timeMs)
})
}
async function respondWithMock(response) {
await sleep(response.delay)
return new Response(response.body, response)
}

View File

@@ -88,6 +88,17 @@ pnpm test
pnpm tsgo --noEmit
```
## Storybook
```bash
pnpm storybook
```
Opens [http://localhost:6006](http://localhost:6006). Pages run against msw
mocks with no backend; query-param navigation works inside a story, leaving the
page is blocked. See [`src/storybook/README.md`](src/storybook/README.md) for the
override surface.
## Linting
```bash

View File

@@ -7,6 +7,8 @@
"preinstall": "npx only-allow pnpm",
"i18n:generate-hash": "node ./i18-generate-hash.cjs",
"dev": "vite",
"storybook": "storybook dev -p 6006",
"storybook:build": "storybook build -o storybook-static",
"build": "vite build",
"preview": "vite preview",
"prettify": "oxfmt",
@@ -158,6 +160,9 @@
"@commitlint/config-conventional": "20.4.4",
"@jest/globals": "30.4.1",
"@jest/types": "30.2.0",
"@storybook/addon-a11y": "10.5.9",
"@storybook/react-vite": "10.5.9",
"@testing-library/dom": "8.20.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0",
"@testing-library/user-event": "14.4.3",
@@ -203,6 +208,7 @@
"redux-mock-store": "1.5.4",
"sass": "1.97.3",
"sharp": "0.35.0",
"storybook": "10.5.9",
"stylelint": "17.7.0",
"svgo": "4.0.2",
"ts-jest": "29.4.9",

1049
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,6 +26,75 @@
"tooltip_ms_teams_url": "The URL of the Microsoft Teams [webhook](https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498) to send alerts to. Learn more about Microsoft Teams integration in the docs [here](https://signoz.io/docs/alerts-management/notification-channel/ms-teams/).",
"tooltip_google_chat_url": "The URL of the Google Chat space [incoming webhook](https://developers.google.com/workspace/chat/quickstart/webhooks) to send alerts to. It must be an https URL on chat.googleapis.com.",
"google_chat_webhook_url_invalid": "Webhook URL must be an https URL on chat.googleapis.com",
"field_jira_site": "Site URL",
"tooltip_jira_site": "Your Jira Cloud base URL, e.g. https://your-domain.atlassian.net. Only Jira Cloud is supported.",
"jira_site_invalid": "Site URL must be an https URL on an atlassian.net domain",
"jira_required_fields": "Site URL, email, API token, project and issue type are required",
"jira_service_account_tip": "Recommended: use a Jira service account so alerts aren't reported under a personal name and the channel keeps working when someone leaves.",
"jira_service_account_tip_link": "Learn how",
"field_jira_email": "Email",
"help_jira_email": "The Atlassian account email used for authentication.",
"field_jira_api_token": "API token",
"help_jira_api_token": "Create one at id.atlassian.com under Security → API tokens.",
"field_jira_project": "Project key",
"field_jira_issue_type": "Issue type",
"help_jira_issue_type": "An issue type that exists in the project, e.g. Task, Bug or Incident.",
"field_jira_summary": "Summary (issue title)",
"help_jira_summary": "Template for the Jira issue title.",
"field_jira_description": "Description",
"help_jira_description": "Template for the issue description. Rendered as rich text with a status panel and links back to SigNoz.",
"jira_advanced_section": "Advanced Options",
"field_jira_priority": "Priority",
"placeholder_jira_priority": "Leave empty to use the project default",
"help_jira_priority": "Must match a priority in the project's scheme, e.g. High.",
"field_jira_labels": "Labels",
"placeholder_jira_labels": "Type a label and press Enter",
"help_jira_labels": "signoz and a deduplication label are added automatically.",
"field_jira_resolve_transition": "Resolve transition",
"field_jira_reopen_transition": "Reopen transition",
"help_jira_resolve_transition": "When the alert resolves, SigNoz moves the Jira issue to a \"Done\" status via a workflow transition. This is auto-detected — leave it empty unless your project has more than one \"Done\" transition (e.g. Done vs. Won't Do) and you want to force a specific one by name.",
"help_jira_reopen_transition": "When a resolved alert fires again (within the reopen window), SigNoz moves the issue back out of \"Done\" to an active status via a workflow transition. This is auto-detected — leave it empty unless you want to force a specific one by name (e.g. To Do or Reopen).",
"placeholder_jira_resolve_transition": "Auto-detected, e.g. Done",
"placeholder_jira_reopen_transition": "Auto-detected, e.g. To Do",
"field_jira_wont_fix_resolution": "Won't-fix resolution",
"help_jira_wont_fix_resolution": "If the issue was resolved with this resolution (e.g. Won't Do), SigNoz won't reopen it when the alert fires again; a new issue is created instead. Leave empty to always reopen.",
"placeholder_jira_wont_fix_resolution": "e.g. Won't Do",
"field_jira_reopen_duration": "Reopen window",
"placeholder_jira_reopen_duration": "e.g. 72h",
"help_jira_reopen_duration": "If a resolved alert fires again within this window, the same ticket is reopened; after the window, a re-fire opens a new ticket instead. Default: 3d.",
"tooltip_jira_reopen_duration": "Accepted units: m (minutes), h (hours), d (days), w (weeks), y (years) — e.g. 30m, 72h or 3d. Minimum 1m.",
"jira_reopen_duration_invalid": "Reopen window must be a duration like 30m, 72h or 3d (minimum 1m)",
"jsmops_tip": "Create an API integration on your JSM team's Operations page and paste its key below.",
"jsmops_tip_link": "Learn how",
"field_jsmops_api_key": "API key",
"help_jsmops_api_key": "The JSM Ops integration API key, from your team's Operations → Integrations → API. Make sure the integration is turned on.",
"field_jsmops_message": "Message (alert title)",
"help_jsmops_message": "Template for the alert title. Truncated to 130 characters.",
"field_jsmops_description": "Description",
"help_jsmops_description": "Template for the alert description. Rendered as rich text; kept under 15,000 characters.",
"jsmops_advanced_section": "Advanced Options",
"field_jsmops_priority": "Priority",
"help_jsmops_priority": "Template resolving to one of P1P5. Leave as-is to map from alert severity.",
"field_jsmops_tags": "Tags",
"placeholder_jsmops_tags": "Type a tag and press Enter",
"help_jsmops_tags": "Tags added to every alert.",
"incidentio_tip": "Create an HTTP alert source in incident.io (On-call \u2192 Alert routing \u2192 Sources) and paste its URL and token below.",
"incidentio_tip_link": "Learn how",
"field_incidentio_url": "Alert source URL",
"help_incidentio_url": "The alert events URL from the source's setup page, e.g. https://api.incident.io/v2/alert_events/http/<source_config_id>.",
"field_incidentio_token": "Token",
"help_incidentio_token": "The alert source's secret token, from the same setup page.",
"field_incidentio_title": "Title",
"help_incidentio_title": "Template for the alert title. Kept stable while the alert fires \u2014 incident.io ignores content updates on repeat events.",
"field_incidentio_description": "Description",
"help_incidentio_description": "Template for the alert description. Markdown, rendered natively by incident.io.",
"incidentio_required_fields": "Alert source URL and token are required",
"incidentio_url_invalid": "URL must be an incident.io alert events URL (https://api.incident.io/v2/alert_events/http/<source_config_id>)",
"field_incidentio_metadata": "Additional metadata",
"help_incidentio_metadata": "Key-value pairs added to every alert's metadata, on top of the alert's labels (these win on a key clash). Values may use templates, e.g. {{ .CommonLabels.severity }}.",
"placeholder_incidentio_metadata_key": "Key",
"placeholder_incidentio_metadata_value": "Value",
"button_incidentio_add_metadata": "Add metadata",
"field_slack_recipient": "Recipient",
"field_slack_title": "Title",

View File

@@ -26,6 +26,75 @@
"tooltip_ms_teams_url": "The URL of the Microsoft Teams [webhook](https://support.microsoft.com/en-us/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498) to send alerts to. Learn more about Microsoft Teams integration in the docs [here](https://signoz.io/docs/alerts-management/notification-channel/ms-teams/).",
"tooltip_google_chat_url": "The URL of the Google Chat space [incoming webhook](https://developers.google.com/workspace/chat/quickstart/webhooks) to send alerts to. It must be an https URL on chat.googleapis.com.",
"google_chat_webhook_url_invalid": "Webhook URL must be an https URL on chat.googleapis.com",
"field_jira_site": "Site URL",
"tooltip_jira_site": "Your Jira Cloud base URL, e.g. https://your-domain.atlassian.net. Only Jira Cloud is supported.",
"jira_site_invalid": "Site URL must be an https URL on an atlassian.net domain",
"jira_required_fields": "Site URL, email, API token, project and issue type are required",
"jira_service_account_tip": "Recommended: use a Jira service account so alerts aren't reported under a personal name and the channel keeps working when someone leaves.",
"jira_service_account_tip_link": "Learn how",
"field_jira_email": "Email",
"help_jira_email": "The Atlassian account email used for authentication.",
"field_jira_api_token": "API token",
"help_jira_api_token": "Create one at id.atlassian.com under Security → API tokens.",
"field_jira_project": "Project key",
"field_jira_issue_type": "Issue type",
"help_jira_issue_type": "An issue type that exists in the project, e.g. Task, Bug or Incident.",
"field_jira_summary": "Summary (issue title)",
"help_jira_summary": "Template for the Jira issue title.",
"field_jira_description": "Description",
"help_jira_description": "Template for the issue description. Rendered as rich text with a status panel and links back to SigNoz.",
"jira_advanced_section": "Advanced Options",
"field_jira_priority": "Priority",
"placeholder_jira_priority": "Leave empty to use the project default",
"help_jira_priority": "Must match a priority in the project's scheme, e.g. High.",
"field_jira_labels": "Labels",
"placeholder_jira_labels": "Type a label and press Enter",
"help_jira_labels": "signoz and a deduplication label are added automatically.",
"field_jira_resolve_transition": "Resolve transition",
"field_jira_reopen_transition": "Reopen transition",
"help_jira_resolve_transition": "When the alert resolves, SigNoz moves the Jira issue to a \"Done\" status via a workflow transition. This is auto-detected — leave it empty unless your project has more than one \"Done\" transition (e.g. Done vs. Won't Do) and you want to force a specific one by name.",
"help_jira_reopen_transition": "When a resolved alert fires again (within the reopen window), SigNoz moves the issue back out of \"Done\" to an active status via a workflow transition. This is auto-detected — leave it empty unless you want to force a specific one by name (e.g. To Do or Reopen).",
"placeholder_jira_resolve_transition": "Auto-detected, e.g. Done",
"placeholder_jira_reopen_transition": "Auto-detected, e.g. To Do",
"field_jira_wont_fix_resolution": "Won't-fix resolution",
"help_jira_wont_fix_resolution": "If the issue was resolved with this resolution (e.g. Won't Do), SigNoz won't reopen it when the alert fires again; a new issue is created instead. Leave empty to always reopen.",
"placeholder_jira_wont_fix_resolution": "e.g. Won't Do",
"field_jira_reopen_duration": "Reopen window",
"placeholder_jira_reopen_duration": "e.g. 72h",
"help_jira_reopen_duration": "If a resolved alert fires again within this window, the same ticket is reopened; after the window, a re-fire opens a new ticket instead. Default: 3d.",
"tooltip_jira_reopen_duration": "Accepted units: m (minutes), h (hours), d (days), w (weeks), y (years) — e.g. 30m, 72h or 3d. Minimum 1m.",
"jira_reopen_duration_invalid": "Reopen window must be a duration like 30m, 72h or 3d (minimum 1m)",
"jsmops_tip": "Create an API integration on your JSM team's Operations page and paste its key below.",
"jsmops_tip_link": "Learn how",
"field_jsmops_api_key": "API key",
"help_jsmops_api_key": "The JSM Ops integration API key, from your team's Operations → Integrations → API. Make sure the integration is turned on.",
"field_jsmops_message": "Message (alert title)",
"help_jsmops_message": "Template for the alert title. Truncated to 130 characters.",
"field_jsmops_description": "Description",
"help_jsmops_description": "Template for the alert description. Rendered as rich text; kept under 15,000 characters.",
"jsmops_advanced_section": "Advanced Options",
"field_jsmops_priority": "Priority",
"help_jsmops_priority": "Template resolving to one of P1P5. Leave as-is to map from alert severity.",
"field_jsmops_tags": "Tags",
"placeholder_jsmops_tags": "Type a tag and press Enter",
"help_jsmops_tags": "Tags added to every alert.",
"incidentio_tip": "Create an HTTP alert source in incident.io (On-call \u2192 Alert routing \u2192 Sources) and paste its URL and token below.",
"incidentio_tip_link": "Learn how",
"field_incidentio_url": "Alert source URL",
"help_incidentio_url": "The alert events URL from the source's setup page, e.g. https://api.incident.io/v2/alert_events/http/<source_config_id>.",
"field_incidentio_token": "Token",
"help_incidentio_token": "The alert source's secret token, from the same setup page.",
"field_incidentio_title": "Title",
"help_incidentio_title": "Template for the alert title. Kept stable while the alert fires \u2014 incident.io ignores content updates on repeat events.",
"field_incidentio_description": "Description",
"help_incidentio_description": "Template for the alert description. Markdown, rendered natively by incident.io.",
"incidentio_required_fields": "Alert source URL and token are required",
"incidentio_url_invalid": "URL must be an incident.io alert events URL (https://api.incident.io/v2/alert_events/http/<source_config_id>)",
"field_incidentio_metadata": "Additional metadata",
"help_incidentio_metadata": "Key-value pairs added to every alert's metadata, on top of the alert's labels (these win on a key clash). Values may use templates, e.g. {{ .CommonLabels.severity }}.",
"placeholder_incidentio_metadata_key": "Key",
"placeholder_incidentio_metadata_value": "Value",
"button_incidentio_add_metadata": "Add metadata",
"field_slack_recipient": "Recipient",
"field_slack_title": "Title",
"field_slack_description": "Description",

View File

@@ -0,0 +1,167 @@
import { spawnSync } from 'node:child_process';
/**
* The caption band both story-shots.mjs and story-shots-diff.mjs stamp on their
* output, and the ImageMagick plumbing under it. A shot records the band's
* height in `shots.json` so the diff can crop it back off before comparing:
* otherwise two runs whose captions differ would report the caption as a change.
*/
export const CONFIG_KEYS = [
'args',
'clock',
'width',
'height',
'grow',
'motion',
'settle',
'ignore',
];
let tools;
const detect = () =>
(tools ??= {
seven: spawnSync('magick', ['-version']).status === 0,
six: spawnSync('convert', ['-version']).status === 0,
});
export const hasMagick = () => {
const { seven, six } = detect();
return seven || six;
};
export const requireMagick = () => {
if (hasMagick()) {
return;
}
console.error(
'ImageMagick not found. Install it (brew install imagemagick, apt install imagemagick).',
);
process.exit(1);
};
export const magick = (args, input) => {
// ImageMagick 6 has no `magick`: its tools are separate binaries.
const [command, ...rest] = detect().seven
? ['magick', ...args]
: ['identify', 'montage'].includes(args[0])
? args
: ['convert', ...args];
const result = spawnSync(command, rest, {
input,
maxBuffer: 1024 * 1024 * 1024,
});
if (result.status !== 0) {
throw new Error(`${command} ${rest.join(' ')}: ${result.stderr}`);
}
return result.stdout;
};
/**
* ImageMagick's built-in default is a serif that reads as a book, not as a
* screenshot label, so the band asks for what is installed: a sans for the
* heading, a mono for the lines that carry ids, args and numbers. An
* unrecognised name is fatal to `convert`, hence the check against the list it
* reports; a machine with none of them keeps the default.
*/
const FONTS = {
heading: [
'Helvetica-Bold',
'DejaVu-Sans-Bold',
'Liberation-Sans-Bold',
'Arial-Bold',
'Noto-Sans-Bold',
'DejaVu-Sans',
'Liberation-Sans',
],
body: [
'Menlo',
'DejaVu-Sans-Mono',
'Liberation-Mono',
'JetBrainsMono-NF-Regular',
'Courier',
],
};
let installed;
const fontArgs = (role) => {
installed ??= new Set(
[
...magick(['-list', 'font'])
.toString()
.matchAll(/^\s*Font:\s*(\S+)/gm),
].map(([, name]) => name),
);
const font = FONTS[role].find((name) => installed.has(name));
return font ? ['-font', font] : [];
};
/** Readable at fit-to-width, whatever the image is. */
export const pointsize = (width) =>
Math.min(Math.max(Math.round(width / 45), 24), 140);
// `label:` expands ImageMagick's own escapes and reads a file when the text
// starts with @, so story names and arg values go through neither.
export const bodyFont = () => fontArgs('body');
export const literal = (text) => text.replaceAll('%', '%%').replace(/^@/, ' @');
/** The gutter is the opposite of the theme, so the band keeps an edge. */
export const palette = (theme) =>
theme === 'light'
? { background: '#101014', foreground: '#f4f4f5' }
: { background: '#f4f4f5', foreground: '#101014' };
export const settingsLine = (config, keys = CONFIG_KEYS) =>
keys
.filter((key) => config?.[key])
.map((key) => `${key}:${config[key]}`)
.join(' ');
const heightOf = (file) => Number(magick(['identify', '-format', '%h', file]));
/**
* Writes `from` to `to` with `lines` above it, and returns how many rows that
* added — which is what a reader has to crop off to get the original back, so
* the band must never change the width. Each line is a `caption:` at the
* image's own width, wrapping instead of widening the canvas: a run whose
* caption is longer must still produce a shot the next run's shot pairs with.
* Type size follows the width, since a three-tile montage of 1680px shots is
* over 5000px wide and is read at fit-to-width.
*/
export const stamp = ({ lines, from, to, theme }) => {
const { background, foreground } = palette(theme);
const width = Number(magick(['identify', '-format', '%w', from]));
const heading = pointsize(width);
const before = heightOf(from);
const spacer = [
'-size',
`${width}x${Math.round(heading * 0.4)}`,
`xc:${background}`,
];
magick([
'-background',
background,
'-fill',
foreground,
'-gravity',
'center',
...spacer,
...lines.flatMap((line, index) => [
...fontArgs(index ? 'body' : 'heading'),
'-size',
`${width}x`,
'-pointsize',
String(index ? Math.round(heading * 0.62) : heading),
`caption:${literal(line)}`,
]),
...spacer,
from,
'-append',
to,
]);
return heightOf(to) - before;
};

View File

@@ -0,0 +1,460 @@
#!/usr/bin/env node
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
import { parseArgs } from 'node:util';
import path from 'node:path';
import os from 'node:os';
import {
bodyFont,
CONFIG_KEYS,
literal,
magick,
palette,
pointsize,
requireMagick,
settingsLine,
stamp,
} from './story-shots-caption.mjs';
/**
* Pairs the PNGs of two story-shots.mjs runs by relative path and reports what
* moved, per pair, largest first.
*
* The comparison is Chromatic's: a pixel counts as changed when its YIQ
* distance from the baseline pixel is over `threshold` of the largest distance
* two colours can have, and pixels that are only antialiasing around an
* otherwise identical edge do not count. `threshold` is their `diffThreshold`
* and its default is theirs too.
*/
const MAX_YIQ_DELTA = 35_215;
const { values: opts, positionals } = parseArgs({
allowPositionals: true,
options: {
mode: { type: 'string', default: 'green' },
threshold: { type: 'string', default: '0.063' },
'include-aa': { type: 'boolean', default: false },
tint: { type: 'string', default: '' },
'no-caption': { type: 'boolean', default: false },
help: { type: 'boolean', short: 'h', default: false },
},
});
const [baseDir, afterDir, outArg] = positionals;
const MODES = new Set(['green', 'green-parallel', 'red', 'red-parallel']);
if (opts.help || !baseDir || !afterDir || !MODES.has(opts.mode)) {
console.log(`usage: node scripts/story-shots-diff.mjs <baseline-dir> <after-dir> [diff-dir]
--mode green the after shot, changed pixels painted over it (default)
--mode green-parallel previous | current | green diff, side by side and labelled
--mode red the after shot faded out, changed pixels painted red
--mode red-parallel previous | current | red diff, side by side and labelled
--threshold <0..1> YIQ distance a pixel must move to count (default 0.063)
--include-aa count antialiasing changes too (default: ignore them)
--tint <#rrggbb> override the mode's highlight colour
--no-caption do not stamp the story and the run settings on top
Prints "<changed pixels> <relative path>", largest first. Needs ImageMagick.`);
process.exit(opts.help ? 0 : 1);
}
const outDir = outArg ?? path.join(path.dirname(baseDir), 'diff');
const threshold = Number(opts.threshold);
const maxDelta = MAX_YIQ_DELTA * threshold * threshold;
const highlight = hexToRgb(
opts.tint || (opts.mode.startsWith('green') ? '#00e05a' : '#ff003a'),
);
function hexToRgb(hex) {
const value = Number.parseInt(hex.replace('#', ''), 16);
return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
}
requireMagick();
/**
* `top` rows are dropped: story-shots.mjs stamps a caption on its shots and
* records how tall it is, and a caption is not part of what the two runs are
* being compared on.
*/
const readRgba = (file, top = 0) => {
const [width, height] = magick(['identify', '-format', '%w %h', file])
.toString()
.split(' ')
.map(Number);
const data = magick([file, '-depth', '8', 'RGBA:-']);
return top > 0 && top < height
? { width, height: height - top, data: data.subarray(top * width * 4) }
: { width, height, data };
};
const writeRgba = ({ width, height, data }, file) =>
writeFile(
file,
magick(
['-depth', '8', '-size', `${width}x${height}`, 'RGBA:-', 'png:-'],
data,
),
);
/* The pixelmatch colour maths, which is what Chromatic's threshold is scaled to. */
const y = (r, g, b) => r * 0.29889531 + g * 0.58662247 + b * 0.11448223;
const i = (r, g, b) => r * 0.59597799 - g * 0.2741761 - b * 0.32180189;
const q = (r, g, b) => r * 0.21147017 - g * 0.52261711 + b * 0.31114694;
/** Squared YIQ distance, signed by which pixel is brighter. */
const colorDelta = (a, b, posA, posB, yOnly = false) => {
let r1 = a[posA];
let g1 = a[posA + 1];
let b1 = a[posA + 2];
const a1 = a[posA + 3];
let r2 = b[posB];
let g2 = b[posB + 1];
let b2 = b[posB + 2];
const a2 = b[posB + 3];
if (a1 === a2 && r1 === r2 && g1 === g2 && b1 === b2) {
return 0;
}
// Anything translucent is composited over the same mid grey in both images,
// so a difference in alpha alone still registers.
if (a1 < 255) {
const alpha = a1 / 255;
r1 = r1 * alpha + 255 * (1 - alpha) * 0.5;
g1 = g1 * alpha + 255 * (1 - alpha) * 0.5;
b1 = b1 * alpha + 255 * (1 - alpha) * 0.5;
}
if (a2 < 255) {
const alpha = a2 / 255;
r2 = r2 * alpha + 255 * (1 - alpha) * 0.5;
g2 = g2 * alpha + 255 * (1 - alpha) * 0.5;
b2 = b2 * alpha + 255 * (1 - alpha) * 0.5;
}
const deltaY = y(r1, g1, b1) - y(r2, g2, b2);
if (yOnly) {
return deltaY;
}
const deltaI = i(r1, g1, b1) - i(r2, g2, b2);
const deltaQ = q(r1, g1, b1) - q(r2, g2, b2);
return (
0.5053 * deltaY * deltaY + 0.299 * deltaI * deltaI + 0.1957 * deltaQ * deltaQ
);
};
/**
* True when the pixel sits on an edge that is drawn one subpixel over rather
* than moved: it is the darkest or lightest of its neighbours in one image, and
* the other image has a pixel around there doing the same job.
*/
const antialiased = (a, x1, y1, width, height, b) => {
const x0 = Math.max(x1 - 1, 0);
const y0 = Math.max(y1 - 1, 0);
const x2 = Math.min(x1 + 1, width - 1);
const y2 = Math.min(y1 + 1, height - 1);
const pos = (y1 * width + x1) * 4;
let zeroes = x1 === x0 || x1 === x2 || y1 === y0 || y1 === y2 ? 1 : 0;
let min = 0;
let max = 0;
let minX = 0;
let minY = 0;
let maxX = 0;
let maxY = 0;
for (let x = x0; x <= x2; x += 1) {
for (let yy = y0; yy <= y2; yy += 1) {
if (x === x1 && yy === y1) {
continue;
}
const delta = colorDelta(a, a, pos, (yy * width + x) * 4, true);
if (delta === 0) {
zeroes += 1;
if (zeroes > 2) {
return false;
}
} else if (delta < min) {
min = delta;
minX = x;
minY = yy;
} else if (delta > max) {
max = delta;
maxX = x;
maxY = yy;
}
}
}
if (min === 0 || max === 0) {
return false;
}
return (
(hasManySiblings(a, minX, minY, width, height) &&
hasManySiblings(b, minX, minY, width, height)) ||
(hasManySiblings(a, maxX, maxY, width, height) &&
hasManySiblings(b, maxX, maxY, width, height))
);
};
/** Whether the pixel has at least three identical neighbours. */
const hasManySiblings = (img, x1, y1, width, height) => {
const x0 = Math.max(x1 - 1, 0);
const y0 = Math.max(y1 - 1, 0);
const x2 = Math.min(x1 + 1, width - 1);
const y2 = Math.min(y1 + 1, height - 1);
const pos = (y1 * width + x1) * 4;
let zeroes = x1 === x0 || x1 === x2 || y1 === y0 || y1 === y2 ? 1 : 0;
for (let x = x0; x <= x2; x += 1) {
for (let yy = y0; yy <= y2; yy += 1) {
if (x === x1 && yy === y1) {
continue;
}
const other = (yy * width + x) * 4;
if (
img[pos] === img[other] &&
img[pos + 1] === img[other + 1] &&
img[pos + 2] === img[other + 2] &&
img[pos + 3] === img[other + 3]
) {
zeroes += 1;
if (zeroes > 2) {
return true;
}
}
}
}
return false;
};
/**
* The changed pixels of the pair, painted over the after shot. The `red` modes
* fade the shot out first, the way a pixelmatch diff reads; the `green` ones
* leave it alone, the way Chromatic's does.
*/
const diffPair = (base, after, mode) => {
const width = Math.min(base.width, after.width);
const height = Math.min(base.height, after.height);
const out = Buffer.from(after.data);
const fade = !mode.startsWith('green');
let changed = 0;
if (fade) {
for (let pos = 0; pos < out.length; pos += 4) {
const grey = y(out[pos], out[pos + 1], out[pos + 2]);
const value = 255 + (grey - 255) * 0.1;
out[pos] = value;
out[pos + 1] = value;
out[pos + 2] = value;
out[pos + 3] = 255;
}
}
for (let row = 0; row < height; row += 1) {
for (let column = 0; column < width; column += 1) {
const basePos = (row * base.width + column) * 4;
const afterPos = (row * after.width + column) * 4;
const delta = colorDelta(base.data, after.data, basePos, afterPos);
if (Math.abs(delta) <= maxDelta) {
continue;
}
if (
!opts['include-aa'] &&
(antialiased(base.data, column, row, base.width, base.height, after.data) ||
antialiased(after.data, column, row, after.width, after.height, base.data))
) {
continue;
}
changed += 1;
out[afterPos] = highlight[0];
out[afterPos + 1] = highlight[1];
out[afterPos + 2] = highlight[2];
out[afterPos + 3] = 255;
}
}
// A shot that grew or shrank has no counterpart for the extra rows and
// columns, so all of them are a change.
const extra =
after.width * after.height -
width * height +
(base.width * base.height - width * height);
return {
data: out,
width: after.width,
height: after.height,
changed: changed + extra,
};
};
/**
* What each run was and how it was configured, from the `shots.json`
* story-shots.mjs leaves beside its output. A run shot before that existed, or
* a directory assembled by hand, simply gets no caption.
*/
const manifest = async (dir) => {
try {
return JSON.parse(await readFile(path.join(dir, 'shots.json'), 'utf8'));
} catch {
return null;
}
};
const [baseRun, afterRun] = await Promise.all([
manifest(baseDir),
manifest(afterDir),
]);
/** The settings the two runs disagree on: what a difference in the shots may be. */
const changedKeys = CONFIG_KEYS.filter(
(key) => (baseRun?.config?.[key] ?? '') !== (afterRun?.config?.[key] ?? ''),
);
const settings = (run, keys) => settingsLine(run?.config, keys);
const shotOf = (run, rel) =>
run?.shots?.find((shot) => shot.file === rel.split(path.sep).join('/'));
const captionOf = (run, rel) => shotOf(run, rel)?.caption ?? 0;
/** ImageMagick's inline crop, so a tile shows the shot without its caption. */
const withoutCaption = (file, { width, height }, top) =>
top > 0 ? `${file}[${width}x${height}+0+${top}]` : file;
/** Story, then the settings both runs shared. One line each, widest font first. */
const header = (rel) => {
const shot = shotOf(afterRun, rel) ?? shotOf(baseRun, rel);
const shared = settings(
afterRun,
CONFIG_KEYS.filter((key) => !changedKeys.includes(key)),
);
return [
shot ? `${shot.title}/${shot.name}` : rel.replace(/\.png$/, ''),
[shot?.id ?? '', shot?.theme ?? '', shot?.status === 'busy' ? '(busy)' : '']
.filter(Boolean)
.join(' '),
shared,
].filter(Boolean);
};
/** A tile's own line: which side it is, and where its run differed. */
const sideLabel = (side, run) =>
[side, settings(run, changedKeys)].filter(Boolean).join(' ');
const captionLines = (rel, lines) =>
opts['no-caption'] ? [] : [...header(rel), ...lines].filter(Boolean);
const pngs = async (dir, prefix = '') => {
const entries = await readdir(path.join(dir, prefix), { withFileTypes: true });
const files = [];
for (const entry of entries) {
const rel = path.join(prefix, entry.name);
if (entry.isDirectory()) {
files.push(...(await pngs(dir, rel)));
} else if (entry.name.endsWith('.png')) {
files.push(rel);
}
}
return files;
};
const results = [];
await mkdir(outDir, { recursive: true });
for (const rel of (await pngs(baseDir)).sort()) {
const afterFile = path.join(afterDir, rel);
const base = readRgba(path.join(baseDir, rel), captionOf(baseRun, rel));
let after;
try {
after = readRgba(afterFile, captionOf(afterRun, rel));
} catch {
console.error(`missing in after: ${rel}`);
continue;
}
await mkdir(path.join(outDir, path.dirname(rel)), { recursive: true });
const diff = diffPair(base, after, opts.mode);
const target = path.join(outDir, rel);
const parallel = opts.mode.endsWith('-parallel');
// With no tiles to label, a run's own settings go in the caption instead.
const caption = captionLines(
rel,
parallel || !changedKeys.length
? []
: [sideLabel('previous', baseRun), sideLabel('current', afterRun)],
);
const diffFile = path.join(os.tmpdir(), `story-shots-${process.pid}.png`);
const body = path.join(os.tmpdir(), `story-shots-${process.pid}-body.png`);
// The gutter is the opposite of the theme's own background, so the tiles and
// the caption keep an edge instead of bleeding into it.
const shot = shotOf(afterRun, rel) ?? shotOf(baseRun, rel);
const theme = shot?.theme ?? rel.split(path.sep)[0];
const { background, foreground } = palette(theme);
if (parallel) {
await writeRgba(diff, diffFile);
const tile = (label, file) => [
'(',
`label:${literal(label)}`,
file,
'-gravity',
'center',
'-append',
'-bordercolor',
background,
'-border',
'12',
')',
];
magick([
'-background',
background,
'-fill',
foreground,
...bodyFont(),
'-pointsize',
// The tiles end up side by side, so they are read at the montage's width.
String(Math.round(pointsize(after.width * 3) * 0.62)),
...tile(
sideLabel('previous', baseRun),
withoutCaption(path.join(baseDir, rel), base, captionOf(baseRun, rel)),
),
...tile(
sideLabel('current', afterRun),
withoutCaption(afterFile, after, captionOf(afterRun, rel)),
),
...tile('diff', diffFile),
'-gravity',
'north',
'+append',
caption.length ? body : target,
]);
if (caption.length) {
stamp({ lines: caption, from: body, to: target, theme });
}
} else {
await writeRgba(diff, caption.length ? body : target);
if (caption.length) {
stamp({ lines: caption, from: body, to: target, theme });
}
}
results.push([diff.changed, rel]);
}
results
.sort((a, b) => b[0] - a[0])
.forEach(([changed, rel]) =>
console.log(`${String(changed).padStart(10)} ${rel}`),
);
console.error(`diffs in ${outDir}`);

514
frontend/scripts/story-shots.mjs Executable file
View File

@@ -0,0 +1,514 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import { mkdir, rename, writeFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { parseArgs } from 'node:util';
import { pathToFileURL } from 'node:url';
import path from 'node:path';
import os from 'node:os';
import {
CONFIG_KEYS,
hasMagick,
settingsLine,
stamp,
} from './story-shots-caption.mjs';
/**
* The wall clock every shot is taken at, passed to the preview as `storyClock`.
* `.storybook/preview-head.html` freezes the same instant by itself, so a
* Chromatic build reads the clock this run does.
*/
const FROZEN_CLOCK = '2026-06-15T12:00:00.000Z';
const { values: opts, positionals } = parseArgs({
allowPositionals: true,
options: {
out: { type: 'string', short: 'o' },
stories: { type: 'string', multiple: true, default: [] },
title: { type: 'string', default: '' },
name: { type: 'string', default: '' },
theme: { type: 'string', multiple: true, default: [] },
args: { type: 'string', multiple: true, default: [] },
port: { type: 'string', default: process.env.SB_PORT ?? '6006' },
width: { type: 'string', default: '1680' },
height: { type: 'string', default: '1200' },
'max-height': { type: 'string', default: '8000' },
grow: { type: 'string', default: 'scrollers' },
settle: { type: 'string', default: '1500' },
clock: { type: 'string', default: FROZEN_CLOCK },
motion: { type: 'boolean', default: false },
ignore: { type: 'string', multiple: true, default: [] },
flat: { type: 'boolean', default: false },
'no-caption': { type: 'boolean', default: false },
list: { type: 'boolean', default: false },
help: { type: 'boolean', short: 'h', default: false },
},
});
const outDir = opts.out ?? positionals[0];
const themes = opts.theme.flatMap((value) => value.split(',')).filter(Boolean);
const storyArgs = opts.args.filter(Boolean).join(';');
if (opts.help || (!outDir && !opts.list)) {
console.log(`usage: node scripts/story-shots.mjs <out-dir> [options]
--stories <match> only stories whose id or title/name path contains <match>
(repeatable, comma-separated; default: every story)
--title <prefix> only stories whose title starts with <prefix>
--name <match> only stories whose name contains <match>
--theme <themes> themes to shoot, e.g. dark,light (default: story default)
--args <k:v;k2:v2> arg overrides, storybook's own ?args= syntax (repeatable).
A value containing a dot is dropped by storybook itself
--port <port> storybook dev server port (default 6006, or $SB_PORT)
--width <px> viewport width, the only fixed dimension (default 1680)
--height <px> shortest the viewport may be (default 1200)
--max-height <px> tallest the viewport may grow to (default 8000)
--grow <what> scrollers (default) grows the viewport until the page's
own scrollers fit, document only follows the document
height (a no-op on any page with the app shell), none
keeps --height
--settle <ms> wait after the page goes quiet (default 1500)
--clock <iso|live> wall clock the page reads (default ${FROZEN_CLOCK})
--motion keep animations and transitions running
--ignore <selector> hide matching elements, on top of [data-shot-ignore]
--flat write <out>/<id>.png instead of <out>/<theme>/<id>.png
--no-caption do not stamp the story and the run settings on the shot
--list print the matched stories and exit
Screenshots land in <out-dir>/<theme>/<story-id>.png, alongside a shots.json
recording what each shot is, how the run was configured, and how tall the
caption on it is. story-shots-diff.mjs reads that to crop the caption off before
comparing, so two runs never diff their own captions.
Captioning needs ImageMagick; without it the shots are written bare.
Playwright is looked up in tests/e2e, then in the global install; override with
PLAYWRIGHT_MODULE. The browser is playwright's own chromium, else an installed
Chrome; override with CHROME_PATH.`);
process.exit(opts.help ? 0 : 1);
}
const base = `http://localhost:${opts.port}`;
// `index.json` carries raw control characters from story jsdoc, so it is read as
// text rather than piped through anything that revalidates it.
const index = JSON.parse(await (await fetch(`${base}/index.json`)).text());
const matches = opts.stories
.flatMap((value) => value.split(','))
.filter(Boolean);
const stories = Object.values(index.entries)
.filter((entry) => {
if (entry.type !== 'story') {
return false;
}
if (opts.title && !entry.title.startsWith(opts.title)) {
return false;
}
if (
opts.name &&
!entry.name.toLowerCase().includes(opts.name.toLowerCase())
) {
return false;
}
if (!matches.length) {
return true;
}
const haystack = `${entry.id} ${entry.title}/${entry.name}`.toLowerCase();
return matches.some((match) => haystack.includes(match.toLowerCase()));
})
.sort((a, b) => a.id.localeCompare(b.id));
if (opts.list) {
stories.forEach((story) =>
console.log(`${story.id}\t${story.title}/${story.name}`),
);
console.log(`${stories.length} stories`);
process.exit(0);
}
if (!stories.length) {
console.error('no story matched');
process.exit(1);
}
const ignoreSelectors = opts.ignore
.flatMap((value) => value.split(','))
.map((value) => value.trim())
.filter(Boolean);
if (opts.clock !== 'live' && Number.isNaN(Date.parse(opts.clock))) {
console.error(`--clock: not a date: ${opts.clock}`);
process.exit(1);
}
/**
* `[data-shot-ignore]` and `--ignore` hide what cannot be settled, the local
* half of Chromatic's `data-chromatic="ignore"`. Everything else the shot needs
* held still - the frozen clock, the parked animations, the lists snapped onto
* their bottom - is done by the preview itself, so a Chromatic build and a shot
* from here see the same page.
*/
const ignoreCss = (
ignore,
) => `[data-shot-ignore], [data-chromatic='ignore']${ignore
.map((selector) => `, ${selector}`)
.join('')} {
visibility: hidden !important;
}`;
/**
* Playwright is not a frontend dependency: it lives in `tests/e2e`, or globally,
* or wherever `$PLAYWRIGHT_MODULE` points. `@playwright/test` re-exports
* `chromium`, so an e2e install alone is enough.
*/
const resolvePlaywright = () => {
const specifiers = process.env.PLAYWRIGHT_MODULE
? [process.env.PLAYWRIGHT_MODULE]
: ['playwright', '@playwright/test'];
const find = (roots) => {
for (const specifier of specifiers) {
for (const root of roots) {
try {
return createRequire(path.join(root, '-')).resolve(specifier);
} catch {
/* next candidate */
}
}
}
return null;
};
const local = find([
import.meta.dirname,
path.resolve(import.meta.dirname, '../../tests/e2e'),
]);
if (local) {
return local;
}
// `npm root -g` prints the global node_modules; resolution starts a level up.
const globalRoot = spawnSync('npm', ['root', '-g'], { encoding: 'utf8' });
const global =
globalRoot.status === 0 && find([path.dirname(globalRoot.stdout.trim())]);
if (global) {
return global;
}
console.error(
'playwright not found. Install it (pnpm -C tests/e2e install, or npm i -g playwright) or set PLAYWRIGHT_MODULE.',
);
return process.exit(1);
};
const pwModule = await import(pathToFileURL(resolvePlaywright()).href);
const pw = pwModule.chromium ? pwModule : pwModule.default;
console.log(
`${stories.length} stories x ${themes.length || 1} theme(s) -> ${outDir}`,
);
/**
* A playwright install carries no browser of its own, and the revision it wants
* is often not the one that was downloaded, so an installed Chrome is the
* fallback before giving up.
*/
const launch = async () => {
if (process.env.CHROME_PATH) {
return pw.chromium.launch({ executablePath: process.env.CHROME_PATH });
}
try {
return await pw.chromium.launch();
} catch (error) {
try {
return await pw.chromium.launch({ channel: 'chrome' });
} catch {
console.error(
`${error.message.split('\n')[0]}\nRun 'playwright install chromium' or set CHROME_PATH to a browser binary.`,
);
return process.exit(1);
}
}
};
const browser = await launch();
const failures = [];
const shots = [];
const runConfig = {
args: storyArgs,
clock: opts.clock,
width: opts.width,
height: opts.height,
grow: opts.grow,
motion: opts.motion ? 'live' : 'still',
settle: opts.settle,
ignore: ignoreSelectors.join(', '),
};
const captioning = !opts['no-caption'] && hasMagick();
if (!opts['no-caption'] && !captioning) {
console.error('ImageMagick not found: shots are written without a caption.');
}
const configLine = settingsLine(runConfig, CONFIG_KEYS);
for (const theme of themes.length ? themes : [null]) {
const dir = opts.flat ? outDir : path.join(outDir, theme ?? 'default');
await mkdir(dir, { recursive: true });
if (theme) {
console.log(`\n[${theme}]`);
}
for (const story of stories) {
// A context per story: reusing one page loses the msw worker
// re-registration race after a few navigations and the story then dies on
// a missing worker.
const context = await browser.newContext({
viewport: { width: Number(opts.width), height: Number(opts.height) },
reducedMotion: opts.motion ? 'no-preference' : 'reduce',
});
const page = await context.newPage();
// react-query retries and msw both keep requests going long after load, so
// the settle waits on the page being quiet rather than on a fixed delay.
let inFlight = 0;
let lastActivity = Date.now();
page.on('request', () => {
inFlight += 1;
lastActivity = Date.now();
});
const done = () => {
inFlight = Math.max(inFlight - 1, 0);
lastActivity = Date.now();
};
page.on('requestfinished', done);
page.on('requestfailed', done);
// The height the rounds had reached when the page turned out to grow with
// the viewport, kept only to flag the story in the log.
let chasing = 0;
const url = new URL(`${base}/iframe.html`);
url.searchParams.set('viewMode', 'story');
url.searchParams.set('id', story.id);
// The preview owns the clock and the motion state, so both are asked for in
// the URL rather than injected here: a Chromatic build gets the defaults.
url.searchParams.set('storyClock', opts.clock);
const globals = [theme && `theme:${theme}`, opts.motion && 'motion:live']
.filter(Boolean)
.join(';');
if (globals) {
url.searchParams.set('globals', globals);
}
if (storyArgs) {
url.searchParams.set('args', storyArgs);
}
try {
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
// Storybook's own render phase is the readiness signal: it reaches
// `finished` only once the loaders, the decorators and the story's `play`
// are all done, which a DOM check cannot see. The dev server transforms
// each page module on first visit, so this is the slow wait.
await page.waitForFunction(
() =>
(window.__STORYBOOK_PREVIEW__?.storyRenders ?? []).some((render) =>
['finished', 'errored', 'aborted'].includes(render.phase),
) || document.body.classList.contains('sb-show-errordisplay'),
undefined,
{ timeout: 120_000 },
);
await page.addStyleTag({ content: ignoreCss(ignoreSelectors) });
if (!opts.motion) {
// Videos and GIFs are parked on their first frame, as Chromatic does.
await page.evaluate(() =>
document.querySelectorAll('video').forEach((video) => video.pause?.()),
);
}
// Text reflows when a webfont lands, so the shot waits for the faces the
// page asked for. Some stories keep a request open by design, hence the
// cap on the quiet wait rather than a plain networkidle.
await page.evaluate(() => document.fonts.ready);
const quietUntil = Date.now() + 15_000;
while (
Date.now() < quietUntil &&
(inFlight > 0 || Date.now() - lastActivity < 600)
) {
await page.waitForTimeout(200);
}
await page.waitForTimeout(Number(opts.settle));
// The width is the fixed dimension and the height follows the page, the
// way a Chromatic viewport does. `src/styles.scss` pins
// `html, body, #root` to `height: 100%; overflow: hidden`, so the
// document can never outgrow the viewport and its height says nothing
// about what is on the page: what overflows are the shell's inner
// scrollers. `scrollers` grows the viewport until the tallest of those
// fits, so nothing is cut off and no scrollbar is left in the shot.
// Growing changes the layout, hence the rounds. A page that sizes a panel
// in `vh` grows its own content as the viewport grows, so no height ever
// fits it and the rounds only chase: `.alert-chart-container` is `57vh`,
// which puts Create Alert's fixed point at 4344px with an empty band on
// top. Such a page is shot at `--height` with its own scrollbar instead,
// which is what it looks like in a browser.
if (opts.grow !== 'none') {
const maximum = Number(opts['max-height']);
const requested = Number(opts.height);
let height = requested;
let fits = false;
for (let round = 0; round < 3 && !fits; round += 1) {
const needed = Math.min(
maximum,
await page.evaluate((withScrollers) => {
const document_ = Math.max(
document.documentElement.scrollHeight,
document.body.scrollHeight,
);
if (!withScrollers) {
return document_;
}
// Popups are skipped: they are out of the flow, and a tall
// dropdown or tooltip would otherwise drag the shot to a
// height nothing on the page itself needs.
const inFlow = (element) => {
for (
let node = element;
node && node !== document.documentElement;
node = node.parentElement
) {
const { position } = getComputedStyle(node);
if (position === 'fixed' || position === 'absolute') {
return false;
}
}
return true;
};
return [...document.querySelectorAll('*')].reduce((tallest, element) => {
const { overflowY } = getComputedStyle(element);
if (
!['auto', 'scroll', 'overlay'].includes(overflowY) ||
element.scrollHeight - element.clientHeight <= 1 ||
!inFlow(element)
) {
return tallest;
}
const box = element.getBoundingClientRect();
const above = box.top + window.scrollY;
const below = Math.max(0, document_ - (box.bottom + window.scrollY));
return Math.max(tallest, above + element.scrollHeight + below);
}, document_);
}, opts.grow === 'scrollers'),
);
fits = needed <= height;
if (fits) {
break;
}
height = needed;
await page.setViewportSize({ width: Number(opts.width), height });
await page.waitForTimeout(Number(opts.settle));
}
if (!fits && height !== requested) {
chasing = height;
height = requested;
await page.setViewportSize({ width: Number(opts.width), height });
await page.waitForTimeout(Number(opts.settle));
}
}
// The preview snapped its bottom-pinned lists at `afterEach`, before the
// page went quiet; a virtuoso list is usually still measuring then.
await page.evaluate(() => window.__signozSnapPinnedScrollers?.());
// A page that is still moving — a list scrolling itself to the bottom, a
// monaco editor re-measuring, a tooltip being repositioned — is shot
// twice in a row until two frames come back identical, since what the
// page is waiting on is not observable from here.
let shot = await page.screenshot();
let stable = false;
for (let attempt = 0; attempt < 8 && !stable; attempt += 1) {
await page.waitForTimeout(400);
const next = await page.screenshot();
stable = next.equals(shot);
shot = next;
}
const file = path.join(dir, `${story.id}.png`);
await writeFile(file, shot);
// The band goes on the shot itself so a single screenshot says what it
// is, and its height is recorded so a diff can take it back off.
let caption = 0;
if (captioning) {
const temporary = path.join(
os.tmpdir(),
`story-shots-caption-${process.pid}.png`,
);
caption = stamp({
lines: [
`${story.title}/${story.name}`,
[story.id, theme ?? 'default', stable ? '' : '(busy)']
.filter(Boolean)
.join(' '),
configLine,
].filter(Boolean),
from: file,
to: temporary,
theme: theme ?? 'dark',
});
await rename(temporary, file);
}
shots.push({
file: path.posix.join(
opts.flat ? '' : (theme ?? 'default'),
`${story.id}.png`,
),
id: story.id,
title: story.title,
name: story.name,
theme: theme ?? 'default',
status: stable ? 'ok' : 'busy',
caption,
});
console.log(
` ${stable ? 'ok ' : 'busy'} ${story.id}${
chasing ? ` (viewport-sized content, stopped chasing ${chasing}px)` : ''
}`,
);
} catch (error) {
failures.push(`${theme ?? 'default'}/${story.id}`);
console.log(` FAIL ${story.id}: ${error.message.split('\n')[0]}`);
} finally {
await context.close();
}
}
}
await browser.close();
// The diff script captions its output from this, so the run's own settings sit
// next to the shots they produced rather than only in the shell history.
await writeFile(
path.join(outDir, 'shots.json'),
`${JSON.stringify({ config: runConfig, shots }, null, '\t')}\n`,
);
if (failures.length) {
console.error(`\n${failures.length} failed: ${failures.join(', ')}`);
process.exit(1);
}

View File

@@ -169,12 +169,12 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
// Check for workspace blocked (trial expired)
if (!isFetchingActiveLicense && isCloudPlatform && trialInfo?.workSpaceBlock) {
const isRouteEnabledForWorkspaceBlockedState =
isAdmin &&
(pathname === ROUTES.SETTINGS ||
pathname === ROUTES.ORG_SETTINGS ||
pathname === ROUTES.MEMBERS_SETTINGS ||
pathname === ROUTES.BILLING ||
pathname === ROUTES.MY_SETTINGS);
pathname === ROUTES.SETTINGS ||
pathname === ROUTES.BILLING ||
(isAdmin &&
(pathname === ROUTES.ORG_SETTINGS ||
pathname === ROUTES.MEMBERS_SETTINGS ||
pathname === ROUTES.MY_SETTINGS));
if (
pathname !== ROUTES.WORKSPACE_LOCKED &&

View File

@@ -103,30 +103,30 @@ function createMockLicense(
overrides: Partial<LicenseResModel> = {},
): LicenseResModel {
return {
key: 'test-key',
event_queue: {
created_at: '0',
id: 'test-license-id',
eventQueue: {
createdAt: '0',
event: LicenseEvent.NO_EVENT,
scheduled_at: '0',
scheduledAt: '0',
status: '',
updated_at: '0',
updatedAt: '0',
},
state: LicenseState.ACTIVATED,
status: LicenseStatus.VALID,
platform: LicensePlatform.CLOUD,
created_at: '0',
createdAt: '0',
plan: {
created_at: '0',
id: '0',
createdAt: '0',
description: '',
is_active: true,
isActive: true,
name: '',
updated_at: '0',
updatedAt: '0',
},
plan_id: '0',
free_until: '0',
updated_at: '0',
valid_from: 0,
valid_until: 0,
freeUntil: '0',
updatedAt: '0',
validFrom: 0,
validUntil: 0,
...overrides,
};
}
@@ -739,7 +739,7 @@ describe('PrivateRoute', () => {
assertStaysOnRoute(ROUTES.MY_SETTINGS);
});
it('should redirect VIEWER to workspace locked even when trying to access settings', async () => {
it('should allow VIEWER to access /settings when workspace is blocked', () => {
renderPrivateRoute({
initialRoute: ROUTES.SETTINGS,
appContext: {
@@ -752,10 +752,10 @@ describe('PrivateRoute', () => {
isCloudUser: true,
});
await assertRedirectsTo(ROUTES.WORKSPACE_LOCKED);
assertStaysOnRoute(ROUTES.SETTINGS);
});
it('should redirect VIEWER to workspace locked when trying to access billing', async () => {
it('should allow VIEWER to access /settings/billing when workspace is blocked', () => {
renderPrivateRoute({
initialRoute: ROUTES.BILLING,
appContext: {
@@ -768,7 +768,7 @@ describe('PrivateRoute', () => {
isCloudUser: true,
});
await assertRedirectsTo(ROUTES.WORKSPACE_LOCKED);
assertStaysOnRoute(ROUTES.BILLING);
});
it('should redirect VIEWER to workspace locked when trying to access org-settings', async () => {
@@ -819,7 +819,7 @@ describe('PrivateRoute', () => {
await assertRedirectsTo(ROUTES.WORKSPACE_LOCKED);
});
it('should redirect EDITOR to workspace locked when trying to access settings', async () => {
it('should allow EDITOR to access /settings when workspace is blocked', () => {
renderPrivateRoute({
initialRoute: ROUTES.SETTINGS,
appContext: {
@@ -832,7 +832,7 @@ describe('PrivateRoute', () => {
isCloudUser: true,
});
await assertRedirectsTo(ROUTES.WORKSPACE_LOCKED);
assertStaysOnRoute(ROUTES.SETTINGS);
});
it('should not redirect when already on workspace locked page', () => {
@@ -850,6 +850,22 @@ describe('PrivateRoute', () => {
assertStaysOnRoute(ROUTES.WORKSPACE_LOCKED);
});
it('should keep a custom role (ANONYMOUS) on workspace locked instead of bouncing to unauthorized', () => {
renderPrivateRoute({
initialRoute: ROUTES.WORKSPACE_LOCKED,
appContext: {
isLoggedIn: true,
isFetchingActiveLicense: false,
activeLicense: createMockLicense({ platform: LicensePlatform.CLOUD }),
trialInfo: createMockTrialInfo({ workSpaceBlock: true }),
user: createMockUser({ role: USER_ROLES.ANONYMOUS as ROLES }),
},
isCloudUser: true,
});
assertStaysOnRoute(ROUTES.WORKSPACE_LOCKED);
});
it('should not redirect self-hosted users to workspace locked even when workSpaceBlock is true', () => {
renderPrivateRoute({
initialRoute: ROUTES.HOME,
@@ -1024,6 +1040,24 @@ describe('PrivateRoute', () => {
assertStaysOnRoute(ROUTES.WORKSPACE_SUSPENDED);
});
it('should keep a custom role (ANONYMOUS) on workspace suspended instead of bouncing to unauthorized', () => {
renderPrivateRoute({
initialRoute: ROUTES.WORKSPACE_SUSPENDED,
appContext: {
isLoggedIn: true,
isFetchingActiveLicense: false,
activeLicense: createMockLicense({
platform: LicensePlatform.CLOUD,
state: LicenseState.DEFAULTED,
}),
user: createMockUser({ role: USER_ROLES.ANONYMOUS as ROLES }),
},
isCloudUser: true,
});
assertStaysOnRoute(ROUTES.WORKSPACE_SUSPENDED);
});
it('should not redirect self-hosted users to workspace suspended when license is defaulted', () => {
renderPrivateRoute({
initialRoute: ROUTES.HOME,
@@ -1580,6 +1614,19 @@ describe('PrivateRoute', () => {
path: ROUTES.SUPPORT,
deniedRoles: [USER_ROLES.AUTHOR as ROLES],
},
WORKSPACE_LOCKED: {
path: ROUTES.WORKSPACE_LOCKED,
deniedRoles: DENIED_ROLES,
},
WORKSPACE_SUSPENDED: {
path: ROUTES.WORKSPACE_SUSPENDED,
deniedRoles: DENIED_ROLES,
},
WORKSPACE_ACCESS_RESTRICTED: {
path: ROUTES.WORKSPACE_ACCESS_RESTRICTED,
deniedRoles: DENIED_ROLES,
},
BILLING: { path: ROUTES.BILLING, deniedRoles: DENIED_ROLES },
};
const authzRouteRolePairs: [string, string, ROLES][] = Object.entries(

View File

@@ -1,11 +1,12 @@
import { Suspense, useCallback, useEffect, useState } from 'react';
import { ReactNode, Suspense, useCallback, useEffect, useState } from 'react';
import { Route, Router, Switch } from 'react-router-dom';
import { CompatRouter } from 'react-router-dom-v5-compat';
import * as Sentry from '@sentry/react';
import { ConfigProvider } from 'antd';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import AppPageProviders from 'app/AppPageProviders';
import AppShell from 'app/AppShell';
import AppLoading from 'components/AppLoading/AppLoading';
import { CmdKPalette } from 'components/cmdKPalette/cmdKPalette';
import NotFound from 'components/NotFound';
@@ -17,22 +18,15 @@ import ROUTES from 'constants/routes';
import AppLayout from 'container/AppLayout';
import Hex from 'crypto-js/enc-hex';
import HmacSHA256 from 'crypto-js/hmac-sha256';
import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { useIsDarkMode, useThemeConfig } from 'hooks/useDarkMode';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { NotificationProvider } from 'hooks/useNotifications';
import { ResourceProvider } from 'hooks/useResourceAttribute';
import { StatusCodes } from 'http-status-codes';
import history from 'lib/history';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import posthog from 'posthog-js';
import { useAppContext } from 'providers/App/App';
import { IUser } from 'providers/App/types';
import { CmdKProvider } from 'providers/cmdKProvider';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { LicenseStatus } from 'types/api/licensesV3/getActive';
import { extractDomain } from 'utils/app';
@@ -44,8 +38,17 @@ import defaultRoutes, {
SUPPORT_ROUTE,
} from './routes';
const appRouter = (children: ReactNode): ReactNode => (
<Router history={history}>
<CompatRouter>{children}</CompatRouter>
</Router>
);
const appLayout = (children: ReactNode): ReactNode => (
<AppLayout>{children}</AppLayout>
);
function App(): JSX.Element {
const themeConfig = useThemeConfig();
const {
user,
isFetchingUser,
@@ -451,48 +454,36 @@ function App(): JSX.Element {
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<ConfigProvider theme={themeConfig}>
<Router history={history}>
<CompatRouter>
<CmdKProvider>
<NotificationProvider>
<ErrorModalProvider>
{isLoggedInState && <CmdKPalette userRole={user.role} />}
{isLoggedInState && (
<ShiftHoldOverlayController userRole={user.role} />
)}
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<KeyboardHotkeysProvider>
<AppLayout>
<PreferenceContextProvider>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route exact path="/" component={Home} />
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</PreferenceContextProvider>
</AppLayout>
</KeyboardHotkeysProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>
</ErrorModalProvider>
</NotificationProvider>
</CmdKProvider>
</CompatRouter>
</Router>
</ConfigProvider>
<AppShell
router={appRouter}
overlays={
isLoggedInState && (
<>
<CmdKPalette userRole={user.role} />
<ShiftHoldOverlayController userRole={user.role} />
</>
)
}
>
<PrivateRoute>
<AppPageProviders layout={appLayout}>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route exact path="/" component={Home} />
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</AppPageProviders>
</PrivateRoute>
</AppShell>
</Sentry.ErrorBoundary>
);
}

View File

@@ -102,15 +102,10 @@ export const DashboardPage = Loadable(
() => import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardPage'),
);
export const DashboardWidget = Loadable(
() =>
import(/* webpackChunkName: "DashboardWidgetPage" */ 'pages/DashboardWidget'),
);
export const DashboardPanelEditorPage = Loadable(
() =>
import(
/* webpackChunkName: "DashboardPanelEditorPage" */ 'pages/DashboardPageV2/PanelEditorPage/PanelEditorPage'
/* webpackChunkName: "DashboardPanelEditorPage" */ 'pages/DashboardPage/PanelEditorPage/PanelEditorPage'
),
);

View File

@@ -13,7 +13,6 @@ import {
DashboardPage,
DashboardPanelEditorPage,
DashboardsListPage,
DashboardWidget,
EditRulesPage,
ErrorDetails,
ForgotPassword,
@@ -183,13 +182,6 @@ const routes: AppRoutes[] = [
isPrivate: false,
key: 'PUBLIC_DASHBOARD',
},
{
path: ROUTES.DASHBOARD_WIDGET,
exact: true,
component: DashboardWidget,
isPrivate: true,
key: 'DASHBOARD_WIDGET',
},
{
path: ROUTES.DASHBOARD_PANEL_EDITOR,
exact: true,

View File

@@ -1,59 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
export interface DayBreakdownEntry {
timestamp: number;
total: number;
quantity: number;
count: number;
size: number;
}
export interface TierEntry {
quantity: number;
unitPrice: number;
tierCost: number;
}
export interface BreakdownEntry {
type: string;
unit: string;
dayWiseBreakdown: {
breakdown: DayBreakdownEntry[];
};
tiers?: TierEntry[];
}
export interface UsageResponsePayloadProps {
billingPeriodStart: number;
billingPeriodEnd: number;
details: {
total: number;
baseFee: number;
breakdown: BreakdownEntry[];
billTotal: number;
};
discount: number;
subscriptionStatus?: string;
}
const getUsage = async (
licenseKey: string,
): Promise<SuccessResponse<UsageResponsePayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(`/billing?licenseKey=${licenseKey}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getUsage;

View File

@@ -1,36 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { CreatePublicDashboardProps } from 'types/api/dashboard/public/create';
/**
* @deprecated Use the generated `useCreatePublicDashboard` hook (or `createPublicDashboard` fetcher) from
* `api/generated/services/dashboard` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const createPublicDashboard = async (
props: CreatePublicDashboardProps,
): Promise<SuccessResponseV2<CreatePublicDashboardProps>> => {
const { dashboardId, timeRangeEnabled = false, defaultTimeRange = DEFAULT_TIME_RANGE } = props;
try {
const response = await axios.post(
`/dashboards/${dashboardId}/public`,
{ timeRangeEnabled, defaultTimeRange },
);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default createPublicDashboard;

View File

@@ -1,27 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { GetPublicDashboardDataProps, PayloadProps,PublicDashboardDataProps } from 'types/api/dashboard/public/get';
/**
* @deprecated Use the generated `useGetPublicDashboardData` hook (or `getPublicDashboardData` fetcher) from
* `api/generated/services/dashboard` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const getPublicDashboardData = async (props: GetPublicDashboardDataProps): Promise<SuccessResponseV2<PublicDashboardDataProps>> => {
try {
const response = await axios.get<PayloadProps>(`/public/dashboards/${props.id}`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getPublicDashboardData;

View File

@@ -1,27 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { GetPublicDashboardMetaProps, PayloadProps,PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta';
/**
* @deprecated Use the generated `useGetPublicDashboard` hook (or `getPublicDashboard` fetcher) from
* `api/generated/services/dashboard` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const getPublicDashboardMeta = async (props: GetPublicDashboardMetaProps): Promise<SuccessResponseV2<PublicDashboardMetaProps>> => {
try {
const response = await axios.get<PayloadProps>(`/dashboards/${props.id}/public`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getPublicDashboardMeta;

View File

@@ -1,34 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { MetricRangePayloadV5 } from 'api/v5/v5';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { GetPublicDashboardWidgetDataProps } from 'types/api/dashboard/public/getWidgetData';
/**
* @deprecated Use the generated `useGetPublicDashboardWidgetQueryRange` hook (or `getPublicDashboardWidgetQueryRange` fetcher) from
* `api/generated/services/dashboard` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const getPublicDashboardWidgetData = async (props: GetPublicDashboardWidgetDataProps): Promise<SuccessResponseV2<MetricRangePayloadV5>> => {
try {
const response = await axios.get(`/public/dashboards/${props.id}/widgets/${props.index}/query_range`, {
params: {
startTime: props.startTime,
endTime: props.endTime,
},
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getPublicDashboardWidgetData;

View File

@@ -1,29 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps,RevokePublicDashboardAccessProps } from 'types/api/dashboard/public/delete';
/**
* @deprecated Use the generated `useDeletePublicDashboard` hook (or `deletePublicDashboard` fetcher) from
* `api/generated/services/dashboard` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const revokePublicDashboardAccess = async (
props: RevokePublicDashboardAccessProps,
): Promise<SuccessResponseV2<PayloadProps>> => {
try {
const response = await axios.delete<PayloadProps>(`/dashboards/${props.id}/public`);
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default revokePublicDashboardAccess;

View File

@@ -1,36 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { UpdatePublicDashboardProps } from 'types/api/dashboard/public/update';
/**
* @deprecated Use the generated `useUpdatePublicDashboard` hook (or `updatePublicDashboard` fetcher) from
* `api/generated/services/dashboard` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const updatePublicDashboard = async (
props: UpdatePublicDashboardProps,
): Promise<SuccessResponseV2<UpdatePublicDashboardProps>> => {
const { dashboardId, timeRangeEnabled = false, defaultTimeRange = DEFAULT_TIME_RANGE } = props;
try {
const response = await axios.put(
`/dashboards/${dashboardId}/public`,
{ timeRangeEnabled, defaultTimeRange },
);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default updatePublicDashboard;

View File

@@ -19,8 +19,10 @@ import type {
import type {
AlertmanagertypesPostableChannelDTO,
AlertmanagertypesPostableNotificationChannelDTO,
AlertmanagertypesReceiverDTO,
CreateChannel201,
CreateNotificationChannel201,
DeleteChannelByIDPathParameters,
GetChannelByID200,
GetChannelByIDPathParameters,
@@ -647,3 +649,87 @@ export const useTestChannelDeprecated = <
> => {
return useMutation(getTestChannelDeprecatedMutationOptions(options));
};
/**
* This endpoint creates a notification channel
* @summary Create notification channel
*/
export const createNotificationChannel = (
alertmanagertypesPostableNotificationChannelDTO?: BodyType<AlertmanagertypesPostableNotificationChannelDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateNotificationChannel201>({
url: `/api/v2/notification_channels`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: alertmanagertypesPostableNotificationChannelDTO,
signal,
});
};
export const getCreateNotificationChannelMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createNotificationChannel>>,
TError,
{ data?: BodyType<AlertmanagertypesPostableNotificationChannelDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createNotificationChannel>>,
TError,
{ data?: BodyType<AlertmanagertypesPostableNotificationChannelDTO> },
TContext
> => {
const mutationKey = ['createNotificationChannel'];
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 createNotificationChannel>>,
{ data?: BodyType<AlertmanagertypesPostableNotificationChannelDTO> }
> = (props) => {
const { data } = props ?? {};
return createNotificationChannel(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateNotificationChannelMutationResult = NonNullable<
Awaited<ReturnType<typeof createNotificationChannel>>
>;
export type CreateNotificationChannelMutationBody =
| BodyType<AlertmanagertypesPostableNotificationChannelDTO>
| undefined;
export type CreateNotificationChannelMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create notification channel
*/
export const useCreateNotificationChannel = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createNotificationChannel>>,
TError,
{ data?: BodyType<AlertmanagertypesPostableNotificationChannelDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createNotificationChannel>>,
TError,
{ data?: BodyType<AlertmanagertypesPostableNotificationChannelDTO> },
TContext
> => {
return useMutation(getCreateNotificationChannelMutationOptions(options));
};

View File

@@ -46,6 +46,8 @@ import type {
GetPublicDashboardPathParameters,
GetPublicDashboardWidgetQueryRange200,
GetPublicDashboardWidgetQueryRangePathParameters,
GetSystemDashboard200,
GetSystemDashboardPathParameters,
ListDashboardViews200,
ListDashboardsForUserV2200,
ListDashboardsForUserV2Params,
@@ -1885,6 +1887,108 @@ export const useMigrateDashboardV2 = <
> => {
return useMutation(getMigrateDashboardV2MutationOptions(options));
};
/**
* Returns a dashboard SigNoz ships and owns, addressed by its stable definition name (e.g. `ai-o11y-overview`) rather than its id. System dashboards are read-only and upgraded through releases. The dashboard's own `name` field carries a reserved prefix that the path segment must not include.
* @summary Get system dashboard
*/
export const getSystemDashboard = (
{ name }: GetSystemDashboardPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetSystemDashboard200>({
url: `/api/v2/dashboards/system/${name}`,
method: 'GET',
signal,
});
};
export const getGetSystemDashboardQueryKey = ({
name,
}: GetSystemDashboardPathParameters) => {
return [`/api/v2/dashboards/system/${name}`] as const;
};
export const getGetSystemDashboardQueryOptions = <
TData = Awaited<ReturnType<typeof getSystemDashboard>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ name }: GetSystemDashboardPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getSystemDashboard>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetSystemDashboardQueryKey({ name });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getSystemDashboard>>
> = ({ signal }) => getSystemDashboard({ name }, signal);
return {
queryKey,
queryFn,
enabled: !!name,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getSystemDashboard>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetSystemDashboardQueryResult = NonNullable<
Awaited<ReturnType<typeof getSystemDashboard>>
>;
export type GetSystemDashboardQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get system dashboard
*/
export function useGetSystemDashboard<
TData = Awaited<ReturnType<typeof getSystemDashboard>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ name }: GetSystemDashboardPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getSystemDashboard>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetSystemDashboardQueryOptions({ name }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get system dashboard
*/
export const invalidateGetSystemDashboard = async (
queryClient: QueryClient,
{ name }: GetSystemDashboardPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetSystemDashboardQueryKey({ name }) },
options,
);
return queryClient;
};
/**
* This endpoint returns the sanitized v2-shape dashboard data for public access. Each panel query is reduced to a safe field subset, so filters and raw query strings are not exposed.
* @summary Get public dashboard data (v2)

View File

@@ -21,18 +21,28 @@ import type {
CreateIngestionKey201,
CreateIngestionKeyLimit201,
CreateIngestionKeyLimitPathParameters,
CreateIngestionLimit201,
DeleteIngestionKeyLimitPathParameters,
DeleteIngestionKeyPathParameters,
DeleteIngestionLimitPathParameters,
GatewaytypesDeprecatedPostableIngestionKeyLimitDTO,
GatewaytypesPostableIngestionKeyDTO,
GatewaytypesPostableIngestionKeyLimitDTO,
GatewaytypesUpdatableIngestionKeyLimitDTO,
GetIngestionKey200,
GetIngestionKeyLimits200,
GetIngestionKeyLimitsPathParameters,
GetIngestionKeyPathParameters,
GetIngestionKeys200,
GetIngestionKeysParams,
GetIngestionLimit200,
GetIngestionLimitPathParameters,
RenderErrorResponseDTO,
SearchIngestionKeys200,
SearchIngestionKeysParams,
UpdateIngestionKeyLimitPathParameters,
UpdateIngestionKeyPathParameters,
UpdateIngestionLimitPathParameters,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
@@ -300,6 +310,108 @@ export const useDeleteIngestionKey = <
> => {
return useMutation(getDeleteIngestionKeyMutationOptions(options));
};
/**
* This endpoint returns an ingestion key for the workspace
* @summary Get ingestion key for workspace
*/
export const getIngestionKey = (
{ keyId }: GetIngestionKeyPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetIngestionKey200>({
url: `/api/v2/gateway/ingestion_keys/${keyId}`,
method: 'GET',
signal,
});
};
export const getGetIngestionKeyQueryKey = ({
keyId,
}: GetIngestionKeyPathParameters) => {
return [`/api/v2/gateway/ingestion_keys/${keyId}`] as const;
};
export const getGetIngestionKeyQueryOptions = <
TData = Awaited<ReturnType<typeof getIngestionKey>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ keyId }: GetIngestionKeyPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKey>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetIngestionKeyQueryKey({ keyId });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getIngestionKey>>> = ({
signal,
}) => getIngestionKey({ keyId }, signal);
return {
queryKey,
queryFn,
enabled: !!keyId,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKey>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetIngestionKeyQueryResult = NonNullable<
Awaited<ReturnType<typeof getIngestionKey>>
>;
export type GetIngestionKeyQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get ingestion key for workspace
*/
export function useGetIngestionKey<
TData = Awaited<ReturnType<typeof getIngestionKey>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ keyId }: GetIngestionKeyPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKey>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetIngestionKeyQueryOptions({ keyId }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get ingestion key for workspace
*/
export const invalidateGetIngestionKey = async (
queryClient: QueryClient,
{ keyId }: GetIngestionKeyPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetIngestionKeyQueryKey({ keyId }) },
options,
);
return queryClient;
};
/**
* This endpoint updates an ingestion key for the workspace
* @summary Update ingestion key for workspace
@@ -400,19 +512,122 @@ export const useUpdateIngestionKey = <
return useMutation(getUpdateIngestionKeyMutationOptions(options));
};
/**
* This endpoint creates an ingestion key limit
* This endpoint returns the ingestion limits for an ingestion key
* @summary Get limits for the ingestion key
*/
export const getIngestionKeyLimits = (
{ keyId }: GetIngestionKeyLimitsPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetIngestionKeyLimits200>({
url: `/api/v2/gateway/ingestion_keys/${keyId}/limits`,
method: 'GET',
signal,
});
};
export const getGetIngestionKeyLimitsQueryKey = ({
keyId,
}: GetIngestionKeyLimitsPathParameters) => {
return [`/api/v2/gateway/ingestion_keys/${keyId}/limits`] as const;
};
export const getGetIngestionKeyLimitsQueryOptions = <
TData = Awaited<ReturnType<typeof getIngestionKeyLimits>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ keyId }: GetIngestionKeyLimitsPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKeyLimits>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetIngestionKeyLimitsQueryKey({ keyId });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getIngestionKeyLimits>>
> = ({ signal }) => getIngestionKeyLimits({ keyId }, signal);
return {
queryKey,
queryFn,
enabled: !!keyId,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKeyLimits>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetIngestionKeyLimitsQueryResult = NonNullable<
Awaited<ReturnType<typeof getIngestionKeyLimits>>
>;
export type GetIngestionKeyLimitsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get limits for the ingestion key
*/
export function useGetIngestionKeyLimits<
TData = Awaited<ReturnType<typeof getIngestionKeyLimits>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ keyId }: GetIngestionKeyLimitsPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionKeyLimits>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetIngestionKeyLimitsQueryOptions({ keyId }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get limits for the ingestion key
*/
export const invalidateGetIngestionKeyLimits = async (
queryClient: QueryClient,
{ keyId }: GetIngestionKeyLimitsPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetIngestionKeyLimitsQueryKey({ keyId }) },
options,
);
return queryClient;
};
/**
* This endpoint creates an ingestion key limit.
* @deprecated
* @summary Create limit for the ingestion key
*/
export const createIngestionKeyLimit = (
{ keyId }: CreateIngestionKeyLimitPathParameters,
gatewaytypesPostableIngestionKeyLimitDTO?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO>,
gatewaytypesDeprecatedPostableIngestionKeyLimitDTO?: BodyType<GatewaytypesDeprecatedPostableIngestionKeyLimitDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateIngestionKeyLimit201>({
url: `/api/v2/gateway/ingestion_keys/${keyId}/limits`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: gatewaytypesPostableIngestionKeyLimitDTO,
data: gatewaytypesDeprecatedPostableIngestionKeyLimitDTO,
signal,
});
};
@@ -426,7 +641,7 @@ export const getCreateIngestionKeyLimitMutationOptions = <
TError,
{
pathParams: CreateIngestionKeyLimitPathParameters;
data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO>;
data?: BodyType<GatewaytypesDeprecatedPostableIngestionKeyLimitDTO>;
},
TContext
>;
@@ -435,7 +650,7 @@ export const getCreateIngestionKeyLimitMutationOptions = <
TError,
{
pathParams: CreateIngestionKeyLimitPathParameters;
data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO>;
data?: BodyType<GatewaytypesDeprecatedPostableIngestionKeyLimitDTO>;
},
TContext
> => {
@@ -452,7 +667,7 @@ export const getCreateIngestionKeyLimitMutationOptions = <
Awaited<ReturnType<typeof createIngestionKeyLimit>>,
{
pathParams: CreateIngestionKeyLimitPathParameters;
data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO>;
data?: BodyType<GatewaytypesDeprecatedPostableIngestionKeyLimitDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -467,12 +682,13 @@ export type CreateIngestionKeyLimitMutationResult = NonNullable<
Awaited<ReturnType<typeof createIngestionKeyLimit>>
>;
export type CreateIngestionKeyLimitMutationBody =
| BodyType<GatewaytypesPostableIngestionKeyLimitDTO>
| BodyType<GatewaytypesDeprecatedPostableIngestionKeyLimitDTO>
| undefined;
export type CreateIngestionKeyLimitMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Create limit for the ingestion key
*/
export const useCreateIngestionKeyLimit = <
@@ -484,7 +700,7 @@ export const useCreateIngestionKeyLimit = <
TError,
{
pathParams: CreateIngestionKeyLimitPathParameters;
data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO>;
data?: BodyType<GatewaytypesDeprecatedPostableIngestionKeyLimitDTO>;
},
TContext
>;
@@ -493,7 +709,7 @@ export const useCreateIngestionKeyLimit = <
TError,
{
pathParams: CreateIngestionKeyLimitPathParameters;
data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO>;
data?: BodyType<GatewaytypesDeprecatedPostableIngestionKeyLimitDTO>;
},
TContext
> => {
@@ -501,6 +717,7 @@ export const useCreateIngestionKeyLimit = <
};
/**
* This endpoint deletes an ingestion key limit
* @deprecated
* @summary Delete limit for the ingestion key
*/
export const deleteIngestionKeyLimit = (
@@ -559,6 +776,7 @@ export type DeleteIngestionKeyLimitMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Delete limit for the ingestion key
*/
export const useDeleteIngestionKeyLimit = <
@@ -580,7 +798,8 @@ export const useDeleteIngestionKeyLimit = <
return useMutation(getDeleteIngestionKeyLimitMutationOptions(options));
};
/**
* This endpoint updates an ingestion key limit
* This endpoint updates an ingestion key limit.
* @deprecated
* @summary Update limit for the ingestion key
*/
export const updateIngestionKeyLimit = (
@@ -653,6 +872,7 @@ export type UpdateIngestionKeyLimitMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Update limit for the ingestion key
*/
export const useUpdateIngestionKeyLimit = <
@@ -779,3 +999,370 @@ export const invalidateSearchIngestionKeys = async (
return queryClient;
};
/**
* This endpoint creates an ingestion limit for the ingestion key referenced by keyId
* @summary Create ingestion limit
*/
export const createIngestionLimit = (
gatewaytypesPostableIngestionKeyLimitDTO?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateIngestionLimit201>({
url: `/api/v2/gateway/ingestion_limits`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: gatewaytypesPostableIngestionKeyLimitDTO,
signal,
});
};
export const getCreateIngestionLimitMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createIngestionLimit>>,
TError,
{ data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createIngestionLimit>>,
TError,
{ data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO> },
TContext
> => {
const mutationKey = ['createIngestionLimit'];
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 createIngestionLimit>>,
{ data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO> }
> = (props) => {
const { data } = props ?? {};
return createIngestionLimit(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateIngestionLimitMutationResult = NonNullable<
Awaited<ReturnType<typeof createIngestionLimit>>
>;
export type CreateIngestionLimitMutationBody =
| BodyType<GatewaytypesPostableIngestionKeyLimitDTO>
| undefined;
export type CreateIngestionLimitMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create ingestion limit
*/
export const useCreateIngestionLimit = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createIngestionLimit>>,
TError,
{ data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createIngestionLimit>>,
TError,
{ data?: BodyType<GatewaytypesPostableIngestionKeyLimitDTO> },
TContext
> => {
return useMutation(getCreateIngestionLimitMutationOptions(options));
};
/**
* This endpoint deletes an ingestion limit
* @summary Delete ingestion limit
*/
export const deleteIngestionLimit = (
{ limitId }: DeleteIngestionLimitPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/gateway/ingestion_limits/${limitId}`,
method: 'DELETE',
signal,
});
};
export const getDeleteIngestionLimitMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteIngestionLimit>>,
TError,
{ pathParams: DeleteIngestionLimitPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteIngestionLimit>>,
TError,
{ pathParams: DeleteIngestionLimitPathParameters },
TContext
> => {
const mutationKey = ['deleteIngestionLimit'];
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 deleteIngestionLimit>>,
{ pathParams: DeleteIngestionLimitPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteIngestionLimit(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteIngestionLimitMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteIngestionLimit>>
>;
export type DeleteIngestionLimitMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete ingestion limit
*/
export const useDeleteIngestionLimit = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteIngestionLimit>>,
TError,
{ pathParams: DeleteIngestionLimitPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteIngestionLimit>>,
TError,
{ pathParams: DeleteIngestionLimitPathParameters },
TContext
> => {
return useMutation(getDeleteIngestionLimitMutationOptions(options));
};
/**
* This endpoint returns an ingestion limit
* @summary Get ingestion limit
*/
export const getIngestionLimit = (
{ limitId }: GetIngestionLimitPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetIngestionLimit200>({
url: `/api/v2/gateway/ingestion_limits/${limitId}`,
method: 'GET',
signal,
});
};
export const getGetIngestionLimitQueryKey = ({
limitId,
}: GetIngestionLimitPathParameters) => {
return [`/api/v2/gateway/ingestion_limits/${limitId}`] as const;
};
export const getGetIngestionLimitQueryOptions = <
TData = Awaited<ReturnType<typeof getIngestionLimit>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ limitId }: GetIngestionLimitPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionLimit>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetIngestionLimitQueryKey({ limitId });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getIngestionLimit>>
> = ({ signal }) => getIngestionLimit({ limitId }, signal);
return {
queryKey,
queryFn,
enabled: !!limitId,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getIngestionLimit>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetIngestionLimitQueryResult = NonNullable<
Awaited<ReturnType<typeof getIngestionLimit>>
>;
export type GetIngestionLimitQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get ingestion limit
*/
export function useGetIngestionLimit<
TData = Awaited<ReturnType<typeof getIngestionLimit>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ limitId }: GetIngestionLimitPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getIngestionLimit>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetIngestionLimitQueryOptions({ limitId }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get ingestion limit
*/
export const invalidateGetIngestionLimit = async (
queryClient: QueryClient,
{ limitId }: GetIngestionLimitPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetIngestionLimitQueryKey({ limitId }) },
options,
);
return queryClient;
};
/**
* This endpoint updates an ingestion limit
* @summary Update ingestion limit
*/
export const updateIngestionLimit = (
{ limitId }: UpdateIngestionLimitPathParameters,
gatewaytypesUpdatableIngestionKeyLimitDTO?: BodyType<GatewaytypesUpdatableIngestionKeyLimitDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/gateway/ingestion_limits/${limitId}`,
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
data: gatewaytypesUpdatableIngestionKeyLimitDTO,
signal,
});
};
export const getUpdateIngestionLimitMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateIngestionLimit>>,
TError,
{
pathParams: UpdateIngestionLimitPathParameters;
data?: BodyType<GatewaytypesUpdatableIngestionKeyLimitDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateIngestionLimit>>,
TError,
{
pathParams: UpdateIngestionLimitPathParameters;
data?: BodyType<GatewaytypesUpdatableIngestionKeyLimitDTO>;
},
TContext
> => {
const mutationKey = ['updateIngestionLimit'];
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 updateIngestionLimit>>,
{
pathParams: UpdateIngestionLimitPathParameters;
data?: BodyType<GatewaytypesUpdatableIngestionKeyLimitDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateIngestionLimit(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateIngestionLimitMutationResult = NonNullable<
Awaited<ReturnType<typeof updateIngestionLimit>>
>;
export type UpdateIngestionLimitMutationBody =
| BodyType<GatewaytypesUpdatableIngestionKeyLimitDTO>
| undefined;
export type UpdateIngestionLimitMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update ingestion limit
*/
export const useUpdateIngestionLimit = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateIngestionLimit>>,
TError,
{
pathParams: UpdateIngestionLimitPathParameters;
data?: BodyType<GatewaytypesUpdatableIngestionKeyLimitDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateIngestionLimit>>,
TError,
{
pathParams: UpdateIngestionLimitPathParameters;
data?: BodyType<GatewaytypesUpdatableIngestionKeyLimitDTO>;
},
TContext
> => {
return useMutation(getUpdateIngestionLimitMutationOptions(options));
};

View File

@@ -0,0 +1,702 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
*/
import { useMutation, useQuery } from 'react-query';
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
ActivateLicense201,
DeleteLicensePathParameters,
GetActiveLicense200,
GetLicense200,
GetLicensePathParameters,
LicensetypesPostableLicenseDTO,
ListLicenses200,
RefreshLicensePathParameters,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* This endpoint validates the license key with the upstream server and activates the license for the organization.
* @deprecated
* @summary Activate a license.
*/
export const activateLicenseDeprecated = (
licensetypesPostableLicenseDTO?: BodyType<LicensetypesPostableLicenseDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v3/licenses`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: licensetypesPostableLicenseDTO,
signal,
});
};
export const getActivateLicenseDeprecatedMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof activateLicenseDeprecated>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof activateLicenseDeprecated>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
> => {
const mutationKey = ['activateLicenseDeprecated'];
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 activateLicenseDeprecated>>,
{ data?: BodyType<LicensetypesPostableLicenseDTO> }
> = (props) => {
const { data } = props ?? {};
return activateLicenseDeprecated(data);
};
return { mutationFn, ...mutationOptions };
};
export type ActivateLicenseDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof activateLicenseDeprecated>>
>;
export type ActivateLicenseDeprecatedMutationBody =
| BodyType<LicensetypesPostableLicenseDTO>
| undefined;
export type ActivateLicenseDeprecatedMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Activate a license.
*/
export const useActivateLicenseDeprecated = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof activateLicenseDeprecated>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof activateLicenseDeprecated>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
> => {
return useMutation(getActivateLicenseDeprecatedMutationOptions(options));
};
/**
* This endpoint refreshes the active license of the organization from the upstream server.
* @deprecated
* @summary Refresh a license.
*/
export const refreshLicenseDeprecated = (signal?: AbortSignal) => {
return GeneratedAPIInstance<void>({
url: `/api/v3/licenses`,
method: 'PUT',
signal,
});
};
export const getRefreshLicenseDeprecatedMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof refreshLicenseDeprecated>>,
TError,
void,
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof refreshLicenseDeprecated>>,
TError,
void,
TContext
> => {
const mutationKey = ['refreshLicenseDeprecated'];
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 refreshLicenseDeprecated>>,
void
> = () => {
return refreshLicenseDeprecated();
};
return { mutationFn, ...mutationOptions };
};
export type RefreshLicenseDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof refreshLicenseDeprecated>>
>;
export type RefreshLicenseDeprecatedMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Refresh a license.
*/
export const useRefreshLicenseDeprecated = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof refreshLicenseDeprecated>>,
TError,
void,
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof refreshLicenseDeprecated>>,
TError,
void,
TContext
> => {
return useMutation(getRefreshLicenseDeprecatedMutationOptions(options));
};
/**
* This endpoint lists all the licenses of the organization.
* @summary List licenses.
*/
export const listLicenses = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListLicenses200>({
url: `/api/v4/licenses`,
method: 'GET',
signal,
});
};
export const getListLicensesQueryKey = () => {
return [`/api/v4/licenses`] as const;
};
export const getListLicensesQueryOptions = <
TData = Awaited<ReturnType<typeof listLicenses>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listLicenses>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListLicensesQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listLicenses>>> = ({
signal,
}) => listLicenses(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listLicenses>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListLicensesQueryResult = NonNullable<
Awaited<ReturnType<typeof listLicenses>>
>;
export type ListLicensesQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List licenses.
*/
export function useListLicenses<
TData = Awaited<ReturnType<typeof listLicenses>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listLicenses>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListLicensesQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List licenses.
*/
export const invalidateListLicenses = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListLicensesQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint validates the license key with the upstream server and activates the license for the organization.
* @summary Activate a license.
*/
export const activateLicense = (
licensetypesPostableLicenseDTO?: BodyType<LicensetypesPostableLicenseDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ActivateLicense201>({
url: `/api/v4/licenses`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: licensetypesPostableLicenseDTO,
signal,
});
};
export const getActivateLicenseMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof activateLicense>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof activateLicense>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
> => {
const mutationKey = ['activateLicense'];
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 activateLicense>>,
{ data?: BodyType<LicensetypesPostableLicenseDTO> }
> = (props) => {
const { data } = props ?? {};
return activateLicense(data);
};
return { mutationFn, ...mutationOptions };
};
export type ActivateLicenseMutationResult = NonNullable<
Awaited<ReturnType<typeof activateLicense>>
>;
export type ActivateLicenseMutationBody =
| BodyType<LicensetypesPostableLicenseDTO>
| undefined;
export type ActivateLicenseMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Activate a license.
*/
export const useActivateLicense = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof activateLicense>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof activateLicense>>,
TError,
{ data?: BodyType<LicensetypesPostableLicenseDTO> },
TContext
> => {
return useMutation(getActivateLicenseMutationOptions(options));
};
/**
* This endpoint deletes the license by id. Licenses managed by SigNoz Cloud cannot be deleted.
* @summary Delete a license.
*/
export const deleteLicense = (
{ id }: DeleteLicensePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v4/licenses/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteLicenseMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteLicense>>,
TError,
{ pathParams: DeleteLicensePathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteLicense>>,
TError,
{ pathParams: DeleteLicensePathParameters },
TContext
> => {
const mutationKey = ['deleteLicense'];
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 deleteLicense>>,
{ pathParams: DeleteLicensePathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteLicense(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteLicenseMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteLicense>>
>;
export type DeleteLicenseMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete a license.
*/
export const useDeleteLicense = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteLicense>>,
TError,
{ pathParams: DeleteLicensePathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteLicense>>,
TError,
{ pathParams: DeleteLicensePathParameters },
TContext
> => {
return useMutation(getDeleteLicenseMutationOptions(options));
};
/**
* This endpoint gets the license by id.
* @summary Get a license.
*/
export const getLicense = (
{ id }: GetLicensePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetLicense200>({
url: `/api/v4/licenses/${id}`,
method: 'GET',
signal,
});
};
export const getGetLicenseQueryKey = ({ id }: GetLicensePathParameters) => {
return [`/api/v4/licenses/${id}`] as const;
};
export const getGetLicenseQueryOptions = <
TData = Awaited<ReturnType<typeof getLicense>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetLicensePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getLicense>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetLicenseQueryKey({ id });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getLicense>>> = ({
signal,
}) => getLicense({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof getLicense>>, TError, TData> & {
queryKey: QueryKey;
};
};
export type GetLicenseQueryResult = NonNullable<
Awaited<ReturnType<typeof getLicense>>
>;
export type GetLicenseQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get a license.
*/
export function useGetLicense<
TData = Awaited<ReturnType<typeof getLicense>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetLicensePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getLicense>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetLicenseQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get a license.
*/
export const invalidateGetLicense = async (
queryClient: QueryClient,
{ id }: GetLicensePathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetLicenseQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint refreshes the active license of the organization from the upstream server.
* @summary Refresh a license.
*/
export const refreshLicense = (
{ id }: RefreshLicensePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v4/licenses/${id}`,
method: 'PUT',
signal,
});
};
export const getRefreshLicenseMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof refreshLicense>>,
TError,
{ pathParams: RefreshLicensePathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof refreshLicense>>,
TError,
{ pathParams: RefreshLicensePathParameters },
TContext
> => {
const mutationKey = ['refreshLicense'];
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 refreshLicense>>,
{ pathParams: RefreshLicensePathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return refreshLicense(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type RefreshLicenseMutationResult = NonNullable<
Awaited<ReturnType<typeof refreshLicense>>
>;
export type RefreshLicenseMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Refresh a license.
*/
export const useRefreshLicense = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof refreshLicense>>,
TError,
{ pathParams: RefreshLicensePathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof refreshLicense>>,
TError,
{ pathParams: RefreshLicensePathParameters },
TContext
> => {
return useMutation(getRefreshLicenseMutationOptions(options));
};
/**
* This endpoint gets the active license of the organization.
* @summary Get the active license.
*/
export const getActiveLicense = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetActiveLicense200>({
url: `/api/v4/licenses/active`,
method: 'GET',
signal,
});
};
export const getGetActiveLicenseQueryKey = () => {
return [`/api/v4/licenses/active`] as const;
};
export const getGetActiveLicenseQueryOptions = <
TData = Awaited<ReturnType<typeof getActiveLicense>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getActiveLicense>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetActiveLicenseQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getActiveLicense>>> = ({
signal,
}) => getActiveLicense(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getActiveLicense>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetActiveLicenseQueryResult = NonNullable<
Awaited<ReturnType<typeof getActiveLicense>>
>;
export type GetActiveLicenseQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get the active license.
*/
export function useGetActiveLicense<
TData = Awaited<ReturnType<typeof getActiveLicense>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getActiveLicense>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetActiveLicenseQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get the active license.
*/
export const invalidateGetActiveLicense = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetActiveLicenseQueryKey() },
options,
);
return queryClient;
};

View File

@@ -0,0 +1,396 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
*/
import { useMutation, useQuery } from 'react-query';
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
PrometheusErrorResponseSchemaDTO,
PrometheusQueryParams,
PrometheusQueryPostParams,
PrometheusQueryRangeParams,
PrometheusQueryRangePostParams,
PrometheusSuccessResponseSchemaDTO,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType } from '../../../generatedAPIInstance';
/**
* Prometheus-compatible endpoint: the request and response contract is the upstream Prometheus HTTP API (https://prometheus.io/docs/prometheus/latest/querying/api/). Parameters are accepted as URL query parameters or a form-encoded body, on GET and POST alike.
* @summary Prometheus instant query
*/
export const prometheusQuery = (
params: PrometheusQueryParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<PrometheusSuccessResponseSchemaDTO>({
url: `/prometheus/api/v1/query`,
method: 'GET',
params,
signal,
});
};
export const getPrometheusQueryQueryKey = (params?: PrometheusQueryParams) => {
return [`/prometheus/api/v1/query`, ...(params ? [params] : [])] as const;
};
export const getPrometheusQueryQueryOptions = <
TData = Awaited<ReturnType<typeof prometheusQuery>>,
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
>(
params: PrometheusQueryParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof prometheusQuery>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getPrometheusQueryQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof prometheusQuery>>> = ({
signal,
}) => prometheusQuery(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof prometheusQuery>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type PrometheusQueryQueryResult = NonNullable<
Awaited<ReturnType<typeof prometheusQuery>>
>;
export type PrometheusQueryQueryError = ErrorType<
PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO
>;
/**
* @summary Prometheus instant query
*/
export function usePrometheusQuery<
TData = Awaited<ReturnType<typeof prometheusQuery>>,
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
>(
params: PrometheusQueryParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof prometheusQuery>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getPrometheusQueryQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Prometheus instant query
*/
export const invalidatePrometheusQuery = async (
queryClient: QueryClient,
params: PrometheusQueryParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getPrometheusQueryQueryKey(params) },
options,
);
return queryClient;
};
/**
* Prometheus-compatible endpoint: the request and response contract is the upstream Prometheus HTTP API (https://prometheus.io/docs/prometheus/latest/querying/api/). Parameters are accepted as URL query parameters or a form-encoded body, on GET and POST alike.
* @summary Prometheus instant query
*/
export const prometheusQueryPost = (
params: PrometheusQueryPostParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<PrometheusSuccessResponseSchemaDTO>({
url: `/prometheus/api/v1/query`,
method: 'POST',
params,
signal,
});
};
export const getPrometheusQueryPostMutationOptions = <
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof prometheusQueryPost>>,
TError,
{ params: PrometheusQueryPostParams },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof prometheusQueryPost>>,
TError,
{ params: PrometheusQueryPostParams },
TContext
> => {
const mutationKey = ['prometheusQueryPost'];
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 prometheusQueryPost>>,
{ params: PrometheusQueryPostParams }
> = (props) => {
const { params } = props ?? {};
return prometheusQueryPost(params);
};
return { mutationFn, ...mutationOptions };
};
export type PrometheusQueryPostMutationResult = NonNullable<
Awaited<ReturnType<typeof prometheusQueryPost>>
>;
export type PrometheusQueryPostMutationError = ErrorType<
PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO
>;
/**
* @summary Prometheus instant query
*/
export const usePrometheusQueryPost = <
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof prometheusQueryPost>>,
TError,
{ params: PrometheusQueryPostParams },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof prometheusQueryPost>>,
TError,
{ params: PrometheusQueryPostParams },
TContext
> => {
return useMutation(getPrometheusQueryPostMutationOptions(options));
};
/**
* Prometheus-compatible endpoint: the request and response contract is the upstream Prometheus HTTP API (https://prometheus.io/docs/prometheus/latest/querying/api/). Parameters are accepted as URL query parameters or a form-encoded body, on GET and POST alike.
* @summary Prometheus range query
*/
export const prometheusQueryRange = (
params: PrometheusQueryRangeParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<PrometheusSuccessResponseSchemaDTO>({
url: `/prometheus/api/v1/query_range`,
method: 'GET',
params,
signal,
});
};
export const getPrometheusQueryRangeQueryKey = (
params?: PrometheusQueryRangeParams,
) => {
return [
`/prometheus/api/v1/query_range`,
...(params ? [params] : []),
] as const;
};
export const getPrometheusQueryRangeQueryOptions = <
TData = Awaited<ReturnType<typeof prometheusQueryRange>>,
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
>(
params: PrometheusQueryRangeParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof prometheusQueryRange>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getPrometheusQueryRangeQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof prometheusQueryRange>>
> = ({ signal }) => prometheusQueryRange(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof prometheusQueryRange>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type PrometheusQueryRangeQueryResult = NonNullable<
Awaited<ReturnType<typeof prometheusQueryRange>>
>;
export type PrometheusQueryRangeQueryError = ErrorType<
PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO
>;
/**
* @summary Prometheus range query
*/
export function usePrometheusQueryRange<
TData = Awaited<ReturnType<typeof prometheusQueryRange>>,
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
>(
params: PrometheusQueryRangeParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof prometheusQueryRange>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getPrometheusQueryRangeQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Prometheus range query
*/
export const invalidatePrometheusQueryRange = async (
queryClient: QueryClient,
params: PrometheusQueryRangeParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getPrometheusQueryRangeQueryKey(params) },
options,
);
return queryClient;
};
/**
* Prometheus-compatible endpoint: the request and response contract is the upstream Prometheus HTTP API (https://prometheus.io/docs/prometheus/latest/querying/api/). Parameters are accepted as URL query parameters or a form-encoded body, on GET and POST alike.
* @summary Prometheus range query
*/
export const prometheusQueryRangePost = (
params: PrometheusQueryRangePostParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<PrometheusSuccessResponseSchemaDTO>({
url: `/prometheus/api/v1/query_range`,
method: 'POST',
params,
signal,
});
};
export const getPrometheusQueryRangePostMutationOptions = <
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof prometheusQueryRangePost>>,
TError,
{ params: PrometheusQueryRangePostParams },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof prometheusQueryRangePost>>,
TError,
{ params: PrometheusQueryRangePostParams },
TContext
> => {
const mutationKey = ['prometheusQueryRangePost'];
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 prometheusQueryRangePost>>,
{ params: PrometheusQueryRangePostParams }
> = (props) => {
const { params } = props ?? {};
return prometheusQueryRangePost(params);
};
return { mutationFn, ...mutationOptions };
};
export type PrometheusQueryRangePostMutationResult = NonNullable<
Awaited<ReturnType<typeof prometheusQueryRangePost>>
>;
export type PrometheusQueryRangePostMutationError = ErrorType<
PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO
>;
/**
* @summary Prometheus range query
*/
export const usePrometheusQueryRangePost = <
TError = ErrorType<PrometheusErrorResponseSchemaDTO | RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof prometheusQueryRangePost>>,
TError,
{ params: PrometheusQueryRangePostParams },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof prometheusQueryRangePost>>,
TError,
{ params: PrometheusQueryRangePostParams },
TContext
> => {
return useMutation(getPrometheusQueryRangePostMutationOptions(options));
};

View File

@@ -0,0 +1,316 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
*/
import { useMutation, useQuery } from 'react-query';
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
GetQuickFilters200,
GetQuickFiltersPathParameters,
ListQuickFilters200,
QuickfiltertypesUpdatableQuickFiltersDTO,
RenderErrorResponseDTO,
UpdateQuickFiltersPathParameters,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* Returns the org's quick filters for every source, each filter as a telemetry field key.
* @summary List quick filters
*/
export const listQuickFilters = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListQuickFilters200>({
url: `/api/v2/quick_filters`,
method: 'GET',
signal,
});
};
export const getListQuickFiltersQueryKey = () => {
return [`/api/v2/quick_filters`] as const;
};
export const getListQuickFiltersQueryOptions = <
TData = Awaited<ReturnType<typeof listQuickFilters>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listQuickFilters>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListQuickFiltersQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listQuickFilters>>> = ({
signal,
}) => listQuickFilters(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listQuickFilters>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListQuickFiltersQueryResult = NonNullable<
Awaited<ReturnType<typeof listQuickFilters>>
>;
export type ListQuickFiltersQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List quick filters
*/
export function useListQuickFilters<
TData = Awaited<ReturnType<typeof listQuickFilters>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listQuickFilters>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListQuickFiltersQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List quick filters
*/
export const invalidateListQuickFilters = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListQuickFiltersQueryKey() },
options,
);
return queryClient;
};
/**
* Returns the org's quick filters for one source, each filter as a telemetry field key.
* @summary Get a source's quick filters
*/
export const getQuickFilters = (
{ source }: GetQuickFiltersPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetQuickFilters200>({
url: `/api/v2/quick_filters/${source}`,
method: 'GET',
signal,
});
};
export const getGetQuickFiltersQueryKey = ({
source,
}: GetQuickFiltersPathParameters) => {
return [`/api/v2/quick_filters/${source}`] as const;
};
export const getGetQuickFiltersQueryOptions = <
TData = Awaited<ReturnType<typeof getQuickFilters>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ source }: GetQuickFiltersPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getQuickFilters>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetQuickFiltersQueryKey({ source });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getQuickFilters>>> = ({
signal,
}) => getQuickFilters({ source }, signal);
return {
queryKey,
queryFn,
enabled: !!source,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getQuickFilters>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetQuickFiltersQueryResult = NonNullable<
Awaited<ReturnType<typeof getQuickFilters>>
>;
export type GetQuickFiltersQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get a source's quick filters
*/
export function useGetQuickFilters<
TData = Awaited<ReturnType<typeof getQuickFilters>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ source }: GetQuickFiltersPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getQuickFilters>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetQuickFiltersQueryOptions({ source }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get a source's quick filters
*/
export const invalidateGetQuickFilters = async (
queryClient: QueryClient,
{ source }: GetQuickFiltersPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetQuickFiltersQueryKey({ source }) },
options,
);
return queryClient;
};
/**
* Replaces the org's quick filters for the source named in the path.
* @summary Update quick filters
*/
export const updateQuickFilters = (
{ source }: UpdateQuickFiltersPathParameters,
quickfiltertypesUpdatableQuickFiltersDTO?: BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/quick_filters/${source}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: quickfiltertypesUpdatableQuickFiltersDTO,
signal,
});
};
export const getUpdateQuickFiltersMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateQuickFilters>>,
TError,
{
pathParams: UpdateQuickFiltersPathParameters;
data?: BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateQuickFilters>>,
TError,
{
pathParams: UpdateQuickFiltersPathParameters;
data?: BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>;
},
TContext
> => {
const mutationKey = ['updateQuickFilters'];
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 updateQuickFilters>>,
{
pathParams: UpdateQuickFiltersPathParameters;
data?: BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateQuickFilters(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateQuickFiltersMutationResult = NonNullable<
Awaited<ReturnType<typeof updateQuickFilters>>
>;
export type UpdateQuickFiltersMutationBody =
| BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>
| undefined;
export type UpdateQuickFiltersMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update quick filters
*/
export const useUpdateQuickFilters = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateQuickFilters>>,
TError,
{
pathParams: UpdateQuickFiltersPathParameters;
data?: BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateQuickFilters>>,
TError,
{
pathParams: UpdateQuickFiltersPathParameters;
data?: BodyType<QuickfiltertypesUpdatableQuickFiltersDTO>;
},
TContext
> => {
return useMutation(getUpdateQuickFiltersMutationOptions(options));
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,280 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'pnpm generate:api'
* SigNoz
*/
import { useMutation, useQuery } from 'react-query';
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type {
CreateSubscription201,
GetSubscription200,
RenderErrorResponseDTO,
SubscriptiontypesPostableSubscriptionDTO,
UpdateSubscription200,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* This endpoint gets the organization's subscription along with its usage and billing details.
* @summary Get the subscription.
*/
export const getSubscription = (signal?: AbortSignal) => {
return GeneratedAPIInstance<GetSubscription200>({
url: `/api/v1/subscriptions`,
method: 'GET',
signal,
});
};
export const getGetSubscriptionQueryKey = () => {
return [`/api/v1/subscriptions`] as const;
};
export const getGetSubscriptionQueryOptions = <
TData = Awaited<ReturnType<typeof getSubscription>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getSubscription>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetSubscriptionQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getSubscription>>> = ({
signal,
}) => getSubscription(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getSubscription>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetSubscriptionQueryResult = NonNullable<
Awaited<ReturnType<typeof getSubscription>>
>;
export type GetSubscriptionQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get the subscription.
*/
export function useGetSubscription<
TData = Awaited<ReturnType<typeof getSubscription>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getSubscription>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetSubscriptionQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get the subscription.
*/
export const invalidateGetSubscription = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetSubscriptionQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint creates a subscription for the organization.
* @summary Create a subscription.
*/
export const createSubscription = (
subscriptiontypesPostableSubscriptionDTO?: BodyType<SubscriptiontypesPostableSubscriptionDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateSubscription201>({
url: `/api/v1/subscriptions`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: subscriptiontypesPostableSubscriptionDTO,
signal,
});
};
export const getCreateSubscriptionMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createSubscription>>,
TError,
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createSubscription>>,
TError,
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
TContext
> => {
const mutationKey = ['createSubscription'];
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 createSubscription>>,
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> }
> = (props) => {
const { data } = props ?? {};
return createSubscription(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateSubscriptionMutationResult = NonNullable<
Awaited<ReturnType<typeof createSubscription>>
>;
export type CreateSubscriptionMutationBody =
| BodyType<SubscriptiontypesPostableSubscriptionDTO>
| undefined;
export type CreateSubscriptionMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create a subscription.
*/
export const useCreateSubscription = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createSubscription>>,
TError,
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createSubscription>>,
TError,
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
TContext
> => {
return useMutation(getCreateSubscriptionMutationOptions(options));
};
/**
* This endpoint updates the organization's subscription.
* @summary Update the subscription.
*/
export const updateSubscription = (
subscriptiontypesPostableSubscriptionDTO?: BodyType<SubscriptiontypesPostableSubscriptionDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<UpdateSubscription200>({
url: `/api/v1/subscriptions`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: subscriptiontypesPostableSubscriptionDTO,
signal,
});
};
export const getUpdateSubscriptionMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateSubscription>>,
TError,
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateSubscription>>,
TError,
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
TContext
> => {
const mutationKey = ['updateSubscription'];
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 updateSubscription>>,
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> }
> = (props) => {
const { data } = props ?? {};
return updateSubscription(data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateSubscriptionMutationResult = NonNullable<
Awaited<ReturnType<typeof updateSubscription>>
>;
export type UpdateSubscriptionMutationBody =
| BodyType<SubscriptiontypesPostableSubscriptionDTO>
| undefined;
export type UpdateSubscriptionMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update the subscription.
*/
export const useUpdateSubscription = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateSubscription>>,
TError,
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateSubscription>>,
TError,
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
TContext
> => {
return useMutation(getUpdateSubscriptionMutationOptions(options));
};

View File

@@ -1,25 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/quickFilters/getCustomFilters';
const getCustomFilters = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const { signal } = props;
try {
const response = await axios.get(`/orgs/me/filters/${signal}`);
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getCustomFilters;

View File

@@ -1,13 +0,0 @@
import axios from 'api';
import { AxiosError } from 'axios';
import { SuccessResponse } from 'types/api';
import { UpdateCustomFiltersProps } from 'types/api/quickFilters/updateCustomFilters';
const updateCustomFiltersAPI = async (
props: UpdateCustomFiltersProps,
): Promise<SuccessResponse<void> | AxiosError> =>
axios.put(`/orgs/me/filters`, {
...props.data,
});
export default updateCustomFiltersAPI;

View File

@@ -1,28 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import {
CheckoutRequestPayloadProps,
CheckoutSuccessPayloadProps,
PayloadProps,
} from 'types/api/billing/checkout';
const updateCreditCardApi = async (
props: CheckoutRequestPayloadProps,
): Promise<SuccessResponseV2<CheckoutSuccessPayloadProps>> => {
try {
const response = await axios.post<PayloadProps>('/checkout', {
url: props.url,
});
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default updateCreditCardApi;

View File

@@ -1,23 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/create';
import { Dashboard } from 'types/api/dashboard/getAll';
const create = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
try {
const response = await axios.post<PayloadProps>('/dashboards', {
...props,
});
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default create;

View File

@@ -1,19 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Dashboard, PayloadProps } from 'types/api/dashboard/getAll';
const getAll = async (): Promise<SuccessResponseV2<Dashboard[]>> => {
try {
const response = await axios.get<PayloadProps>('/dashboards');
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getAll;

View File

@@ -1,21 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/delete';
const deleteDashboard = async (
props: Props,
): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.delete<PayloadProps>(`/dashboards/${props.id}`);
return {
httpStatusCode: response.status,
data: null,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default deleteDashboard;

View File

@@ -1,20 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/get';
import { Dashboard } from 'types/api/dashboard/getAll';
const get = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
try {
const response = await axios.get<PayloadProps>(`/dashboards/${props.id}`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default get;

View File

@@ -1,23 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/lockUnlock';
const lock = async (props: Props): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.put<PayloadProps>(
`/dashboards/${props.id}/lock`,
{ lock: props.lock },
);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default lock;

View File

@@ -1,23 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
import { PayloadProps, Props } from 'types/api/dashboard/update';
const update = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
try {
const response = await axios.put<PayloadProps>(`/dashboards/${props.id}`, {
...props.data,
});
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default update;

View File

@@ -1,28 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import {
CheckoutRequestPayloadProps,
CheckoutSuccessPayloadProps,
PayloadProps,
} from 'types/api/billing/checkout';
const manageCreditCardApi = async (
props: CheckoutRequestPayloadProps,
): Promise<SuccessResponseV2<CheckoutSuccessPayloadProps>> => {
try {
const response = await axios.post<PayloadProps>('/portal', {
url: props.url,
});
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default manageCreditCardApi;

View File

@@ -1,25 +0,0 @@
import { ApiV3Instance as axios } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import {
LicenseEventQueueResModel,
PayloadProps,
} from 'types/api/licensesV3/getActive';
const getActive = async (): Promise<
SuccessResponseV2<LicenseEventQueueResModel>
> => {
try {
const response = await axios.get<PayloadProps>('/licenses/active');
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getActive;

View File

@@ -1,24 +0,0 @@
import { ApiV3Instance as axios } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/licenses/apply';
const apply = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {
try {
const response = await axios.post<PayloadProps>('/licenses', {
key: props.key,
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default apply;

View File

@@ -1,20 +0,0 @@
import { ApiV3Instance as axios } from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps } from 'types/api/licenses/apply';
const apply = async (): Promise<SuccessResponseV2<PayloadProps>> => {
try {
const response = await axios.put<PayloadProps>('/licenses');
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default apply;

View File

@@ -2,8 +2,10 @@ import { cloneDeep, isEmpty } from 'lodash-es';
import { SuccessResponse, Warning } from 'types/api';
import { MetricRangePayloadV3 } from 'types/api/metrics/getQueryRange';
import {
BuilderQuery,
DistributionData,
MetricRangePayloadV5,
QueryEnvelope,
QueryRangeRequestV5,
RawData,
ScalarData,
@@ -11,6 +13,11 @@ import {
} from 'types/api/v5/queryRange';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
const isBuilderQueryEnvelope = (
envelope: QueryEnvelope,
): envelope is QueryEnvelope & { spec: BuilderQuery } =>
envelope.type === 'builder_query' || envelope.type === 'builder_ai_query';
function getColName(
col: ScalarData['columns'][number],
legendMap: Record<string, string>,
@@ -409,21 +416,19 @@ export function convertV5ResponseToLegacy(
const v5Data = payload?.data;
const aggregationPerQuery =
params?.compositeQuery?.queries
?.filter((query) => query.type === 'builder_query')
.reduce(
(acc, query) => {
if (
query.type === 'builder_query' &&
'aggregations' in query.spec &&
query.spec.name
) {
acc[query.spec.name] = query.spec.aggregations;
}
return acc;
},
{} as Record<string, any>,
) || {};
params?.compositeQuery?.queries?.filter(isBuilderQueryEnvelope).reduce(
(acc, query) => {
if (
isBuilderQueryEnvelope(query) &&
'aggregations' in query.spec &&
query.spec.name
) {
acc[query.spec.name] = query.spec.aggregations;
}
return acc;
},
{} as Record<string, any>,
) || {};
// clickhouse_sql queries have no aggregation metadata; their value columns
// are named/keyed by the real SQL alias the response carries (see getColId).

View File

@@ -14,6 +14,7 @@ import {
QueryBuilderFormula as V5QueryBuilderFormula,
QueryEnvelope,
QueryRangePayloadV5,
RequestType,
} from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
@@ -935,3 +936,41 @@ describe('convertBuilderQueriesToV5 having normalization', () => {
});
});
});
describe('convertBuilderQueriesToV5 builder query type', () => {
const buildEnvelope = (
builderQueryType: IBuilderQuery['builderQueryType'],
requestType: RequestType,
): QueryEnvelope => {
const [envelope] = convertBuilderQueriesToV5(
{
A: {
dataSource: DataSource.TRACES,
queryName: 'A',
builderQueryType,
} as unknown as IBuilderQuery,
},
requestType,
);
return envelope;
};
it.each<[RequestType]>([
['trace'],
['raw'],
['time_series'],
['scalar'],
['distribution'],
])('sends builder_ai_query for the %s request type', (requestType) => {
expect(buildEnvelope('builder_ai_query', requestType).type).toBe(
'builder_ai_query',
);
});
it.each<[string, IBuilderQuery['builderQueryType']]>([
['an unmarked query', undefined],
['an explicitly generic query', 'builder_query'],
])('sends builder_query for %s', (_label, builderQueryType) => {
expect(buildEnvelope(builderQueryType, 'trace').type).toBe('builder_query');
});
});

View File

@@ -6,6 +6,7 @@ import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEmpty } from 'lodash-es';
import { DynamicVariableSuggestion } from 'providers/Dashboard/store/dynamicVariableSuggestions';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
IBuilderQuery,
@@ -364,7 +365,7 @@ export function convertBuilderQueriesToV5(
}
return {
type: 'builder_query' as QueryType,
type: queryData.builderQueryType ?? 'builder_query',
spec,
};
},
@@ -545,20 +546,22 @@ function reduceQueriesToObject(queryArray: any[]): {
/**
* Prepares V5 query range payload from GetQueryResultsProps
*/
export const prepareQueryRangePayloadV5 = ({
query,
globalSelectedInterval,
graphType,
selectedTime,
tableParams,
variables = {},
start: startTime,
end: endTime,
formatForWeb,
originalGraphType,
fillGaps,
dynamicVariables,
}: GetQueryResultsProps): PrepareQueryRangePayloadV5Result => {
export const prepareQueryRangePayloadV5 = (
{
query,
globalSelectedInterval,
graphType,
selectedTime,
tableParams,
variables = {},
start: startTime,
end: endTime,
formatForWeb,
originalGraphType,
fillGaps,
}: GetQueryResultsProps,
dynamicVariables: DynamicVariableSuggestion[] = [],
): PrepareQueryRangePayloadV5Result => {
let legendMap: Record<string, string> = {};
const requestType = mapPanelTypeToRequestType(graphType);
let queries: QueryEnvelope[] = [];
@@ -671,9 +674,9 @@ export const prepareQueryRangePayloadV5 = ({
(acc, [key, value]) => {
acc[key] = {
value,
type: dynamicVariables
?.find((v) => v.name === key)
?.type?.toLowerCase() as VariableType,
type: dynamicVariables.some((v) => v.name === key)
? ('dynamic' as VariableType)
: undefined,
};
return acc;
},

View File

@@ -0,0 +1,61 @@
import { ReactNode } from 'react';
import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys';
import { ResourceProvider } from 'hooks/useResourceAttribute';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import {
QueryBuilderContext,
QueryBuilderProvider,
} from 'providers/QueryBuilder';
import { QueryBuilderContextType } from 'types/common/queryBuilder';
import { AppLayer } from './types';
export interface AppPageProvidersProps {
children: ReactNode;
layout: AppLayer;
/** When set, replaces `QueryBuilderProvider` with a fixed context value. */
queryBuilder?: Partial<QueryBuilderContextType>;
}
/**
* The layers a routed page renders in, below `PrivateRoute` and inside the app
* chrome. A new provider belongs here when only pages need it, or when it has to
* sit inside `AppLayout`.
*
* One ordering constraint: `AppLayout` calls `useKeyboardHotkeys`, so the
* hotkeys provider has to stay above the layout. The rest of the order is the
* one `AppRoutes` has, kept as-is so a story and a route render the same tree.
*/
function AppPageProviders({
children,
layout,
queryBuilder,
}: AppPageProvidersProps): JSX.Element {
const hotkeys = (
<KeyboardHotkeysProvider>
<>
{layout(<PreferenceContextProvider>{children}</PreferenceContextProvider>)}
</>
</KeyboardHotkeysProvider>
);
return (
<ResourceProvider>
{queryBuilder ? (
<QueryBuilderContext.Provider
value={queryBuilder as QueryBuilderContextType}
>
{hotkeys}
</QueryBuilderContext.Provider>
) : (
<QueryBuilderProvider>{hotkeys}</QueryBuilderProvider>
)}
</ResourceProvider>
);
}
AppPageProviders.defaultProps = {
queryBuilder: undefined,
};
export default AppPageProviders;

View File

@@ -0,0 +1,56 @@
import { ReactNode } from 'react';
import { HelmetProvider } from 'react-helmet-async';
import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { Provider } from 'react-redux';
// eslint-disable-next-line no-restricted-imports
import { Store } from 'redux';
import { GlobalTimeStoreAdapter } from 'components/GlobalTimeStoreAdapter/GlobalTimeStoreAdapter';
import { ThemeProvider } from 'hooks/useDarkMode';
import TimezoneProvider from 'providers/Timezone';
import { AppLayer } from './types';
export interface AppProvidersProps {
children: ReactNode;
store: Store;
queryClient: QueryClient;
appContext: AppLayer;
searchParams: AppLayer;
}
/**
* The layers that exist before the app knows anything. Mounted for the whole
* session, including while `AppProvider` is still fetching the user and the boot
* spinner is on screen, and never remounted after that.
*
* A new provider belongs here only if it holds process-wide state that does not
* depend on the user, the license or the route. One that fetches on mount would
* fire unauthenticated from here; put it in `AppShell` or lower.
*/
function AppProviders({
children,
store,
queryClient,
appContext,
searchParams,
}: AppProvidersProps): JSX.Element {
return (
<HelmetProvider>
{searchParams(
<ThemeProvider>
<TimezoneProvider>
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<GlobalTimeStoreAdapter />
{appContext(children)}
</Provider>
</QueryClientProvider>
</TimezoneProvider>
</ThemeProvider>,
)}
</HelmetProvider>
);
}
export default AppProviders;

View File

@@ -0,0 +1,52 @@
import { ReactNode } from 'react';
import { ConfigProvider } from 'antd';
import { useThemeConfig } from 'hooks/useDarkMode';
import { NotificationProvider } from 'hooks/useNotifications';
import { CmdKProvider } from 'providers/cmdKProvider';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import { AppLayer } from './types';
export interface AppShellProps {
children: ReactNode;
router: AppLayer;
/** Mounted beside the routed content: the command palette and its siblings. */
overlays?: ReactNode;
}
/**
* The layers between a resolved session and a page. Mounted once the boot
* fetches settle, above `PrivateRoute`, so it also covers the redirects and the
* not-found route, and it survives every navigation.
*
* A new provider belongs here if it needs the router or the user and has to
* outlive the page: a global overlay, a shortcut host, anything one route opens
* and the next one keeps.
*
* `ConfigProvider` reads `useThemeConfig`, which needs `ThemeProvider` above it,
* so the antd theme is settled here instead of by the caller.
*/
function AppShell({ children, router, overlays }: AppShellProps): JSX.Element {
const themeConfig = useThemeConfig();
return (
<ConfigProvider theme={themeConfig}>
{router(
<CmdKProvider>
<NotificationProvider>
<ErrorModalProvider>
{overlays}
{children}
</ErrorModalProvider>
</NotificationProvider>
</CmdKProvider>,
)}
</ConfigProvider>
);
}
AppShell.defaultProps = {
overlays: undefined,
};
export default AppShell;

View File

@@ -0,0 +1,3 @@
import { ReactNode } from 'react';
export type AppLayer = (children: ReactNode) => ReactNode;

View File

@@ -1,176 +0,0 @@
export default function ApacheIcon(): JSX.Element {
return (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.7112 1.05739C8.3796 1.30831 8.08524 1.60498 7.83691 1.93853L8.17977 2.58567C8.40218 2.26279 8.64677 1.95576 8.91177 1.66681C8.93063 1.64581 8.94091 1.63596 8.94091 1.63596L8.91177 1.66681C8.66003 1.95965 8.43081 2.27111 8.22606 2.59853C8.67305 2.56672 9.11808 2.51165 9.55934 2.43353C9.60936 2.25525 9.62374 2.06886 9.60168 1.88502C9.57962 1.70118 9.52154 1.52349 9.43077 1.3621C9.43077 1.3621 9.09991 0.828957 8.7112 1.05739Z"
fill="url(#paint0_linear_2061_4195)"
/>
<path
d="M7.55932 6.49365L7.42432 6.51722L7.49332 6.50651C7.51432 6.50265 7.53703 6.49837 7.55932 6.49365Z"
fill="#BE202E"
/>
<path
opacity="0.35"
d="M7.55932 6.49365L7.42432 6.51722L7.49332 6.50651C7.51432 6.50265 7.53703 6.49837 7.55932 6.49365Z"
fill="#BE202E"
/>
<path
d="M7.67407 5.92858L7.6955 5.92558C7.72464 5.9213 7.75336 5.91616 7.78122 5.91016L7.67493 5.92816L7.67407 5.92858Z"
fill="#BE202E"
/>
<path
opacity="0.35"
d="M7.67407 5.92858L7.6955 5.92558C7.72464 5.9213 7.75336 5.91616 7.78122 5.91016L7.67493 5.92816L7.67407 5.92858Z"
fill="#BE202E"
/>
<path
d="M7.16872 4.25736C7.273 4.0625 7.37858 3.87221 7.48543 3.6865C7.59629 3.49393 7.70829 3.3075 7.82143 3.12721L7.84115 3.09507C7.95315 2.91793 8.066 2.74779 8.17972 2.58464L7.83686 1.9375L7.75886 2.03393C7.65986 2.15736 7.55743 2.29107 7.452 2.43036C7.33329 2.58893 7.21115 2.75779 7.08729 2.93564C6.97286 3.09979 6.85672 3.27164 6.74058 3.44993C6.64158 3.60121 6.54258 3.75721 6.444 3.91707L6.43286 3.93507L6.879 4.8145C6.97443 4.62707 7.071 4.44136 7.16872 4.25736Z"
fill="url(#paint1_linear_2061_4195)"
/>
<path
d="M5.13606 9.22519C5.07692 9.38748 5.01763 9.55305 4.95821 9.72191L4.95563 9.72919L4.93035 9.80076C4.89049 9.91476 4.85535 10.015 4.77563 10.2503C4.92444 10.3467 5.0416 10.4847 5.11249 10.6472C5.10283 10.4581 5.01907 10.2804 4.87935 10.1526C5.16043 10.1981 5.44862 10.1654 5.71236 10.0581C5.97609 9.95074 6.20521 9.77291 6.37463 9.54405C6.40102 9.50088 6.42464 9.45607 6.44535 9.40991C6.37459 9.49741 6.28141 9.56408 6.17575 9.6028C6.07008 9.64152 5.95589 9.65084 5.84535 9.62976C6.20937 9.49539 6.51802 9.24316 6.72221 8.91319C6.76978 8.83691 6.81563 8.75376 6.86278 8.66162C6.6974 8.84328 6.48756 8.97872 6.25393 9.05463C6.02029 9.13053 5.77091 9.14426 5.53035 9.09448L5.16949 9.13391C5.15706 9.16434 5.14721 9.19476 5.13606 9.22519Z"
fill="url(#paint2_linear_2061_4195)"
/>
<path
d="M5.30448 8.417C5.38248 8.21528 5.46276 8.01157 5.54533 7.80586C5.62419 7.60871 5.70519 7.41057 5.78833 7.21143C5.87148 7.01228 5.95633 6.81228 6.0429 6.61143C6.1309 6.40828 6.22076 6.20557 6.31248 6.00328C6.40419 5.801 6.49633 5.60257 6.5889 5.408C6.62262 5.33714 6.65662 5.26643 6.6909 5.19586C6.75005 5.07414 6.80962 4.95343 6.86962 4.83371C6.87262 4.82728 6.87605 4.82086 6.87948 4.81443L6.4329 3.93457L6.41105 3.97014C6.30733 4.14157 6.20362 4.313 6.10205 4.49128C6.00048 4.66957 5.89848 4.853 5.80205 5.03814C5.7189 5.19443 5.63776 5.35157 5.55862 5.50957L5.51148 5.606C5.41419 5.80614 5.32633 5.99943 5.24705 6.18543C5.15705 6.396 5.07762 6.59686 5.00876 6.788C4.9629 6.91357 4.92305 7.03486 4.88362 7.151C4.85233 7.25043 4.82276 7.34986 4.79448 7.451C4.72762 7.68471 4.67048 7.91771 4.62305 8.15L5.07133 9.035C5.13076 8.87671 5.19162 8.71614 5.2539 8.55328L5.30448 8.417Z"
fill="url(#paint3_linear_2061_4195)"
/>
<path
d="M4.615 8.18082C4.55868 8.46014 4.51975 8.74268 4.49843 9.02682C4.49843 9.03668 4.49843 9.04653 4.49629 9.05639C4.3547 8.8778 4.1801 8.72808 3.982 8.61539C4.24526 8.95089 4.41819 9.34823 4.48429 9.76953C4.28982 9.78674 4.0942 9.75338 3.91643 9.67268C4.05057 9.80984 4.21714 9.91096 4.40072 9.96668C4.14974 10.0146 3.9168 10.1307 3.72743 10.3022C3.97375 10.178 4.25059 10.1271 4.525 10.1557C4.21858 11.024 3.91129 11.9827 3.604 13.0001C3.64664 12.988 3.6856 12.9655 3.71739 12.9346C3.74918 12.9037 3.7728 12.8654 3.78615 12.8231C3.841 12.6388 4.20443 11.4298 4.77443 9.84068L4.82372 9.70439L4.83743 9.66625C4.89772 9.49968 4.96015 9.32968 5.02472 9.15625L5.06758 9.03753V9.03539L4.62143 8.15039C4.61929 8.16025 4.61672 8.17053 4.615 8.18082Z"
fill="url(#paint4_linear_2061_4195)"
/>
<path
d="M6.94843 4.89059L6.90986 4.96987C6.87129 5.04959 6.83214 5.13102 6.79243 5.21416C6.74957 5.30416 6.70671 5.3963 6.66386 5.49059C6.64157 5.53816 6.621 5.58573 6.59743 5.63416C6.53057 5.7793 6.46286 5.92916 6.39429 6.08373C6.30857 6.27402 6.22286 6.47173 6.13714 6.67687C6.054 6.87259 5.96971 7.07516 5.88429 7.28459C5.80314 7.48459 5.721 7.68973 5.63786 7.90002C5.56386 8.08888 5.48914 8.28273 5.41371 8.48159C5.40986 8.49145 5.40643 8.50087 5.403 8.51073C5.32814 8.70873 5.25257 8.91187 5.17629 9.12016L5.17114 9.1343L5.532 9.09487L5.51057 9.09102C6.039 8.98351 6.52026 8.7126 6.88629 8.31659C7.0686 8.1182 7.22683 7.89896 7.35771 7.66345C7.47147 7.46033 7.57252 7.25036 7.66029 7.03473C7.74386 6.83245 7.824 6.61387 7.90114 6.37645C7.79448 6.43086 7.68084 6.47037 7.56343 6.49388C7.54157 6.49859 7.52057 6.50287 7.49657 6.50673C7.47257 6.51059 7.45071 6.51445 7.42757 6.51745C7.80325 6.36305 8.10458 6.06924 8.26843 5.69759C8.12155 5.79773 7.95733 5.86966 7.78414 5.90973C7.75586 5.91616 7.72757 5.92087 7.69843 5.92516L7.677 5.92816C7.80484 5.87656 7.92565 5.80903 8.03657 5.72716C8.05843 5.71087 8.07943 5.69373 8.1 5.67573C8.13129 5.64873 8.16086 5.62045 8.18914 5.59002C8.20714 5.57116 8.22471 5.55145 8.24186 5.53087C8.28293 5.48179 8.32059 5.42996 8.35457 5.37573C8.36529 5.35859 8.376 5.34145 8.38629 5.32345C8.39957 5.29773 8.41243 5.27245 8.42486 5.24716C8.481 5.13402 8.526 5.03288 8.56157 4.94459C8.57957 4.90173 8.595 4.85888 8.60829 4.82116C8.61386 4.80616 8.619 4.79116 8.62371 4.7783C8.63786 4.73545 8.64943 4.69816 8.65843 4.66473C8.66941 4.62595 8.67828 4.58661 8.685 4.54688C8.67015 4.5586 8.65454 4.56934 8.63829 4.57902C8.4822 4.66169 8.31419 4.71953 8.14029 4.75045H8.13257L8.08157 4.75859L8.09057 4.75473L6.957 4.87902L6.94843 4.89059Z"
fill="url(#paint5_linear_2061_4195)"
/>
<path
d="M8.22475 2.59871C8.12404 2.75343 8.01389 2.92914 7.89561 3.12843L7.87675 3.16014C7.77446 3.33157 7.66604 3.52114 7.55147 3.72886C7.45261 3.90771 7.34989 4.10014 7.24332 4.30614C7.15018 4.48586 7.05418 4.67671 6.95532 4.87871L8.08889 4.75443C8.33914 4.65567 8.55501 4.48583 8.70989 4.26586C8.74804 4.211 8.78618 4.15357 8.82432 4.09443C8.94089 3.91271 9.05489 3.71257 9.15689 3.51371C9.25018 3.33322 9.33429 3.14813 9.40889 2.95914C9.44758 2.86102 9.48092 2.76087 9.50875 2.65914C9.52932 2.58028 9.54561 2.50571 9.55804 2.43457C9.11675 2.51236 8.67172 2.56715 8.22475 2.59871Z"
fill="url(#paint6_linear_2061_4195)"
/>
<path
d="M7.49234 6.50635C7.46963 6.5102 7.44648 6.51406 7.42334 6.51706C7.44648 6.51406 7.47134 6.5102 7.49234 6.50635Z"
fill="#BE202E"
/>
<path
opacity="0.35"
d="M7.49234 6.50635C7.46963 6.5102 7.44648 6.51406 7.42334 6.51706C7.44648 6.51406 7.47134 6.5102 7.49234 6.50635Z"
fill="#BE202E"
/>
<path
d="M7.49234 6.50635C7.46963 6.5102 7.44648 6.51406 7.42334 6.51706C7.44648 6.51406 7.47134 6.5102 7.49234 6.50635Z"
fill="url(#paint7_linear_2061_4195)"
/>
<defs>
<linearGradient
id="paint0_linear_2061_4195"
x1="7.26579"
y1="1.16584"
x2="9.7782"
y2="0.46843"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F69923" />
<stop offset="0.312" stopColor="#F79A23" />
<stop offset="0.838" stopColor="#E97826" />
</linearGradient>
<linearGradient
id="paint1_linear_2061_4195"
x1="1.76109"
y1="12.4382"
x2="6.87606"
y2="1.48267"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.323" stopColor="#9E2064" />
<stop offset="0.63" stopColor="#C92037" />
<stop offset="0.751" stopColor="#CD2335" />
<stop offset="1" stopColor="#E97826" />
</linearGradient>
<linearGradient
id="paint2_linear_2061_4195"
x1="3.47784"
y1="11.6287"
x2="6.5258"
y2="5.10046"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#282662" />
<stop offset="0.095" stopColor="#662E8D" />
<stop offset="0.788" stopColor="#9F2064" />
<stop offset="0.949" stopColor="#CD2032" />
</linearGradient>
<linearGradient
id="paint3_linear_2061_4195"
x1="1.94596"
y1="11.7748"
x2="7.06094"
y2="0.819304"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.323" stopColor="#9E2064" />
<stop offset="0.63" stopColor="#C92037" />
<stop offset="0.751" stopColor="#CD2335" />
<stop offset="1" stopColor="#E97826" />
</linearGradient>
<linearGradient
id="paint4_linear_2061_4195"
x1="2.46768"
y1="11.0455"
x2="5.15578"
y2="5.28798"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#282662" />
<stop offset="0.095" stopColor="#662E8D" />
<stop offset="0.788" stopColor="#9F2064" />
<stop offset="0.949" stopColor="#CD2032" />
</linearGradient>
<linearGradient
id="paint5_linear_2061_4195"
x1="3.08048"
y1="12.3044"
x2="8.19546"
y2="1.3489"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.323" stopColor="#9E2064" />
<stop offset="0.63" stopColor="#C92037" />
<stop offset="0.751" stopColor="#CD2335" />
<stop offset="1" stopColor="#E97826" />
</linearGradient>
<linearGradient
id="paint6_linear_2061_4195"
x1="2.70679"
y1="12.9581"
x2="7.82195"
y2="2.00223"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.323" stopColor="#9E2064" />
<stop offset="0.63" stopColor="#C92037" />
<stop offset="0.751" stopColor="#CD2335" />
<stop offset="1" stopColor="#E97826" />
</linearGradient>
<linearGradient
id="paint7_linear_2061_4195"
x1="3.41759"
y1="12.4619"
x2="8.53277"
y2="1.50629"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.323" stopColor="#9E2064" />
<stop offset="0.63" stopColor="#C92037" />
<stop offset="0.751" stopColor="#CD2335" />
<stop offset="1" stopColor="#E97826" />
</linearGradient>
</defs>
</svg>
);
}

View File

@@ -1,28 +0,0 @@
export default function DockerIcon(): JSX.Element {
return (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_2061_4220)">
<path
d="M13.574 5.82107C13.2652 5.60679 12.5575 5.52644 12.0042 5.63358C11.9398 5.09788 11.6439 4.62915 11.1292 4.21399L10.8332 3.99971L10.6274 4.30774C10.37 4.70951 10.2413 5.27198 10.2799 5.80768C10.2928 5.99517 10.3571 6.32998 10.5502 6.62461C10.37 6.73175 9.99686 6.86567 9.50789 6.86567H0.204685L0.17895 6.97281C0.0888774 7.5085 0.0888774 9.18254 1.14401 10.4682C1.9418 11.4458 3.12561 11.9414 4.68258 11.9414C8.05386 11.9414 10.5502 10.3209 11.7211 7.38797C12.1843 7.40136 13.1751 7.38797 13.677 6.38354C13.6898 6.35676 13.7156 6.30319 13.8056 6.10231L13.8571 5.99517L13.574 5.82107ZM7.6421 2.04443H6.22668V3.38367H7.6421V2.04443ZM7.6421 3.65151H6.22668V4.99074H7.6421V3.65151ZM5.96933 3.65151H4.5539V4.99074H5.96933V3.65151ZM4.29655 3.65151H2.88113V4.99074H4.29655V3.65151ZM2.62378 5.25859H1.20835V6.59782H2.62378V5.25859ZM4.29655 5.25859H2.88113V6.59782H4.29655V5.25859ZM5.96933 5.25859H4.5539V6.59782H5.96933V5.25859ZM7.6421 5.25859H6.22668V6.59782H7.6421V5.25859ZM9.31488 5.25859H7.89945V6.59782H9.31488V5.25859Z"
fill="#2396ED"
/>
</g>
<defs>
<clipPath id="clip0_2061_4220">
<rect
width="13.7143"
height="13.7143"
fill="white"
transform="translate(0.142822 0.143066)"
/>
</clipPath>
</defs>
</svg>
);
}

View File

@@ -1,36 +0,0 @@
export default function ElasticSearchIcon(): JSX.Element {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.38041 6.94508L9.70241 8.45858L13.0524 5.52158C13.1019 5.27892 13.1255 5.03171 13.1229 4.78408C13.1236 3.9846 12.8681 3.20592 12.3941 2.56216C11.92 1.9184 11.2522 1.44339 10.4885 1.20675C9.72487 0.970101 8.9055 0.984259 8.15047 1.24714C7.39544 1.51003 6.74446 2.00782 6.29291 2.66758L5.73291 5.56008L6.38041 6.94508Z"
fill="#FED10A"
/>
<path
d="M2.943 10.4593C2.89356 10.7062 2.86994 10.9575 2.8725 11.2093C2.87361 12.012 3.13177 12.7932 3.60915 13.4385C4.08654 14.0838 4.75804 14.5592 5.52528 14.7952C6.29252 15.0311 7.11514 15.0151 7.87262 14.7495C8.63009 14.4839 9.28259 13.9827 9.7345 13.3193L10.2845 10.4398L9.55 9.02928L6.215 7.50928L2.943 10.4593Z"
fill="#24BBB1"
/>
<path
d="M2.92394 4.71302L5.19994 5.25002L5.69994 2.66552C5.39077 2.43015 5.01365 2.30134 4.62509 2.29839C4.23654 2.29544 3.8575 2.41852 3.54479 2.64916C3.23208 2.8798 3.00256 3.20559 2.89063 3.57768C2.77869 3.94978 2.79039 4.34813 2.92394 4.71302Z"
fill="#EF5098"
/>
<path
d="M2.72503 5.2583C2.23266 5.42016 1.80253 5.73058 1.49379 6.14687C1.18505 6.56317 1.01286 7.0649 1.00091 7.58305C0.988962 8.1012 1.13784 8.61033 1.42706 9.04041C1.71628 9.4705 2.13164 9.80042 2.61603 9.9848L5.81003 7.0998L5.22653 5.8498L2.72503 5.2583Z"
fill="#17A8E0"
/>
<path
d="M10.312 13.3197C10.6209 13.5543 10.9975 13.6825 11.3853 13.6851C11.7732 13.6877 12.1514 13.5646 12.4634 13.3342C12.7755 13.1038 13.0044 12.7785 13.116 12.407C13.2276 12.0356 13.2159 11.6379 13.0825 11.2737L10.812 10.7412L10.312 13.3197Z"
fill="#93C83E"
/>
<path
d="M10.7735 10.145L13.2735 10.7285C13.7666 10.5672 14.1976 10.257 14.507 9.84058C14.8165 9.42415 14.9891 8.922 15.0013 8.40333C15.0134 7.88467 14.8643 7.375 14.5747 6.94457C14.2851 6.51414 13.8691 6.18413 13.384 6L10.1135 8.8665L10.7735 10.145Z"
fill="#0779A1"
/>
</svg>
);
}

View File

@@ -1,27 +0,0 @@
function HerokuIcon(): JSX.Element {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_2061_4245)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.6285 0H2.10787C1.31283 0 0.666748 0.644312 0.666748 1.44112V14.5602C0.666748 15.3557 1.31283 16 2.10787 16H13.6285C14.4237 16 15.0678 15.3557 15.0678 14.5602V1.44112C15.0678 0.644312 14.4237 0 13.6285 0ZM14.2677 14.5602C14.2677 14.9135 13.9815 15.1994 13.6285 15.1994H2.10787C1.75506 15.1994 1.46688 14.9135 1.46688 14.5602V1.44112C1.46688 1.08654 1.75506 0.800133 2.10787 0.800133H13.6285C13.9815 0.800133 14.2677 1.08654 14.2677 1.44112V14.5602ZM4.26699 13.6009L6.06823 12.0002L4.26699 10.3999V13.6009ZM10.7719 7.11485C10.4481 6.78927 9.8569 6.40038 8.86819 6.40038C7.78342 6.40038 6.66611 6.68302 5.86752 6.94177V2.40082H4.26704V9.33907L5.39807 8.82667C5.41688 8.81826 7.24025 8.00086 8.86819 8.00086C9.68027 8.00086 9.86022 8.44796 9.86885 8.82158V13.6009H11.4676V8.801C11.4693 8.6983 11.4592 7.81051 10.7719 7.11485ZM8.66761 5.00027H10.2681C10.9912 4.1811 11.3597 3.30903 11.4677 2.40066H9.86881C9.69063 3.30726 9.29643 4.17424 8.66761 5.00027Z"
fill="#6762A6"
/>
</g>
<defs>
<clipPath id="clip0_2061_4245">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>
);
}
export default HerokuIcon;

File diff suppressed because one or more lines are too long

View File

@@ -1,68 +0,0 @@
export default function MongoDBIcon(): JSX.Element {
return (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.26568 13.0004L6.94382 12.8937C6.94382 12.8937 6.98668 11.2651 6.39739 11.1507C6.01168 10.7015 6.45439 -8.02403 7.86439 11.0868C7.59687 11.2225 7.39217 11.4564 7.29311 11.7395C7.24001 12.1577 7.23082 12.5803 7.26568 13.0004Z"
fill="url(#paint0_linear_2061_4238)"
/>
<path
d="M7.43957 11.4272C8.29654 10.7821 8.95282 9.90701 9.33214 8.90369C9.71147 7.90037 9.79826 6.81 9.58243 5.75931C8.95243 2.98002 7.46058 2.06631 7.29986 1.71745C7.1612 1.50022 7.04284 1.27068 6.94629 1.03174L7.065 8.7756C7.065 8.7756 6.819 11.1422 7.43957 11.4272Z"
fill="url(#paint1_linear_2061_4238)"
/>
<path
d="M6.78015 11.5296C6.78015 11.5296 4.15687 9.74286 4.30858 6.58214C4.32273 5.62928 4.54121 4.69054 4.94926 3.82935C5.3573 2.96816 5.94542 2.20456 6.67387 1.59014C6.759 1.51782 6.82663 1.42715 6.87168 1.32493C6.91674 1.22272 6.93805 1.11163 6.93401 1C7.0973 1.35143 7.07072 6.247 7.08787 6.81957C7.1543 9.04686 6.96401 11.1091 6.78015 11.5296Z"
fill="url(#paint2_linear_2061_4238)"
/>
<defs>
<linearGradient
id="paint0_linear_2061_4238"
x1="5.1021"
y1="7.10853"
x2="8.80138"
y2="8.36386"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.231" stopColor="#999875" />
<stop offset="0.563" stopColor="#9B9977" />
<stop offset="0.683" stopColor="#A09F7E" />
<stop offset="0.768" stopColor="#A9A889" />
<stop offset="0.837" stopColor="#B7B69A" />
<stop offset="0.896" stopColor="#C9C7B0" />
<stop offset="0.948" stopColor="#DEDDCB" />
<stop offset="0.994" stopColor="#F8F6EB" />
<stop offset="1" stopColor="#FBF9EF" />
</linearGradient>
<linearGradient
id="paint1_linear_2061_4238"
x1="6.45855"
y1="0.976404"
x2="8.09399"
y2="11.1888"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#48A547" />
<stop offset="1" stopColor="#3F9143" />
</linearGradient>
<linearGradient
id="paint2_linear_2061_4238"
x1="4.08293"
y1="6.89501"
x2="8.47176"
y2="5.42519"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#41A247" />
<stop offset="0.352" stopColor="#4BA74B" />
<stop offset="0.956" stopColor="#67B554" />
<stop offset="1" stopColor="#69B655" />
</linearGradient>
</defs>
</svg>
);
}

File diff suppressed because one or more lines are too long

View File

@@ -1,22 +0,0 @@
function NginxIcon(): JSX.Element {
return (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.97775 1H7.00561C7.14837 1.068 7.28743 1.14353 7.42218 1.22629C8.97332 2.11829 10.5245 3.01057 12.0756 3.90314C12.1356 3.93517 12.1846 3.9845 12.2163 4.04472C12.2479 4.10493 12.2607 4.17327 12.253 4.24086C12.2496 6.12186 12.253 8.00243 12.2509 9.88257C12.2307 9.9723 12.1759 10.0504 12.0983 10.0999C10.4489 11.0496 8.79932 11.9987 7.14961 12.9473C7.10963 12.9778 7.06144 12.9956 7.01125 12.9984C6.96106 13.0012 6.91118 12.9889 6.86803 12.9631C5.21661 12.0163 3.56675 11.0677 1.91846 10.1174C1.86489 10.0921 1.82003 10.0515 1.78952 10.0007C1.75901 9.94988 1.74423 9.89119 1.74703 9.832C1.74703 7.95143 1.74703 6.071 1.74703 4.19071C1.74299 4.13178 1.75661 4.07299 1.78616 4.02184C1.8157 3.97069 1.85983 3.92951 1.91289 3.90357C3.46203 3.01271 5.01118 2.12129 6.56032 1.22929C6.69832 1.15043 6.83375 1.06686 6.97775 1Z"
fill="#019639"
/>
<path
d="M3.90007 4.65924C3.90007 6.21039 3.90007 7.76167 3.90007 9.3131C3.89809 9.39901 3.91326 9.48446 3.94468 9.56445C3.9761 9.64444 4.02315 9.71736 4.08307 9.77896C4.19794 9.89243 4.34824 9.96309 4.5089 9.97916C4.66956 9.99522 4.83087 9.95572 4.96593 9.86724C5.05634 9.80581 5.13035 9.72321 5.18152 9.62662C5.23269 9.53003 5.25946 9.4224 5.2595 9.3131C5.2595 8.19024 5.25736 7.06739 5.2595 5.94453C6.28322 7.17024 7.30907 8.39424 8.33707 9.61653C8.4799 9.76122 8.65676 9.86772 8.85145 9.92628C9.04614 9.98484 9.25242 9.99357 9.45136 9.95167C9.59184 9.924 9.71973 9.85198 9.81624 9.74622C9.91275 9.64045 9.97278 9.50652 9.9875 9.3641C9.98979 7.78096 9.98979 6.19796 9.9875 4.6151C9.97275 4.44614 9.8952 4.28884 9.77016 4.17425C9.64512 4.05966 9.48168 3.99609 9.31207 3.99609C9.14247 3.99609 8.97902 4.05966 8.85399 4.17425C8.72895 4.28884 8.6514 4.44614 8.63665 4.6151C8.63665 5.75596 8.62979 6.89553 8.63665 8.03596C7.63122 6.85053 6.63822 5.65481 5.63665 4.4651C5.5046 4.29578 5.32979 4.16474 5.13023 4.08549C4.93067 4.00624 4.71359 3.98165 4.50136 4.01424C4.34059 4.03214 4.19154 4.10704 4.08124 4.22536C3.97093 4.34369 3.90666 4.49761 3.90007 4.65924Z"
fill="white"
/>
</svg>
);
}
export default NginxIcon;

File diff suppressed because one or more lines are too long

View File

@@ -1,60 +0,0 @@
export default function RedisIcon(): JSX.Element {
return (
<svg
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_2061_4282)">
<path
d="M13.3198 10.158C12.5879 10.5395 8.79654 12.0984 7.98938 12.5192C7.18221 12.94 6.73382 12.936 6.09616 12.6312C5.45855 12.3263 1.42388 10.6966 0.697072 10.3492C0.333858 10.1756 0.142822 10.029 0.142822 9.8906V8.50438C0.142822 8.50438 5.3955 7.3609 6.24348 7.05667C7.09141 6.75244 7.38563 6.74145 8.10723 7.00578C8.82895 7.2702 13.1439 8.0487 13.8571 8.30992L13.8568 9.67653C13.8569 9.81356 13.6923 9.96388 13.3198 10.158Z"
fill="#912626"
/>
<path
d="M13.3195 8.77972C12.5877 9.16104 8.79642 10.72 7.98926 11.1407C7.18215 11.5616 6.73376 11.5575 6.09615 11.2527C5.45849 10.9481 1.42397 9.31805 0.697221 8.97086C-0.029529 8.62345 -0.0447433 8.38436 0.66915 8.10482C1.38304 7.82518 5.39544 6.25098 6.24353 5.94675C7.09145 5.64263 7.38561 5.63154 8.10722 5.89597C8.82888 6.16029 12.5975 7.66034 13.3106 7.9215C14.024 8.18298 14.0513 8.39829 13.3195 8.77972Z"
fill="#C6302B"
/>
<path
d="M13.3198 7.91483C12.5879 8.29637 8.79654 9.85519 7.98938 10.2762C7.18221 10.6968 6.73382 10.6928 6.09616 10.388C5.4585 10.0833 1.42388 8.45338 0.697072 8.10597C0.333858 7.9324 0.142822 7.78604 0.142822 7.64756V6.26119C0.142822 6.26119 5.3955 5.11776 6.24348 4.81353C7.09141 4.50929 7.38563 4.49826 8.10723 4.76263C8.829 5.02701 13.144 5.80535 13.8571 6.06661L13.8568 7.43338C13.8569 7.57036 13.6923 7.72069 13.3198 7.91483Z"
fill="#912626"
/>
<path
d="M13.3195 6.53701C12.5877 6.91844 8.79642 8.47726 7.98926 8.89817C7.18215 9.31892 6.73376 9.3148 6.09615 9.00998C5.45849 8.70537 1.42397 7.07541 0.697221 6.72816C-0.029529 6.38085 -0.0447433 6.14171 0.66915 5.86207C1.38304 5.58258 5.39549 4.00828 6.24353 3.7041C7.09145 3.39992 7.38561 3.38889 8.10722 3.65326C8.82888 3.91758 12.5975 5.41753 13.3106 5.6788C14.024 5.94023 14.0513 6.15558 13.3195 6.53701Z"
fill="#C6302B"
/>
<path
d="M13.3198 5.58855C12.5879 5.96998 8.79654 7.5289 7.98938 7.94987C7.18221 8.37062 6.73382 8.36649 6.09616 8.06167C5.4585 7.75701 1.42388 6.12705 0.697072 5.7798C0.333858 5.60607 0.142822 5.45965 0.142822 5.32133V3.9349C0.142822 3.9349 5.3955 2.79153 6.24348 2.48735C7.09141 2.18307 7.38563 2.17214 8.10723 2.43646C8.829 2.70083 13.144 3.47917 13.8571 3.74044L13.8568 5.10715C13.8569 5.24403 13.6923 5.39435 13.3198 5.58855Z"
fill="#912626"
/>
<path
d="M13.3195 4.21078C12.5876 4.59221 8.79639 6.15113 7.98923 6.57188C7.18212 6.99263 6.73373 6.98851 6.09612 6.68385C5.45852 6.37903 1.42394 4.74917 0.697248 4.40187C-0.0295558 4.05462 -0.0447165 3.81542 0.669123 3.53583C1.38302 3.2563 5.39546 1.68221 6.2435 1.37792C7.09143 1.07369 7.38559 1.06276 8.1072 1.32714C8.82886 1.59151 12.5975 3.09146 13.3106 3.35272C14.0239 3.61394 14.0513 3.82935 13.3195 4.21078Z"
fill="#C6302B"
/>
<path
d="M8.67572 2.86218L7.49661 2.9846L7.23267 3.61974L6.80635 2.91099L5.44483 2.78863L6.46076 2.42226L6.15594 1.85986L7.1071 2.23186L8.00378 1.93829L7.76142 2.51981L8.67572 2.86218ZM7.16228 5.94351L4.96172 5.03081L8.11494 4.54679L7.16228 5.94351ZM4.11138 3.21522C5.04219 3.21522 5.79674 3.50772 5.79674 3.86847C5.79674 4.22933 5.04219 4.52177 4.11138 4.52177C3.18058 4.52177 2.42603 4.22927 2.42603 3.86847C2.42603 3.50772 3.18058 3.21522 4.11138 3.21522Z"
fill="white"
/>
<path
d="M10.0693 3.0357L11.9355 3.77316L10.0709 4.50993L10.0693 3.0357Z"
fill="#621B1C"
/>
<path
d="M8.00464 3.85234L10.0693 3.03564L10.0709 4.50988L9.86844 4.58906L8.00464 3.85234Z"
fill="#9A2928"
/>
</g>
<defs>
<clipPath id="clip0_2061_4282">
<rect
width="13.7143"
height="13.7143"
fill="white"
transform="translate(0.142822 0.143066)"
/>
</clipPath>
</defs>
</svg>
);
}

View File

@@ -8,7 +8,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { X } from '@signozhq/icons';
import { Widgets } from 'types/api/dashboard/getAll';
import { Widgets } from 'types/api/widgets/widget';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { DataSource } from 'types/common/queryBuilder';

View File

@@ -5,16 +5,16 @@ import { useHistory, useLocation } from 'react-router-dom';
import { Color } from '@signozhq/design-tokens';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/GridCardLayout/config';
import GridCard from 'container/GridCardLayout/GridCard';
import { Card } from 'container/GridCardLayout/styles';
import { ViewMenuAction } from 'container/WidgetCard/config';
import GridCard from 'container/WidgetCard/Card';
import { Card } from 'container/WidgetCard/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { isEmpty } from 'lodash-es';
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { UpdateTimeInterval } from 'store/actions';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { Widgets } from 'types/api/widgets/widget';
import { GlobalReducer } from 'types/reducer/globalTime';
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';

View File

@@ -5,15 +5,15 @@ import { useHistory, useLocation } from 'react-router-dom';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/GridCardLayout/config';
import GridCard from 'container/GridCardLayout/GridCard';
import { Card } from 'container/GridCardLayout/styles';
import { ViewMenuAction } from 'container/WidgetCard/config';
import GridCard from 'container/WidgetCard/Card';
import { Card } from 'container/WidgetCard/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { UpdateTimeInterval } from 'store/actions';
import { Widgets } from 'types/api/dashboard/getAll';
import { Widgets } from 'types/api/widgets/widget';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';

View File

@@ -4,7 +4,7 @@ import { useSelector } from 'react-redux';
import { Card } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import { CardContainer } from 'container/GridCardLayout/styles';
import { CardContainer } from 'container/WidgetCard/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ChevronDown, ChevronUp } from '@signozhq/icons';
import { AppState } from 'store/reducers';

View File

@@ -1,7 +1,7 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { getWidgetQuery } from 'pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil';
import { Widgets } from 'types/api/dashboard/getAll';
import { Widgets } from 'types/api/widgets/widget';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { v4 as uuidv4 } from 'uuid';

View File

@@ -6,9 +6,9 @@ import { Col, Row } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/GridCardLayout/config';
import GridCard from 'container/GridCardLayout/GridCard';
import { Card } from 'container/GridCardLayout/styles';
import { ViewMenuAction } from 'container/WidgetCard/config';
import GridCard from 'container/WidgetCard/Card';
import { Card } from 'container/WidgetCard/styles';
import { Button } from 'container/MetricsApplication/Tabs/styles';
import { useGraphClickHandler } from 'container/MetricsApplication/Tabs/util';
import { useIsDarkMode } from 'hooks/useDarkMode';

View File

@@ -8,7 +8,7 @@ import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { getQueryPayloadFromWidgetsData } from 'pages/Celery/CeleryOverview/CeleryOverviewUtils';
import { AppState } from 'store/reducers';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { Widgets } from 'types/api/widgets/widget';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { GlobalReducer } from 'types/reducer/globalTime';

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