Compare commits

..

30 Commits

Author SHA1 Message Date
nityanandagohain
9763445f7d fix: address comments 2026-08-12 00:35:37 +05:30
nityanandagohain
ae72866a4b fix: update function name 2026-08-05 10:37:39 +05:30
nityanandagohain
a5ac26196e fix: remove unwanted test 2026-08-05 10:28:55 +05:30
nityanandagohain
cb8a1de781 fix: cleanup 2026-08-05 10:27:53 +05:30
nityanandagohain
d9870f11c1 chore: keep scope to fields api 2026-08-05 09:44:27 +05:30
nityanandagohain
25e4e2f0e4 Merge remote-tracking branch 'origin/main' into issue_5601_metadata 2026-08-05 09:08:23 +05:30
nityanandagohain
b81f58ea59 fix: more fixes 2026-07-24 17:33:34 +05:30
nityanandagohain
3fe7af3867 Merge branch 'issue_5601' into issue_5601_metadata 2026-07-24 17:04:36 +05:30
nityanandagohain
b2c5af428e fix: refactor as requested 2026-07-24 12:07:37 +05:30
nityanandagohain
093f9b41c4 fix: minor cleanup 2026-07-22 16:22:19 +05:30
nityanandagohain
596e128005 fix: updated openapi 2026-07-22 15:26:52 +05:30
nityanandagohain
2e1da92367 fix: remove source and change to builder ai query 2026-07-22 15:24:43 +05:30
nityanandagohain
1e8d72e8fa Merge remote-tracking branch 'origin/main' into issue_5601 2026-07-21 17:08:34 +05:30
nityanandagohain
b9ec4a5efd fix: support for quick filters 2026-07-17 12:54:45 +05:30
nityanandagohain
247c3c4aee feat: support for trace aggregate in metadata endpoint 2026-07-17 00:41:07 +05:30
nityanandagohain
cce080e1ae fix: add back the flag in metadata 2026-07-16 11:37:57 +05:30
nityanandagohain
deca060a4d Merge remote-tracking branch 'origin/main' into issue_5601 2026-07-16 11:00:55 +05:30
nityanandagohain
03c7e524e7 fix: fix tests 2026-07-15 20:23:33 +05:30
nityanandagohain
815dc7d88b Merge remote-tracking branch 'origin/main' into issue_5601 2026-07-15 20:10:05 +05:30
nityanandagohain
f50d9199fe fix: address comments 2026-07-15 19:47:39 +05:30
nityanandagohain
31efe177a4 fix: address comments 2026-07-14 18:53:30 +05:30
nityanandagohain
d502d12ac3 fix: update openapi 2026-07-10 14:27:07 +05:30
nityanandagohain
bd9f15a716 fix: update integration test 2026-07-10 14:21:16 +05:30
nityanandagohain
813ef988c9 fix: edge cases and correct cost key 2026-07-10 12:06:34 +05:30
nityanandagohain
40e6799285 fix: add resource fingerprint cte 2026-07-10 00:36:25 +05:30
nityanandagohain
1caa60a3cd fix: cleanup and more tests 2026-07-09 23:54:05 +05:30
nityanandagohain
3f781f0083 fix: more cleanup 2026-07-09 12:39:01 +05:30
nityanandagohain
6aec05cf7a fix: more tests 2026-07-09 08:45:22 +05:30
nityanandagohain
683a52f35a fix: take perf into consideration 2026-07-09 08:45:22 +05:30
nityanandagohain
e924fa1e62 feat: support llm trace list and span list 2026-07-09 08:45:20 +05:30
647 changed files with 44993 additions and 31904 deletions

View File

@@ -1,11 +0,0 @@
# Comments
Applies to everything in the repo — code, config, workflows.
- **No unnecessary comments.** Do not comment where the code is self-explanatory; never restate what the code already says.
- **Document only** non-obvious behavior, constraints, formats, and edge cases.
- **Rationale goes in prose, not source.** Why a version is pinned, why a job exists, how a subsystem fits together — that belongs in the README or the PR.
- **Never remove pre-existing comments** when editing code. The bar above applies to comments you write, not comments already there.
- **Never talk to the reviewer.** No comments about where a change came from, what was changed, or why the change is correct — that belongs in the PR description and is noise the moment it merges.
Language rules build on this one: [`go-comments`](go-comments.md), [`py-comments`](py-comments.md).

View File

@@ -1,12 +0,0 @@
---
paths:
- "**/*.go"
---
# Go comments
The bar is the [`comments`](comments.md) rule: nothing where the code is self-explanatory.
- **Names carry the meaning.** Make function, type, and variable names self-explanatory so the comment is unnecessary in the first place. If a comment is needed to explain what a function does, fix the name, not the comment.
- **Godoc**: Skip comments that merely restate the identifier. Document only non-obvious behavior, constraints, formats, and edge cases.
- **Generated code**: If the comment is emitted by an external codegen tool, leave it as-is — do not add or trim comments in generated files.

View File

@@ -1,7 +0,0 @@
# Pull requests
- **Follow the template** (`.github/pull_request_template.md`): fill in its headings (Description / Issues closed by this PR / Screenshots / Additional Information). Don't add sections the template doesn't have.
- **Keep only the headings that apply.** Delete every heading that has nothing under it, along with its `<!--...-->` placeholder comment. The body must never contain an empty heading — if only Description applies, the body has exactly that one heading.
- **Keep the description concise and human-readable.** 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.
- **Reference issues with `Closes #issue-number`** under "Issues closed by this PR" so they auto-close on merge. This goes in the PR description only — never in commit messages.
- **AI assistance in commits may optionally be disclosed with an `Assisted-by:` trailer** naming the model (e.g. `Assisted-by: Claude Opus 4.5`) — do NOT use a `Co-authored-by:` trailer for this.

View File

@@ -1,13 +0,0 @@
---
paths:
- "**/*.py"
---
# Python comments
The bar is the [`comments`](comments.md) rule: nothing where the code is self-explanatory.
- **Names carry the meaning.** Make function and variable names self-explanatory so the comment or docstring is unnecessary in the first place. If a docstring is needed to explain what a function does, fix the name, not the docstring.
- **No file-level docstring.** The filename says what the module is for — `tool_bin.py` gets the tool binary. A module docstring restating that is noise, and a paragraph of design prose at the top of a file goes stale where nobody is looking. A constraint belongs next to the code it constrains, not in a preamble.
- **Docstrings**: only when they say something the name and signature don't — drop them otherwise. Keep them short. A contract that genuinely needs a few lines (interacting flags, retry semantics, an edge case) is fine; a narrative is not.
- **No song and dance.** Comment the constraint or the edge case. Not the narrative, not the rationale, not what the next line does.

View File

@@ -1,19 +0,0 @@
---
paths:
- "tests/**/*.py"
---
# pytest conventions
For the Python integration suite under `tests/`. Setup, running, and suite layout live in [`docs/contributing/tests/integration.md`](../../docs/contributing/tests/integration.md).
- **No `_`-prefixed helper functions in test modules — this is the rule that matters most.** A reader must be able to see what a test does in its body alone, without chasing private helpers that scatter the meaning across the file. Inline the logic: an expression, a comprehension, a few repeated lines are all fine — repetition across tests is cheaper than indirection. When several tests genuinely share non-trivial setup or assertions, that is what fixtures are for — in `tests/fixtures/`, see the next rule. A module-level `_helper()` is never the answer.
- **Fixtures live in `tests/fixtures/` — never under `integration/tests/`.** Not in test modules, not in suite `conftest.py` files. `tests/fixtures/` is the shared library (auth, signoz, clickhouse, logs/metrics/traces seeding, …): reuse what's there before writing anything new; when a new fixture is genuinely needed, add it to the matching `tests/fixtures/` module and register new modules in `tests/conftest.py` `pytest_plugins`. **The one exception: SigNoz-level fixtures in a suite's `conftest.py`.** A suite that needs its own SigNoz spun up with different envs (`create_signoz`/`create_migrator` with `env_overrides` + `cache_key` — e.g. basepath, metricreduction, querier_json_body) keeps that in its `conftest.py`; that is always okay.
- **Fixture only when there is a lifecycle; otherwise a plain function.** A fixture earns its indirection by owning setup/teardown (`yield` + cleanup — `insert_metrics` truncating on teardown) or by provisioning a resource (containers, SigNoz instances). A stateless action or lookup (`create_saved_view`, `find_saved_view_by_name`, wiping a resource list) is a plain importable function in the matching `tests/fixtures/` module, taking `signoz`/`token` as ordinary arguments — never wrap a plain callable in a fixture-factory just to inject `signoz`.
- **Fixtures own their cleanup.** When a test needs seeded state, put the seed + cleanup pair in a fixture (`yield`, then tear down) so tests in the same suite don't interfere — the pattern `insert_metrics` sets: yield a callable, truncate on teardown.
- **Fixture-factory over indirect parametrization.** A fixture that yields a callable (e.g. `insert_metrics(metrics)`) is clearer than `@pytest.mark.parametrize(..., indirect=True)` + `request.param` — the value is an explicit argument, not resolved by magic.
- **Skip at collection, not inside the test body.** Use `pytest.param(..., marks=pytest.mark.skip(reason="…"))` so a skipped case shows as SKIPPED-with-reason **and** short-circuits before its fixtures run (no environment spin-up for a test that won't execute).
- **Test config comes from explicit `--flags`, not the environment.** Wire configuration as pytest options declared in `tests/conftest.py` (`pytest_addoption` — e.g. `--sqlstore-provider`, `--clickhouse-version`); do **not** add `os.environ` fallbacks inside tests or fixtures.
- **snake_case parametrize ids.** `ids=["fill_gaps", "fill_zero"]`, not camelCase.
- **Name suite files with the two-digit prefix (`NN_*.py`).** `pyproject.toml` restricts collection to `[0-9][0-9]_*.py` (plus the bootstrap `setup.py` / `run.py`) — a file that doesn't match is silently never collected.
- **Always run pytest from `tests/`.** `--import-mode=importlib` is what allows same-basename files across suites (`querier/01_logs.py` vs `rawexportdata/01_logs.py`), but it disables pytest's implicit `sys.path` injection — `import fixtures` only resolves via `pythonpath = ["."]` from that rootdir.

View File

@@ -1,13 +1,85 @@
<!--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
## Pull Request
---
### 📄 Summary
> Why does this change exist?
> What problem does it solve, and why is this the right approach?
#### Screenshots / Screen Recordings (if applicable)
> Include screenshots or screen recordings that clearly show the behavior before the change and the result after the change. This helps reviewers quickly understand the impact and verify the update.
<!--Reference issues using `Closes #issue-number` to enable automatic closure on merge. -->
#### Issues closed by this PR
> Reference issues using `Closes #issue-number` to enable automatic closure on merge.
<!--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
### ✅ Change Type
_Select all that apply_
<!--Please delete paragraphs that you did not use before submitting.-->
- [ ] ✨ Feature
- [ ] 🐛 Bug fix
- [ ] ♻️ Refactor
- [ ] 🛠️ Infra / Tooling
- [ ] 🧪 Test-only
---
### 🐛 Bug Context
> Required if this PR fixes a bug
#### Root Cause
> What caused the issue?
> Regression, faulty assumption, edge case, refactor, etc.
#### Fix Strategy
> How does this PR address the root cause?
---
### 🧪 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:
- Potential regressions:
- Rollback plan:
---
### 📝 Changelog
> Fill only if this affects users, APIs, UI, or documented behavior
> Use **N/A** for internal or non-user-facing changes
| Field | Value |
|------|-------|
| Deployment Type | Cloud / OSS / Enterprise |
| Change Type | Feature / Bug Fix / Maintenance |
| Description | User-facing summary |
---
### 📋 Checklist
- [ ] Tests added or explicitly not required
- [ ] Manually tested
- [ ] Breaking changes documented
- [ ] Backward compatibility considered
---
## 👀 Notes for Reviewers
<!-- Anything reviewers should keep in mind while reviewing -->
---

83
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,83 @@
name: "Update PR labels and Block PR until related docs are shipped for the feature"
on:
pull_request:
branches:
- main
types: [opened, edited, labeled, unlabeled]
permissions:
pull-requests: write
contents: read
jobs:
docs_label_check:
runs-on: ubuntu-latest
steps:
- name: Check PR Title and Manage Labels
uses: actions/github-script@v6
with:
script: |
const prTitle = context.payload.pull_request.title;
const prNumber = context.payload.pull_request.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
// Fetch the current PR details to get labels
const pr = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber
});
const labels = pr.data.labels.map(label => label.name);
if (prTitle.startsWith('feat:')) {
const hasDocsRequired = labels.includes('docs required');
const hasDocsShipped = labels.includes('docs shipped');
const hasDocsNotRequired = labels.includes('docs not required');
// If "docs not required" is present, skip the checks
if (hasDocsNotRequired && !hasDocsRequired) {
console.log("Skipping checks due to 'docs not required' label.");
return; // Exit the script early
}
// If "docs shipped" is present, remove "docs required" if it exists
if (hasDocsShipped && hasDocsRequired) {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: prNumber,
name: 'docs required'
});
console.log("Removed 'docs required' label.");
}
// Add "docs required" label if neither "docs shipped" nor "docs required" are present
if (!hasDocsRequired && !hasDocsShipped) {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: ['docs required']
});
console.log("Added 'docs required' label.");
}
}
// Fetch the updated labels after any changes
const updatedPr = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber
});
const updatedLabels = updatedPr.data.labels.map(label => label.name);
const updatedHasDocsRequired = updatedLabels.includes('docs required');
const updatedHasDocsShipped = updatedLabels.includes('docs shipped');
// Block PR if "docs required" is still present and "docs shipped" is missing
if (updatedHasDocsRequired && !updatedHasDocsShipped) {
core.setFailed("This PR requires documentation. Please remove the 'docs required' label and add the 'docs shipped' label to proceed.");
}

View File

@@ -53,21 +53,6 @@ jobs:
with:
PRIMUS_REF: main
GO_VERSION: 1.24
semconv-generated:
if: |
github.event_name == 'merge_group' ||
(github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))
runs-on: ubuntu-latest
steps:
- name: self-checkout
uses: actions/checkout@v4
- name: go-install
uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: check-semconv-generated-files
run: go run ./scripts/semconv -check
build:
if: |
github.event_name == 'merge_group' ||

View File

@@ -61,7 +61,6 @@ jobs:
- querierauthz
- role
- rootuser
- savedview
- serviceaccount
- spanmapper
- querier_json_body

6
.gitignore vendored
View File

@@ -90,6 +90,8 @@ queries.active
.devenv/**/tmp/**
.qodo
.dev
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
@@ -229,6 +231,4 @@ cython_debug/
# LSP config files
pyrightconfig.json
# dev
.dev/
.claude/worktrees/

View File

@@ -233,10 +233,6 @@ py-clean: ## Clear all pycache and pytest cache from tests directory recursively
##############################################################
# generate commands
##############################################################
.PHONY: semconv-generate
semconv-generate: ## Regenerate semantic-convention families for Go and TypeScript
@go run ./scripts/semconv
.PHONY: gen-mocks
gen-mocks:
@echo ">> Generating mocks"

File diff suppressed because it is too large Load Diff

View File

@@ -349,7 +349,7 @@ func (Step) JSONSchema() (jsonschema.Schema, error) {
### `oneOf` with a discriminator
For a sum type whose variants are keyed by a property (e.g. `kind`), expose the variants via `JSONSchemaOneOf()` and add a discriminator. Without it, code generators intersect the variants (`A & B & C`) instead of producing a clean discriminated union (`A | B | C`). How to model the sum type itself is covered in [types.md](types.md#sum-types-the-kindspec-envelope) — this section is only about its schema.
For a sum type whose variants are keyed by a property (e.g. `kind`), expose the variants via `JSONSchemaOneOf()` and add a discriminator. Without it, code generators intersect the variants (`A & B & C`) instead of producing a clean discriminated union (`A | B | C`).
The parent keeps its `JSONSchemaOneOf()` (the `oneOf` itself) and *additionally* tags it via `PrepareJSONSchema` with the `x-signoz-discriminator` extension; `signoz.attachDiscriminators` then promotes that marker to a real OpenAPI 3 `discriminator` (and strips the duplicate parent properties) after reflection.

View File

@@ -99,69 +99,6 @@ Each flavor exists for a concrete reason:
The core `AuthDomain` holds the two live halves — `storableAuthDomain` and `authDomainConfig` — and owns business methods such as `Update(config)`. Conversions use the `New<Output>From<Input>` form: `NewAuthDomainFromConfig`, `NewAuthDomainFromStorableAuthDomain`, `NewGettableAuthDomainFromAuthDomain`.
## Sum types: the kind/spec envelope
When a domain type is a *sum type* — exactly one of several variants, selected by a discriminator — model it as an envelope with a `kind` and a `spec`:
```go
type FooConfig struct {
Kind FooKind `json:"kind" required:"true"`
Spec any `json:"spec" required:"true"`
}
```
```json
{ "kind": "bar", "spec": { "url": "...", "timeout": "30s" } }
```
`Kind` is a `valuer.String` enum implementing `Enum()`; `Spec` holds exactly one concrete variant type (`BarSpec`, `BazSpec`, …). `RuleThresholdData` and `EvaluationEnvelope` in `pkg/types/ruletypes/` are the canonical in-tree examples; the dashboard panel/query/variable plugins in `pkg/types/dashboardtypes/` are the same pattern behind generics. (`QueryEnvelope` in querybuildertypes uses `type` as the discriminator key for historical reasons; new envelopes use `kind`.)
### The envelope goes at the point of variance, not the resource root
Put the envelope on the field that actually varies. The resource root is almost never a sum type — a `Foo` has a `name` and an `enabled` flag regardless of which kind it is configured with; only its configuration varies, so the envelope is the `config` field:
```json
{ "name": "my-foo", "enabled": true, "config": { "kind": "bar", "spec": { "...": "..." } } }
```
Hoisting `kind`/`spec` to the root would turn the whole resource into a `oneOf`: every flavor (`PostableFoo`, `UpdatableFoo`, `GettableFoo`) then needs one variant schema per kind, each repeating the common fields; every new common field has to be added to all of them; and generated clients get unions of large objects instead of one small union that narrows on `config.kind`. A root-level `kind` also collides with the resource-model meaning of the word — root `kind` conventionally answers "what resource is this" (`Dashboard`), never "which flavor of config does it hold".
The existing domains already follow this placement:
- **Rules** — plain root; envelopes on the varying fields: `thresholds: {kind, spec}` and `evaluation: {kind, spec}`.
- **Dashboards** — metadata at the root plus one typed `spec`; the unions sit deep inside, at each panel/query/variable plugin (`{kind, spec}` in `perses_plugin_wrappers.go`).
- **Saved views** — root `{schemaVersion, spec}`, where `spec` is a *versioning* envelope holding one fixed type, not a union; the unions are inside it (`spec.queries: [{type, spec}]`). Same word, different job — a versioned body is not a discriminated union.
### Why this tagging style
Of the union encodings in common use, the envelope is the *adjacently tagged* one — tag and payload side by side. Variant payloads stay collision-free, and each kind maps to a named wrapper schema that carries the discriminator, which is exactly what OpenAPI generators need. The alternatives lose on those points: *internally tagged* (`{"kind": "bar", ...fields flattened}`) mixes common and variant fields, admits cross-variant key collisions, and forces every variant schema to redeclare the discriminator; *sibling optional fields* (`{"kind": "bar", "barConfig": {}, "bazConfig": {}}`) is the anti-pattern the first rule below exists to prevent.
The rules that make the envelope work:
- **Never model variants as sibling fields.** A struct with `Bar *BarSpec`, `Baz *BazSpec` next to a discriminator cannot be expressed as an OpenAPI discriminated union, forces nilability checks on every consumer, and silently admits contradictory payloads (kind=bar with a baz spec). The chosen variant *is* the payload.
- **The envelope owns `UnmarshalJSON`.** Decode `kind` first, then switch on it to decode and validate the matching concrete type into `Spec`. Unknown kinds and missing specs are rejected at the boundary:
```go
func (typ *FooConfig) UnmarshalJSON(data []byte) error {
var raw map[string]json.RawMessage
// ... unmarshal raw, decode raw["kind"] ...
switch kind {
case FooKindBar:
spec := BarSpec{}
if err := json.Unmarshal(raw["spec"], &spec); err != nil {
return err
}
typ.Spec = spec
// ... one case per kind, default rejects ...
}
typ.Kind = kind
return nil
}
```
- **Consumers type-assert on `Spec`** (`config.Spec.(BarSpec)`) after switching on `Kind`. If assertion sites multiply, add typed accessors on the envelope (see `EvaluationEnvelope.GetEvaluation()`).
- **OpenAPI needs one unexported variant struct per kind** (`fooConfigBar{Kind; Spec BarSpec}`), exposed via `JSONSchemaOneOf()` and mapped via `PrepareJSONSchema` with the `x-signoz-discriminator` extension. The schema mechanics are covered in [handler.md](handler.md#oneof-with-a-discriminator).
- **A legacy persisted shape gets a data migration or a `StorableX`.** When rows were written before the envelope existed, prefer an idempotent `sqlmigration` that rewrites them into the new shape, so the storable type simply nests the envelope. Only when the old shape must keep being written (external writers, rollback windows) keep it in a storable twin and convert at the type boundary.
## Conventions that tie the flavors together
- **Conversions** use either a `New<Output>From<Input>` constructor — e.g. `NewChannelFromReceiver`, `NewGettableAuthDomainFromAuthDomain` — or a receiver-style `ToY()` method. Both forms coexist in the codebase; use whichever fits the call site.
@@ -202,8 +139,6 @@ Both are optional. Do not introduce them if `PostableX` already covers the case.
- Every domain package defines the core type `X`. Only `X` is mandatory.
- Add `PostableX` / `GettableX` / `UpdatableX` / `StorableX` one at a time, only when the shape actually diverges from `X`.
- Model sum types as a `{kind, spec}` envelope with a validating `UnmarshalJSON` — never as sibling variant fields next to a discriminator.
- The envelope goes on the field that varies, never at the resource root — common fields stay on the resource, outside the union.
- Domain logic lives on `X`, not on the flavor types.
- Conversions can be a `New<Output>From<Input>` constructor or a receiver-style `ToY()` method — pick whichever reads best at the call site.
- Use a type alias when two shapes are truly identical.

View File

@@ -1,377 +1,123 @@
# PromQL Serving — clickhouseprometheusv2
This document gives the context for `pkg/prometheus/clickhouseprometheusv2`.
This package is the second-generation ClickHouse-backed Prometheus provider.
The document tells you why the package exists. It tells you the correctness
rules that shaped it. It shows how we prove that each construct does not
change results. Keep these invariants when you change the provider. If your
change breaks an invariant, flag it and discuss it first.
This document is the subsystem context for `pkg/prometheus/clickhouseprometheusv2`,
the second-generation ClickHouse-backed Prometheus provider. It explains why the
package exists, the correctness constraints that shaped it, and how each fetch
reduction is proven not to change results. Any change to the provider must keep
these invariants; if a change would violate one, it must be flagged and
discussed.
---
## Why a second provider
The v1 provider (`pkg/prometheus/clickhouseprometheus`) serves the promql
engine through the remote-read protobuf adapter. It fetches every raw sample
of a query's union window. It serializes all of them and gives them to the
engine. The cost follows the ingested data, not the question. This is how a
dashboard of PromQL panels can take an instance down.
The v1 provider (`pkg/prometheus/clickhouseprometheus`) serves the promql engine
through the remote-read protobuf adapter: every raw sample of a query's union
window is fetched, serialized, and handed to the engine. The cost is a function
of ingested data, not of the question asked — which is how a dashboard of PromQL
panels can take an instance down.
In v2, each query runs in one of two ways. The classifier decides per query:
In v2 the stock promql engine evaluates over a native `storage.Querier`: no
translation layer, per-selector fetch windows, and fetch reductions that are
provably invisible to the engine.
- **Transpiled**: ClickHouse evaluates the query. Only final (or near-final)
per-group grid arrays come back. The statements use the
`timeSeries*ToGrid` aggregate functions. The supported ClickHouse floor is
25.6 or later, so these functions are assumed available.
- **Engine**: the stock promql engine evaluates over this package's native
`storage.Querier`. Every shape that does not transpile takes this path.
**The core rule: a PromQL result that differs from upstream Prometheus is a
lost user. A construct that cannot reproduce engine semantics exactly falls
back. It does not approximate.** The conformance suite
**The core constraint: every reduction either preserves engine semantics exactly
or is not performed.** A PromQL result that differs from upstream Prometheus is
a lost user. The conformance suite
(`tests/integration/tests/promqlconformance/`) replays Prometheus' own test
corpus against both providers. It is the arbiter. The classification golden
(`testdata/classification_golden.json`) freezes the route of each corpus
expression. The rest of this document is the PromQL-to-SQL story. That
mapping is where correctness is won or lost.
corpus against both providers and is the arbiter.
---
## The evaluation model the SQL must reproduce
A PromQL range query is an instant query evaluated at each grid point
`t_i = start + i*step`, for `i = 0..(end-start)/step`. At each `t_i`:
- An instant selector resolves to the latest sample in the left-open
lookback window `(t_i - lookback, t_i]`. If that latest sample is a stale
marker, the selector resolves to nothing. Older real samples in the window
do not change this.
- A range selector `[r]` collects every sample in `(t_i - r, t_i]`. Stale
markers are excluded.
- `offset d` shifts both windows to `(t_i - d - w, t_i - d]`.
The transpilation invariant follows from this model. Each transpiled
construct produces one array per output series. The array has exactly one
slot per grid point. Slot `i` holds the value at `t_i`. NULL means absent.
This makes composition correct, not only convenient. The engine evaluates
these operators independently per `t_i`. A representation that gets every
slot right gets the whole query right. Spatial aggregation over arrays is
sound because it combines values that belong to the same `t_i` by
construction. Scan time maps slot `i` back to `t_i = start + i*step`
(`toMatrix`). The sections below fill those slots with exactly the numbers
the engine computes. We validated each equivalence against the vendored
engine on live data before its shape entered the allowlist. An unproven
shape stays on the engine path.
## Classification: finding what a statement can answer
`classify` walks the parsed AST and looks for "core units". A core unit is a
maximal subtree of this shape:
[agg by/without (...)] [fn(] selector[range] [offset d] [)] [op scalar]...
`classifyCore` peels that chain from the outside in. It takes an optional
sum/min/max/avg/count aggregation. It then takes one allowlisted function or
a bare instant selector. It then takes the selector with its offset. On the
way out, it collects number-literal arithmetic, comparisons (including
`bool`), and unary minus into a scalar-op pipeline. A node qualifies only if
its type, arguments, and children are in the proven set. This is an
allowlist. An overlooked construct becomes a fallback, not a wrong number.
Three unit kinds come out. Each kind has its own SQL form:
- `unitRange`: rate, irate, increase, delta, idelta over a range selector.
- `unitInstant`: instant vector selection, bare or comparison-filtered.
- `unitOverTime`: avg/min/max/sum/count/last `_over_time`.
If the whole tree is one unit, the plan is "full". The statement's rows are
the query result. Otherwise, `rewrite` cuts out each maximal unit and puts a
synthetic selector `__signoz_transpiled_N__` in its place. The engine then
runs the rewritten expression over the units' materialized results. This is
a "hybrid" plan. `histogram_quantile`, `topk`, `or`/`and`/`unless`, and
vector matching keep exact engine semantics. Their expensive inputs were
aggregated server-side.
Classification refuses a shape when it cannot guarantee exact semantics
server-side:
- The `@` modifier, anywhere.
- Default-resolution subqueries. Their resolution is a server runtime
setting that the transpiler cannot see.
- Duration expressions (`offset step()`, `[range()]`, ...), anywhere. The
engine resolves them into the selector's static fields only at evaluation
time. At classification time those fields hold zero values. A transpile
would silently use the wrong offset or range.
- Steps or ranges that are not whole seconds. The grid functions take
whole-second parameters.
- Grouping by `__name__`, or matching on it, in hybrid plans. The synthetic
name would leak into results.
- Name-keeping units in hybrid plans. Bare and comparison-filtered instant
selectors and `last_over_time` keep their real `__name__` (`keepsName`).
Substitution would replace that name. These units transpile only as full
plans.
- Every function outside the allowlist: changes, resets,
quantile_over_time, absent, native-histogram functions, and more.
Units inside a fixed-resolution subquery evaluate on the subquery's own
grid, not the query grid. That grid is the set of epoch-aligned multiples of
the resolution strictly after `outerStart - offset - range`, ending at
`outer end - offset`. This is the exact derivation the engine uses. A grid
shifted by one step changes which samples every window sees.
## From one unit to one statement
`buildUnitSQL` renders each unit as one statement. For
`sum by (pod) (rate(m{job="api"}[5m]))` the skeleton is:
SELECT g0, sumForEach(grid) AS grid FROM (
SELECT any(series.g0) AS g0,
timeSeriesRateToGrid(<start>, <end>, <step>, <range>)(fromUnixTimestamp64Milli(unix_milli), value) AS grid
FROM signoz_metrics.distributed_samples_v4 AS points
INNER JOIN (
SELECT fingerprint, JSONExtractString(labels, 'pod') AS g0
FROM signoz_metrics.time_series_v4
WHERE <series predicates>
GROUP BY fingerprint, g0
) AS series ON points.fingerprint = series.fingerprint
WHERE metric_name = ? AND temporality IN ['Cumulative', 'Unspecified']
AND unix_milli > <start - range> AND unix_milli <= <end>
AND bitAnd(flags, 1) = 0
GROUP BY points.fingerprint
) GROUP BY g0
SETTINGS allow_experimental_ts_to_grid_aggregate_function = 1
Read it from the inside out.
**The time window** is the selector's semantics, verbatim. Strict `>` on the
lower bound and `<=` on the upper bound is the left-open `(t - w, t]` rule.
The offset shifts the whole window. `bitAnd(flags, 1) = 0` drops stale
markers. PromQL excludes them from range vectors.
**The inner GROUP BY** computes one grid array per series.
`timeSeriesRateToGrid(start, end, step, range)` is a parametric aggregate.
It takes (timestamp, value) pairs and produces `Array(Nullable(Float64))`
with one slot per grid point. It is correct because it implements the
engine's `extrapolatedRate`, decision for decision: counter resets, the
zero-point clamp, the extrapolation thresholds, the two-samples rule, and
the left-open window. We verified this: we fed identical samples to both and
compared slot for slot. The only observed difference is the last bit.
ClickHouse's C++ and Go round the same formula differently. That is the
floating-point floor, not a semantic gap. irate/delta/idelta map to their
own `timeSeries*ToGrid` functions, with the same verification. `increase`
has no function of its own. We emit
`arrayMap(x -> x * <range seconds>, <rate expr>)`. This is exact by
definition: `extrapolatedRate` computes the same extrapolated delta for both
and divides by the range only when `isRate`. The multiplication reverses it
exactly. The grid parameters render as literals, not bound args. They are
aggregate-function parameters. The experimental gate rides as a SETTINGS
clause on the statement itself, so telemetrystore hooks cannot remove it.
The group key is functionally dependent on the fingerprint: one fingerprint
is the hash of one labelset. So the inner query groups by the fingerprint
alone and reads the key columns with `any()`. This is exact, and it makes
the per-row hash key smaller.
**The join** gives each series its group key, in one of two forms.
`by (...)` extracts each listed label as a plain column
(`JSONExtractString(labels, 'pod') AS g0`) and groups on the columns. The
projection is a known short list, and the label names live in Go. To build,
sort, and stringify every label pair per row would be waste. This is correct
because column-tuple equality is label-set equality on the projection. An
extracted `''` means the label is absent. That is Prometheus semantics for
`by()` over missing labels. The empties are skipped when the columns turn
back into labels. `without` and no-aggregation project a label set that
varies per series. They get the canonical key: `toJSONString` of the sorted
[label, value] pairs that the unit projects. `without` excludes the listed
labels plus `__name__`. No-aggregation keeps everything; the name comes off
in Go, per the engine's name-dropping rules. Here the sort is load-bearing.
Stored JSON key order is not canonical across fingerprints. Two orderings of
the same labels must land in one group. Empty values are filtered for the
same absent-label reason. The same string parses back into the output label
set (`labelsFromGroupKey`).
**The outer GROUP BY** is the spatial aggregation. sum/min/max/avg/count
by/without become the `-ForEach` combinators. Element-wise aggregation over
grid arrays is the engine's per-`t_i` aggregation: slot `i` of every input
array refers to the same `t_i`. The combinators skip NULLs. That is the
engine aggregating only the series present at `t_i`. An index where every
series is absent stays NULL. Two edges need explicit handling. First,
`countForEach` wraps in a map of 0 back to NULL. A count over an all-absent
index is an absent point, not 0. Second, a unit without aggregation still
passes through `maxForEach`. That is the identity for the common
one-fingerprint group. It is a deterministic NULL-skipping merge when a
regex `__name__` selector collapses distinct metrics onto one projected
label set. One caveat is inherent: the summation order over series differs
from the engine's. Spatial aggregates can differ in the last ULP. Float
addition is not associative. No ordering reproduces the engine's result
bit-exactly from inside a GROUP BY.
## Instant selectors: staleness needs two aggregates
`unitInstant` uses window = lookback. It must reproduce the shadowing rule:
the point is absent when the latest in-window sample is a stale marker.
`timeSeriesLastToGrid` alone cannot express that. To skip stale rows in
WHERE would resurrect the older real sample that the marker buried. So stale
rows stay in the scan for this kind only. The grid expression compares three
aggregates per slot:
arrayMap((tall, tok, vok) -> if(tall IS NULL OR tok IS NULL OR tall != tok, NULL, vok),
timeSeriesLastToGrid(...)(ts, toFloat64(unix_milli)), -- last sample overall
timeSeriesLastToGridIf(...)(ts, toFloat64(unix_milli), bitAnd(flags, 1) = 0), -- last non-stale, its timestamp
timeSeriesLastToGridIf(...)(ts, value, bitAnd(flags, 1) = 0)) -- last non-stale, its value
This is correct by cases on a slot's window. No samples at all: both
timestamp aggregates are NULL, so the slot is NULL. That is absent, as the
engine says. Latest sample non-stale: it is the latest overall and the
latest non-stale. The timestamps agree. The slot takes its value. That is
the engine's pick. Latest sample stale: the last-overall timestamp is the
marker's. The last-non-stale timestamp is older, or NULL when the window
holds only markers. They disagree. The slot is NULL. The marker shadows,
exactly as the engine's rule says. Timestamps are unique per series (ingest
dedups). So timestamp equality identifies "the same sample" without
ambiguity. We probed the `-If` combinator against these experimental
aggregates before we trusted it.
## Windowed *_over_time: whole buckets instead of a grid function
avg/min/max/sum/count `_over_time` aggregate every raw sample in the window.
No `timeSeries*ToGrid` function computes them. (`last_over_time` is the
exception. The last sample of a range vector is exactly
`timeSeriesLastToGrid`. PromQL excludes stale markers from range vectors; we
exclude them in WHERE.) These shapes transpile only when the range is a
whole multiple of the step. Then the window needs no per-sample fan-out.
With `W = range/step`, the window `(t_k - range, t_k]` is exactly the union
of W step buckets. Both are left-open on the same boundaries. So bucket
membership fully determines window membership. Each sample lands in exactly
one bucket:
intDiv(unix_milli - <start> + <range> - 1, <step>)
This is `ceil((ts - start)/step)` shifted by W-1, so the earliest in-window
sample sits at 0. Slot k's window is buckets in `[k, k+W-1]`. The
alternative fans each sample into all W windows that cover it. That
multiplies rows by W. For a long range over a short step, that is a row
explosion measured in billions. The bucketed form's row count is
series × buckets: the size of the output, for any W.
Each series aggregates in one group. The `-Resample` combinator
(`sumResample`, `countResample`) holds the dense per-bucket partials inside
one group state: a bucket count, plus the function's value aggregate (sum
for sum/avg, min, max). An earlier form grouped by (series, bucket) and
assembled with `groupArrayInsertAt`. At scale that made 37M hash groups, and
per-thread partials scaled memory with the thread count. The slide then
combines each slot's at-most-W bucket partials by direct aggregation
(`arraySum(arraySlice(...))`). Window sums are added the way the engine adds
them. There is no prefix-sum differencing: its large-minus-large
cancellation would drift past the shadow tolerance on counter-sized values.
This is correct per slot because the bucket union is the exact window
multiset, and avg/min/max/sum/count are order-insensitive on a multiset
(sum/avg up to summation order; see the float caveat above). A slot with
zero window count is absent. min/max filter their slices on the bucket
counts. An empty bucket's default can never look like a value: a real sample
can legitimately be +Inf.
Two shapes fall back to the engine path, which is exact: a range that does
not divide the step, and a window wider than `maxWindowBuckets` buckets (the
slide costs W combines per slot). A range narrower than the step needs
neither gate: the windows are pairwise disjoint, one bucket per slot, no
slide. That form is exact only together with the window-sliver predicate
below.
## Scalar ops, full plans, hybrid plans
The scalar-op pipeline runs in Go on the returned arrays
(`applyScalarOps`), slot by slot. Arithmetic operators compute. Comparisons
filter: the slot keeps the vector-side value or becomes NULL. Under `bool`
they return 0/1. This is trivially correct. It is the same float64 operation
the engine applies, to the same slot value, in the same operator order the
AST dictates. Go instead of another SQL layer changes where, not what.
A full plan's arrays map straight to the result matrix. A hybrid plan
materializes each unit's arrays as synthetic series under its
`__signoz_transpiled_N__` name. The engine evaluates the rewritten
expression over a storage that serves synthetic names from memory and
everything else live. Substitution is sound because a unit's output is a
plain instant vector to the engine: same values at same timestamps, under a
different name. The name cannot matter. Plans that group by or match on
`__name__` were refused at classification. Name-keeping units are never
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`.
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
belongs to no window. It cannot move any grid point, but the grid aggregate
would buffer it. A WHERE predicate keeps only the in-window rows:
`positiveModulo(selStart - unix_milli, step) < window`, with the scan capped
at the last grid point. The lattice anchors at the selector start, because
the end can sit off-lattice on unaligned grids. This cut a 36k-series
one-week rate from 74s/28GiB to 16s/4.3GiB on fleet data. Over slivered
rows, `timeSeriesLastToGrid`'s window widening is harmless, so instant
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)
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
a window that begins mid-bucket still matches the bucket's row. How matchers
become SQL, and why regexes are anchored, is documented at
`applySeriesConditions`. Empty-valued labels come off at this boundary. An
empty value means "label absent" in Prometheus, but stored attribute JSON
can carry them.
Matchers resolve to series once per selector (`selectSeries`) against the series
tables, which hold one row per (fingerprint, bucket) at 1h/6h/1d/1w
granularities. Table selection and window rounding delegate to the shared
metrics schema package (`pkg/telemetryschema/metricstelemetryschema`); the
window start rounds down to the bucket boundary so a window beginning mid-bucket
still matches the bucket's row.
## The engine path
How matchers become SQL is documented at `applySeriesConditions`. The rules that
carry semantics:
Queries that do not transpile run in the stock engine over this package's
`storage.Querier`. This is still not the v1 path. Samples are fetched per
selector with the engine's per-selector hints, not the query-wide union
window. So `foo / foo offset 1d` reads two narrow windows, not the widest
one twice. Instant selectors of subquery-free queries fetch only the last
sample per step bucket (`lastSamplePerStep`). Buckets anchor at the
selector's first evaluation timestamp. The code recovers it from the hints
as `hints.Start + lookback - 1ms`, the inverse of how the engine derives
`hints.Start`. Bucket boundaries then coincide with evaluation timestamps.
A non-final sample of a bucket can never be the latest sample in
`(t - lookback, t]` for any grid `t`. Real timestamps are preserved, so the
engine's own lookback and staleness handling stay exact. Range selectors
always fetch raw: every sample feeds the range function. The subquery-free
proof travels in the context as `prometheus.QueryTraits`. Subquery selectors
evaluate at the subquery's step, while the hints carry the top-level step.
Row assembly maps stale flags to the engine's StaleNaN. It merges series
with identical label sets (`sortAndMerge`): the engine assumes storages
never emit duplicates.
- `__name__` matchers (all four types) translate to the `metric_name` column.
- Every other matcher becomes a `JSONExtractString` condition on the labels
column. An equality matcher against `""` matches series *without* the label,
mirroring PromQL, because `JSONExtractString` returns `""` for missing keys.
- Regexes are anchored (`^(?:...)$`) before they reach `match()`: PromQL
matchers match the whole value, ClickHouse `match()` searches for a
substring.
- The series-lookup upper bound is inclusive (`unix_milli <= end`) because the
exporter floors registration rows to the bucket start: a series first
registered in the bucket beginning exactly at `end` would otherwise be
invisible while its samples are in range.
Empty-valued labels come off at this boundary: an empty value means "label
absent" in Prometheus, but stored attribute JSON can carry them.
---
## Sample fetch
Samples are fetched per selector using the engine's per-selector hints, not the
query-wide union window — `foo / foo offset 1d` reads two narrow windows
instead of the widest one twice.
**Last-sample-per-step reduction.** Instant selectors of subquery-free queries
fetch only the last sample per step bucket. The engine resolves an instant
selector at each grid timestamp `t` to the latest sample in the left-open
lookback window `(t lookback, t]`. Buckets anchor at the selector's first
evaluation timestamp — recovered from the hints as
`hints.Start + lookback 1ms`, the inverse of how the engine derives
`hints.Start` — so bucket boundaries coincide with evaluation timestamps, and a
non-final sample of a bucket can never be the latest sample in
`(t lookback, t]` for any grid `t`. Real timestamps are preserved, so the
engine's own lookback and staleness handling stay exact.
Range selectors always fetch raw — every sample feeds the range function. The
subquery-free proof travels in the context as `prometheus.QueryTraits`, because
subquery selectors evaluate at the subquery's step while the hints carry the
top-level step; call sites that do not attach traits get the conservative raw
fetch.
**Row assembly** maps stale flags to the engine's `StaleNaN` and merges series
with identical label sets (`sortAndMerge`) — the engine assumes storages never
emit duplicates. Duplicate timestamps pass through as stored: uniqueness is
ingest's job, and v1 feeds them to the engine as-is over the same data.
**The fingerprint filter is a shard-local semi-join.** The samples query
restricts to the matched series by re-running the series predicates as an
`IN (SELECT fingerprint FROM <local series table> ...)` subquery, not a GLOBAL
broadcast of the matched set. ClickHouse materializes the subquery's set per
shard before the scan, so it still engages the fingerprint primary-key column.
Because the subquery re-executes the predicates after the lookup ran, it can
match series registered in between; sample rows whose fingerprint the lookup
never saw are skipped — the lookup is the read snapshot.
---
## Sharding
`samples_v4` and `time_series_v4` (and all their rollups) shard on the same
key: `cityHash64(env, temporality, metric_name, fingerprint)`. So a series'
samples and catalog rows live on the same shard. The transpiled statement
exploits that. The distributed samples table at the top-level FROM makes
ClickHouse rewrite the whole inner query per shard. The join against the
shard-local series table and the per-series grid aggregation run next to
the data. The initiator only merges aggregate states and applies the
spatial `-ForEach` step. This is the same layout as the telemetrymetrics
statement builder. The group-key join alone restricts the transpiled scan
to the matched series. The engine path's samples fetch restricts by the
same predicates as a shard-local semi-join, not a GLOBAL broadcast of the
matched set. The temporality filter on every samples statement is a
semantic no-op: the matched fingerprints already come from those
temporalities. It engages the leading samples primary-key column.
Delta-temporality series stay invisible to PromQL here, exactly as in v1.
The rollout gate is parity with v1. To make Delta visible is its own change
with its own semantics to design. A Delta stream fed to `rate()`
`samples_v4` and `time_series_v4` (and all their rollups) shard on the same key
`cityHash64(env, temporality, metric_name, fingerprint)` — so a series'
samples and catalog rows live on the same shard. The semi-join above exploits
that: each shard filters by its own series rows, which are exactly the series
of that shard's samples.
The temporality filter on every samples statement
(`temporality IN ['Cumulative', 'Unspecified']`) is a semantic no-op — the
matched fingerprints already come from those temporalities — that engages the
leading samples primary-key column.
Delta-temporality series stay invisible to PromQL exactly as they are in v1:
the rollout gate is parity with v1, and a Delta stream fed to `rate()`
as-if-cumulative would be wrong, not just new.
---
## Observability
Every statement carries a `log_comment` with
`code.namespace=clickhouse-prometheus-v2` and `code.function.name` naming
the call site (`selectSeries`, `selectSamples`, `transpiledUnit`,
`LabelValues`, `LabelNames`). This provider's work is attributable in
`system.query_log` without guessing from query text.
`code.namespace=clickhouse-prometheus-v2` and `code.function.name` naming the
call site, so this provider's work is attributable in `system.query_log`.

View File

@@ -98,6 +98,14 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
Route: "",
})
if constants.IsDotMetricsEnabled {
for idx, feature := range featureSet {
if feature.Name == licensetypes.DotMetricsEnabled {
featureSet[idx].Active = true
}
}
}
ah.Respond(w, featureSet)
}

View File

@@ -17,3 +17,15 @@ func GetOrDefaultEnv(key string, fallback string) string {
}
return v
}
// constant functions that override env vars
const DotMetricsEnabled = "DOT_METRICS_ENABLED"
var IsDotMetricsEnabled = false
func init() {
if GetOrDefaultEnv(DotMetricsEnabled, "true") == "true" {
IsDotMetricsEnabled = true
}
}

View File

@@ -487,11 +487,8 @@
// Simplifies boolean returns
"sonarjs/prefer-while": "error",
// Suggests while loops over for loops
"sonarjs/elseif-without-else": "off",
"sonarjs/elseif-without-else": "off"
// Requires final else in if-else-if chains (was disabled)
"signoz/no-conditional-text-nodes-with-siblings": "warn",
// Vendored from eslint-plugin-react-google-translate
"signoz/no-return-text-nodes": "warn"
},
"ignorePatterns": [
"src/parser/*.ts",

View File

@@ -16,7 +16,6 @@
"lint:generated": "oxlint ./src/api/generated --fix",
"lint:fix": "oxlint ./src --fix",
"lint:styles": "stylelint \"src/**/*.scss\"",
"test:plugins": "node --test \"plugins/__tests__/*.test.mjs\"",
"jest": "jest",
"jest:coverage": "jest --coverage",
"jest:watch": "jest --watch",
@@ -126,7 +125,6 @@
"rrule": "2.8.1",
"styled-components": "^5.3.11",
"timestamp-nano": "^1.0.0",
"translation-resilience": "^0.2.0",
"typescript": "5.9.3",
"uplot": "1.6.31",
"uuid": "14.0.1",

View File

@@ -1,130 +0,0 @@
# Plugin rule tests
Tests for the custom oxlint rules in `plugins/rules/`.
```bash
pnpm test:plugins
```
Runs on `node --test` rather than jest. The jest config is built for application
code — jsdom, ts-jest ESM transforms, a large `transformIgnorePatterns` wall —
and none of it applies to a suite whose only job is to shell out to the linter.
## Why it drives the real binary
Each case is written to a temp file and linted by the actual `oxlint` binary,
with every builtin category switched off so the only diagnostics that can appear
belong to the rule under test. Assertions therefore describe what CI enforces.
The alternative — walking the AST in-process — would need a stand-in for
oxlint's JS plugin AST. That AST is ESTree-shaped but not ESTree, and it carries
no type information, so a stand-in would drift from the runtime it claims to
model and the tests would certify behaviour that never happens.
All cases in a suite share one `oxlint` invocation and are mapped back by
filename. Per-case spawning costs roughly 80ms each; batching keeps both suites
together at around 250ms.
## Adding a suite
```js
import { ruleTester } from './rule-tester.mjs';
await ruleTester({
rule: 'no-navigator-clipboard',
valid: ['const x = 1;'],
invalid: [
{
code: 'navigator.clipboard.writeText("x");',
errors: [{ message: 'useCopyToClipboard', line: 1, column: 1 }],
},
],
});
```
`ruleTester` must be awaited at the top level — it loads the plugin and runs
`oxlint` before declaring the tests.
- `rule` — the key the plugin exports it under. `plugin` defaults to
`plugins/signoz.mjs`; pass a path relative to `frontend/` for another plugin.
- Cases are `.tsx` unless a `filename` gives another extension.
- `errors` takes a count or an array. Each entry may assert `message` (substring
or `RegExp`), `line` and `column`; omitted fields are not checked.
- `name` labels the case in the output and defaults to its first line of code.
- `output` asserts the source after suggestions are applied — see below.
- `todo` marks a case as a known defect — see below.
## Suggestions
Both Google Translate rules attach their wrap as a *suggestion*, not a fix, so
`--fix` leaves the code alone and `--fix-suggestions` applies it. The wrap is
`<span className="translate-safe">`, and `.translate-safe` is `display: contents`
in `src/styles.scss`: React owns an element that absorbs Translate's `<font>`
swap, while the box tree stays as it was, so a flex or grid parent still sees one
contiguous text run rather than a new item with its own `gap`.
It stays a suggestion because the element is still a DOM child even with no box:
`> *`, `:nth-child` and sibling selectors still count it, and a component that
inspects its children — `React.Children.map`, antd `Tooltip`, `Space` — sees an
element where a string used to be. That is what oxlint means by "May change
program behavior" in `--fix-suggestions --help`.
An invalid case carrying `output` is linted twice: once for diagnostics, and
once with `--fix-suggestions` over an untouched copy of the same files. The
second run costs one extra `oxlint` spawn per suite and only happens when at
least one case asks for it.
```js
{
code: "export const A = () => <div>{f ? 'a' : 'b'}<b/></div>;",
errors: 2,
output:
'export const A = () => <div>{f ? <span className="translate-safe">a</span> : <span className="translate-safe">b</span>}<b/></div>;',
}
```
Suggestions do not reformat, so a real run is `oxlint --fix-suggestions` then
`oxfmt`.
## Known defects
A case carrying `todo` asserts what the rule *should* do. It still runs, but a
failure is reported as a todo rather than failing the suite, so a bug can be
pinned as an executable spec instead of prose. Fixing the rule turns the todo
green; deleting the flag then makes it a regression guard.
Cases are prefixed `FP:` where the rule reports something it should not, `GAP:`
where it misses something it should catch, and `TYPE-AWARE:` where the miss is
only fixable once the linter can resolve types. Everything without a flag is a
characterisation test recording current behaviour.
The current todos:
**Gaps — constructs the rules never visit.** `isProblematicConditional` requires
a `JSXElement` parent, so a conditional inside a fragment is never inspected even
though the failure does not care about the parent's kind. `no-return-text-nodes`
listens only for `FunctionDeclaration` and reads the name off `node.id`, so
arrow-function components and anonymous default exports are invisible — this
codebase writes components as arrow functions, which is why that rule reports
nothing across `src`.
Class components are left out deliberately rather than pinned as a gap: there
are none in `src`.
**Type-aware gaps.** Upstream resolves branch types through
`@typescript-eslint/utils` and reports anything typed `string` or `number`.
oxlint's JS plugin runtime exposes no type information — `sourceCode.parserServices`
is always `{}` — so those code paths were removed rather than left dormant. The
`TYPE-AWARE:` todos record what they used to catch, and become the acceptance
criteria if oxlint ever hands types to JS plugins.
## Not a defect
Without types, `no-conditional-text-nodes-with-siblings` falls back to a callee
allowlist (`t`, `formatMessage`, `toString`, `toLocaleString`). Cases around
that allowlist pin its edges; widening it is the supported way to catch more
call expressions.
Both branches of a ternary are reported separately, so one fix can clear two
diagnostics. That inflates the count but every reported node is genuinely a text
node, so the cases assert both.

View File

@@ -1,302 +0,0 @@
import { ruleTester } from './rule-tester.mjs';
const CONDITIONAL = 'Conditionally rendered text nodes with siblings';
const PRECEDED = 'Text nodes which are preceded by a conditional expression';
await ruleTester({
rule: 'no-conditional-text-nodes-with-siblings',
valid: [
{
name: 'conditional text node without siblings',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? 'yes' : 'no'}\n\t</div>\n);",
},
{
name: 'boolean branches are not text',
code:
'export const A = () => (\n\t<div>\n\t\t{flag ? true : false}\n\t\t<span>x</span>\n\t</div>\n);',
},
{
name: 'null branches are not text',
code:
'export const A = () => (\n\t<div>\n\t\t{flag ? null : null}\n\t\t<span>x</span>\n\t</div>\n);',
},
{
name: 'element branches are already wrapped',
code:
'export const A = () => (\n\t<div>\n\t\t{flag ? <b>y</b> : <i>n</i>}\n\t\t<span>x</span>\n\t</div>\n);',
},
{
name: 'text node before the conditional is safe',
code:
'export const A = () => (\n\t<div>\n\t\tleading text\n\t\t{flag && <b>y</b>}\n\t</div>\n);',
},
{
name: 'member expression on the condition side is not rendered',
code:
'export const A = () => (\n\t<div>\n\t\t{obj.name && <b>y</b>}\n\t\t<span>x</span>\n\t</div>\n);',
},
{
name: 'binary comparison on the condition side is not rendered',
code:
"export const A = () => (\n\t<div>\n\t\t{obj.name === 'x' && <b>y</b>}\n\t\t<span>x</span>\n\t</div>\n);",
},
// An empty string renders no text node at all, so there is nothing for
// Google Translate to wrap and nothing for React to lose. Reporting it used
// to be the rule's most common false positive.
{
name: 'element branch with an empty-string fallback',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? <b>Free Trial</b> : ''}\n\t\t<span>s</span>\n\t</div>\n);",
},
{
name: 'both branches empty',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? '' : ''}\n\t\t<span>s</span>\n\t</div>\n);",
},
{
name: 'logical and with an empty-string right-hand side',
code:
"export const A = () => (\n\t<div>\n\t\t{flag && ''}\n\t\t<span>s</span>\n\t</div>\n);",
},
// A template literal is checked the same way as the quoted form, so `{' '}`
// and ``{` `}`` agree.
{
name: 'empty template literal branch',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? `` : ''}\n\t\t<span>s</span>\n\t</div>\n);",
},
{
name: 'whitespace-only template literal branch is skipped',
code:
'export const A = () => (\n\t<div>\n\t\t{flag ? ` ` : <b>x</b>}\n\t\t<span>s</span>\n\t</div>\n);',
},
],
invalid: [
{
name: 'string literal branches with an element sibling',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? 'yes' : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 19 },
],
output:
'export const A = () => (\n\t<div>\n\t\t{flag ? <span className="translate-safe">yes</span> : <span className="translate-safe">no</span>}\n\t\t<span>x</span>\n\t</div>\n);',
},
{
name: 'logical and with a string right-hand side',
code:
"export const A = () => (\n\t<div>\n\t\t{flag && 'yes'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [{ message: CONDITIONAL, line: 3, column: 12 }],
},
{
name: 'numeric literals render as text',
code:
'export const A = () => (\n\t<div>\n\t\t{flag ? 1 : 2}\n\t\t<span>x</span>\n\t</div>\n);',
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 15 },
],
output:
'export const A = () => (\n\t<div>\n\t\t{flag ? <span className="translate-safe">{1}</span> : <span className="translate-safe">{2}</span>}\n\t\t<span>x</span>\n\t</div>\n);',
},
{
name: 'template literal branch',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? `yes ${n}` : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 24 },
],
output:
'export const A = () => (\n\t<div>\n\t\t{flag ? <span className="translate-safe">{`yes ${n}`}</span> : <span className="translate-safe">no</span>}\n\t\t<span>x</span>\n\t</div>\n);',
},
{
name: 'member expression branch',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? obj.name : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 22 },
],
output:
'export const A = () => (\n\t<div>\n\t\t{flag ? <span className="translate-safe">{obj.name}</span> : <span className="translate-safe">no</span>}\n\t\t<span>x</span>\n\t</div>\n);',
},
{
name: 'a string needing escapes stays inside braces',
code:
'export const A = () => (\n\t<div>\n\t\t{flag ? "it\'s" : <b>x</b>}\n\t\t<span>s</span>\n\t</div>\n);',
errors: [{ message: CONDITIONAL, line: 3, column: 11 }],
output:
'export const A = () => (\n\t<div>\n\t\t{flag ? <span className="translate-safe">{"it\'s"}</span> : <b>x</b>}\n\t\t<span>s</span>\n\t</div>\n);',
},
{
name: 'optional chaining branch',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? obj?.deep?.name : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 29 },
],
},
{
name: 'nested ternary reports every text branch',
code:
"export const A = () => (\n\t<div>\n\t\t{a ? (b ? 'x' : 'y') : 'z'}\n\t\t<span>s</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 13 },
{ message: CONDITIONAL, line: 3, column: 19 },
{ message: CONDITIONAL, line: 3, column: 26 },
],
},
{
name: 'static text following a conditional',
code:
'export const A = () => (\n\t<div>\n\t\t{flag && <b>y</b>}\n\t\ttrailing text\n\t</div>\n);',
errors: [{ message: PRECEDED, line: 3, column: 21 }],
// Only the visible run is wrapped; the surrounding newlines and tabs are
// formatting and must stay outside the element.
output:
'export const A = () => (\n\t<div>\n\t\t{flag && <b>y</b>}\n\t\t<span className="translate-safe">trailing text</span>\n\t</div>\n);',
},
{
name: 'conditional text plus trailing static text reports both kinds',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? 'a' : 'b'}\n\t\tliteral tail\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 17 },
{ message: PRECEDED, line: 3, column: 21 },
],
},
// The callee allowlist below is the untyped fallback. Without type
// information the rule can only recognise known string-returning helpers,
// so `t()` and `formatMessage()` are reported while an arbitrary call is
// not. These cases pin that boundary.
{
name: 't() branch is reported via the callee allowlist',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? t('key') : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 22 },
],
},
{
name: 'formatMessage() branch is reported via the callee allowlist',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? formatMessage({id:'k'}) : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 37 },
],
},
{
name: 'arbitrary call is not recognised, only the literal branch reports',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? getString() : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [{ message: CONDITIONAL, line: 3, column: 25 }],
},
{
name: 'bare identifier is not recognised, only the literal branch reports',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? name : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [{ message: CONDITIONAL, line: 3, column: 18 }],
},
{
name: 'toString() branch is reported',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? val.toString() : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 28 },
],
},
{
name: 'toLocaleString() branch is reported',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? val.toLocaleString() : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 34 },
],
},
{
name: 't() in its own container following a conditional',
code:
"export const A = () => (\n\t<div>\n\t\t{flag && <b>y</b>}\n\t\t{t('key')}\n\t</div>\n);",
errors: [{ message: PRECEDED, line: 4, column: 4 }],
},
{
name: 'toString() in its own container following a conditional',
code:
'export const A = () => (\n\t<div>\n\t\t{flag && <b>y</b>}\n\t\t{val.toString()}\n\t</div>\n);',
errors: [{ message: PRECEDED, line: 4, column: 4 }],
// The whole container is replaced, so the result is not `{<span>{…}</span>}`.
output:
'export const A = () => (\n\t<div>\n\t\t{flag && <b>y</b>}\n\t\t<span className="translate-safe">{val.toString()}</span>\n\t</div>\n);',
},
{
name: 'whitespace-only string branch is skipped, the other branch reports',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? ' ' : 'x'}\n\t\t<span>s</span>\n\t</div>\n);",
errors: [{ message: CONDITIONAL, line: 3, column: 17 }],
},
{
name: 'template literal holding an expression is not blank',
code:
'export const A = () => (\n\t<div>\n\t\t{flag ? `${n}` : <b>x</b>}\n\t\t<span>s</span>\n\t</div>\n);',
errors: [{ message: CONDITIONAL, line: 3, column: 11 }],
},
// Upstream resolves branch types through `@typescript-eslint/utils` and
// reports anything typed `string` or `number`. oxlint's JS plugin runtime
// exposes no type information, so those paths were dropped and only the
// callee allowlist remains. Kept as todos: if oxlint ever hands types to JS
// plugins these become the acceptance criteria.
{
todo: 'needs type information to know the call returns a string',
name: 'TYPE-AWARE: call returning a string',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? getString() : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 25 },
],
},
{
todo: 'needs type information to know the identifier is a string',
name: 'TYPE-AWARE: identifier holding a string',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? name : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 18 },
],
},
{
todo: 'needs type information to know the identifier is a string',
name: 'TYPE-AWARE: string identifier following a conditional',
code:
'export const A = () => (\n\t<div>\n\t\t{flag && <b>y</b>}\n\t\t{label}\n\t</div>\n);',
errors: [{ message: PRECEDED, line: 4, column: 4 }],
},
// `isChildOfJSXElement` matches only `JSXElement`, so a fragment parent is
// never inspected. The Google Translate failure does not care whether the
// parent is an element or a fragment.
{
todo: 'fragment parents are never inspected',
name: 'GAP: conditional text with a sibling inside a fragment',
code:
"export const A = () => (\n\t<>\n\t\t{flag ? 'yes' : 'no'}\n\t\t<span>x</span>\n\t</>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 19 },
],
},
],
});

View File

@@ -1,162 +0,0 @@
import { ruleTester } from './rule-tester.mjs';
const RETURNS_TEXT = 'React components should avoid returning text nodes';
await ruleTester({
rule: 'no-return-text-nodes',
valid: [
{
name: 'lowercase function is not a component',
code: "export function foo() {\n\treturn 'text';\n}",
},
{
name: 'returning JSX',
code: 'export function Foo() {\n\treturn <div>hi</div>;\n}',
},
{
name: 'returning null',
code: 'export function Foo() {\n\treturn null;\n}',
},
{
name: 'returning boolean',
code: 'export function Foo() {\n\treturn true;\n}',
},
{ name: 'bare return', code: 'export function Foo() {\n\treturn;\n}' },
{
name: 'returning a variable is not a literal',
code: "export function Foo() {\n\tconst s = 'x';\n\treturn s;\n}",
},
{
name: 'lowercase nested function inside a component',
code:
"export function Foo() {\n\tfunction helper() {\n\t\treturn 'x';\n\t}\n\treturn <div/>;\n}",
},
{
// The repo has no class components, so this is out of scope rather than
// a gap worth closing.
name: 'class method',
code: "export class Foo {\n\trender() {\n\t\treturn 'text';\n\t}\n}",
},
],
invalid: [
{
name: 'string literal',
code: "export function Foo() {\n\treturn 'text';\n}",
errors: [{ message: RETURNS_TEXT, line: 2, column: 2 }],
output:
'export function Foo() {\n\treturn <span className="translate-safe">{\'text\'}</span>;\n}',
},
{
// JSX does not parse in a `.ts` file, so no suggestion is offered there.
name: 'string literal in a non-JSX file',
filename: 'case.ts',
code: "export function Foo() {\n\treturn 'text';\n}",
errors: [{ message: RETURNS_TEXT, line: 2, column: 2 }],
output: "export function Foo() {\n\treturn 'text';\n}",
},
{
name: 'numeric literal',
code: 'export function Foo() {\n\treturn 42;\n}',
errors: [{ message: RETURNS_TEXT, line: 2, column: 2 }],
},
{
name: 'template literal',
code: 'export function Foo() {\n\treturn `text ${x}`;\n}',
errors: [{ message: RETURNS_TEXT, line: 2, column: 2 }],
},
{
name: 'inside an if consequent',
code:
"export function Foo() {\n\tif (a) {\n\t\treturn 'x';\n\t}\n\treturn <div/>;\n}",
errors: [{ message: RETURNS_TEXT, line: 3, column: 3 }],
},
{
name: 'inside an else block',
code:
"export function Foo() {\n\tif (a) {\n\t\treturn <div/>;\n\t} else {\n\t\treturn 'x';\n\t}\n}",
errors: [{ message: RETURNS_TEXT, line: 5, column: 3 }],
},
{
name: 'inside an else-if chain',
code:
"export function Foo() {\n\tif (a) {\n\t\treturn <div/>;\n\t} else if (b) {\n\t\treturn 'x';\n\t}\n\treturn null;\n}",
errors: [{ message: RETURNS_TEXT, line: 5, column: 3 }],
},
{
name: 'inside a switch case',
code:
"export function Foo() {\n\tswitch (a) {\n\t\tcase 1:\n\t\t\treturn 'x';\n\t\tdefault:\n\t\t\treturn <div/>;\n\t}\n}",
errors: [{ message: RETURNS_TEXT, line: 4, column: 4 }],
},
{
name: 'inside try, catch and finally',
code:
"export function Foo() {\n\ttry {\n\t\treturn 'a';\n\t} catch {\n\t\treturn 'b';\n\t} finally {\n\t\treturn 'c';\n\t}\n}",
errors: [
{ message: RETURNS_TEXT, line: 3, column: 3 },
{ message: RETURNS_TEXT, line: 5, column: 3 },
{ message: RETURNS_TEXT, line: 7, column: 3 },
],
},
{
name: 'inside a for loop',
code:
"export function Foo() {\n\tfor (let i = 0; i < 3; i++) {\n\t\treturn 'x';\n\t}\n\treturn <div/>;\n}",
errors: [{ message: RETURNS_TEXT, line: 3, column: 3 }],
},
{
name: 'inside a for-of loop',
code:
"export function Foo() {\n\tfor (const i of list) {\n\t\treturn 'x';\n\t}\n\treturn <div/>;\n}",
errors: [{ message: RETURNS_TEXT, line: 3, column: 3 }],
},
{
name: 'inside a for-in loop',
code:
"export function Foo() {\n\tfor (const k in obj) {\n\t\treturn 'x';\n\t}\n\treturn <div/>;\n}",
errors: [{ message: RETURNS_TEXT, line: 3, column: 3 }],
},
{
name: 'inside a while loop',
code:
"export function Foo() {\n\twhile (a) {\n\t\treturn 'x';\n\t}\n\treturn <div/>;\n}",
errors: [{ message: RETURNS_TEXT, line: 3, column: 3 }],
},
{
name: 'inside a do-while loop',
code: "export function Foo() {\n\tdo {\n\t\treturn 'x';\n\t} while (a);\n}",
errors: [{ message: RETURNS_TEXT, line: 3, column: 3 }],
},
{
name: 'capitalised nested function is treated as a component',
code:
"export function Foo() {\n\tfunction Helper() {\n\t\treturn 'x';\n\t}\n\treturn <div/>;\n}",
errors: [{ message: RETURNS_TEXT, line: 3, column: 3 }],
},
// The rule listens only for `FunctionDeclaration` and reads the component
// name off `node.id`. Everything below returns a text node from something
// React renders as a component, and none of it is reported. This codebase
// writes components as arrow functions, which is why the rule currently
// finds nothing in `src`.
{
todo: 'arrow function components are never visited',
name: 'GAP: arrow component with an expression body',
code: "export const Foo = () => 'text';",
errors: 1,
},
{
todo: 'arrow function components are never visited',
name: 'GAP: arrow component with a block body',
code: "export const Foo = () => {\n\treturn 'text';\n};",
errors: [{ message: RETURNS_TEXT, line: 2, column: 2 }],
},
{
todo: 'anonymous declarations have no node.id to read a name from',
name: 'GAP: anonymous default-exported component',
code: "export default function () {\n\treturn 'text';\n}",
errors: [{ message: RETURNS_TEXT, line: 2, column: 2 }],
},
],
});

View File

@@ -1,257 +0,0 @@
/**
* Test harness for oxlint JS plugins.
*
* Rules are exercised through the real `oxlint` binary rather than a hand-rolled
* AST walker, so what the tests assert is exactly what CI enforces. oxlint's JS
* plugin AST is close to ESTree but not identical, and it exposes no type
* information, so any in-process fake would drift from the real runtime.
*
* All cases in a suite are written to one temp directory and linted in a single
* oxlint invocation, then mapped back by filename. Spawning per case costs ~80ms
* each; batching keeps a full suite under a second.
*/
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import {
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath, pathToFileURL } from 'node:url';
const FRONTEND_DIR = path.resolve(fileURLToPath(import.meta.url), '../../..');
const OXLINT_BIN = path.join(FRONTEND_DIR, 'node_modules/.bin/oxlint');
// oxlint enables its default categories unless every one is switched off, and a
// stray builtin diagnostic would be indistinguishable from the rule under test.
const CATEGORIES_OFF = {
correctness: 'off',
suspicious: 'off',
pedantic: 'off',
perf: 'off',
style: 'off',
restriction: 'off',
nursery: 'off',
};
function normaliseCase(entry, index) {
const testCase = typeof entry === 'string' ? { code: entry } : entry;
const extension = testCase.filename
? path.extname(testCase.filename).slice(1)
: 'tsx';
return {
...testCase,
index,
basename: `case-${String(index).padStart(3, '0')}.${extension}`,
};
}
function diagnosticFilename(diagnostic) {
const raw = diagnostic.filename ?? '';
const asPath = raw.startsWith('file://') ? fileURLToPath(raw) : raw;
return path.basename(asPath);
}
function toError(diagnostic) {
const span = diagnostic.labels?.[0]?.span;
return {
message: diagnostic.message,
line: span?.line,
column: span?.column,
};
}
function runOxlint(dir, configPath, extraArgs = []) {
const args = ['--config', configPath, '--format', 'json', ...extraArgs, '.'];
try {
return execFileSync(OXLINT_BIN, args, {
cwd: dir,
encoding: 'utf8',
// A rule that reports on every case can produce a lot of output.
maxBuffer: 64 * 1024 * 1024,
});
} catch (error) {
// oxlint exits non-zero whenever it reports a diagnostic, which is the
// expected outcome for every `invalid` case.
if (typeof error.stdout === 'string' && error.stdout.trim() !== '') {
return error.stdout;
}
throw new Error(`oxlint failed to run:\n${error.stderr || error.message}`, {
cause: error,
});
}
}
function writeSuite(dir, cases, { pluginPath, ruleId }) {
for (const testCase of cases) {
const target = path.join(dir, testCase.basename);
mkdirSync(path.dirname(target), { recursive: true });
writeFileSync(target, testCase.code);
}
const configPath = path.join(dir, '.oxlintrc.json');
writeFileSync(
configPath,
JSON.stringify({
jsPlugins: [pluginPath],
categories: CATEGORIES_OFF,
rules: { [ruleId]: 'error' },
}),
);
return configPath;
}
/**
* Lints every case in one pass, and applies suggestions in a second pass over an
* untouched copy when any case declares `output`.
*
* @returns {{errors: Map<string, object[]>, outputs: Map<string, string>}}
*/
function lintCases(cases, options) {
const root = mkdtempSync(path.join(tmpdir(), 'oxlint-rule-tester-'));
try {
const lintDir = path.join(root, 'lint');
mkdirSync(lintDir);
const report = JSON.parse(
runOxlint(lintDir, writeSuite(lintDir, cases, options)),
);
const errors = new Map(cases.map((testCase) => [testCase.basename, []]));
for (const diagnostic of report.diagnostics ?? []) {
const bucket = errors.get(diagnosticFilename(diagnostic));
// oxlint reports config-level problems without a filename; surfacing
// them as a suite failure beats silently testing nothing.
if (!bucket) {
throw new Error(`Unexpected diagnostic: ${diagnostic.message}`);
}
bucket.push(toError(diagnostic));
}
const outputs = new Map();
if (cases.some((testCase) => testCase.output !== undefined)) {
const fixDir = path.join(root, 'fix');
mkdirSync(fixDir);
runOxlint(fixDir, writeSuite(fixDir, cases, options), ['--fix-suggestions']);
for (const testCase of cases) {
outputs.set(
testCase.basename,
readFileSync(path.join(fixDir, testCase.basename), 'utf8'),
);
}
}
return { errors, outputs };
} finally {
rmSync(root, { recursive: true, force: true });
}
}
function assertMessage(actual, expected, label) {
if (expected instanceof RegExp) {
assert.match(actual, expected, label);
} else {
assert.ok(
actual.includes(expected),
`${label}\n expected message to contain: ${expected}\n actual: ${actual}`,
);
}
}
function assertErrors(actual, expected, code) {
const context = `\n--- code ---\n${code}\n--- reported ---\n${JSON.stringify(actual, null, 2)}`;
if (typeof expected === 'number') {
assert.equal(actual.length, expected, `error count${context}`);
return;
}
assert.equal(actual.length, expected.length, `error count${context}`);
expected.forEach((want, i) => {
const got = actual[i];
if (want.message !== undefined) {
assertMessage(got.message, want.message, `error[${i}] message${context}`);
}
if (want.line !== undefined) {
assert.equal(got.line, want.line, `error[${i}] line${context}`);
}
if (want.column !== undefined) {
assert.equal(got.column, want.column, `error[${i}] column${context}`);
}
});
}
/**
* Declares a suite for one rule.
*
* A case carrying `todo` asserts the behaviour the rule *should* have. It still
* runs, but a failure is reported as a todo instead of failing the suite, so a
* known bug can be pinned as an executable spec. Delete the flag once the rule
* is fixed and the case starts guarding the fix.
*
* An invalid case carrying `output` also asserts the source after
* `--fix-suggestions` has been applied.
*
* @param {object} options
* @param {string} options.rule - rule name as exported by the plugin
* @param {string} [options.plugin] - path to the plugin, relative to `frontend/`
* @param {Array<string | {code: string, name?: string, filename?: string, todo?: string}>} options.valid
* @param {Array<{code: string, name?: string, filename?: string, todo?: string, output?: string, errors: number | Array<{message?: string | RegExp, line?: number, column?: number}>}>} options.invalid
*/
export async function ruleTester({
rule,
plugin = 'plugins/signoz.mjs',
valid = [],
invalid = [],
}) {
const pluginPath = path.join(FRONTEND_DIR, plugin);
const { default: pluginModule } = await import(pathToFileURL(pluginPath));
assert.ok(
pluginModule.rules?.[rule],
`plugin ${plugin} does not export a rule named "${rule}"`,
);
const ruleId = `${pluginModule.meta.name}/${rule}`;
const validCases = valid.map(normaliseCase);
const invalidCases = invalid.map((entry, i) =>
normaliseCase(entry, valid.length + i),
);
const { errors, outputs } = lintCases([...validCases, ...invalidCases], {
pluginPath,
ruleId,
});
const declare = (t, testCase, expected) => {
const label = testCase.name ?? testCase.code.trim().split('\n')[0];
return t.test(label, { todo: testCase.todo }, () => {
assertErrors(errors.get(testCase.basename), expected, testCase.code);
if (testCase.output !== undefined) {
assert.equal(
outputs.get(testCase.basename),
testCase.output,
`suggestion output\n--- code ---\n${testCase.code}`,
);
}
});
};
test(ruleId, async (t) => {
await t.test('valid', async (t) => {
for (const testCase of validCases) {
await declare(t, testCase, 0);
}
});
await t.test('invalid', async (t) => {
for (const testCase of invalidCases) {
await declare(t, testCase, testCase.errors);
}
});
});
}

View File

@@ -1,313 +0,0 @@
/**
* Rule: no-conditional-text-nodes-with-siblings
*
* Conditionally rendered text nodes with siblings should be wrapped in an
* element (for example a `<span>`), otherwise Google Translate causes a browser
* error. Translate replaces the text node with a `<font>` wrapper, React still
* holds a reference to the original node, and the next render throws on
* `removeChild`.
*
* Adapted from https://github.com/getcouped/eslint-plugin-react-google-translate
* (v1.0.4). The upstream rule resolves branch types through
* `@typescript-eslint/utils`; oxlint's JS plugin runtime exposes no type
* information, so those paths are dropped and call expressions are matched
* against the allowlist below instead.
*/
// Calls known to render as text. Without types this is the only way to
// recognise a string-returning call; widen it to catch more helpers.
const TEXT_RETURNING_CALLEES = new Set(['formatMessage', 't']);
const STRINGIFY_METHODS = new Set(['toString', 'toLocaleString']);
function calleeName(node) {
return node.type === 'Identifier' ? node.name : null;
}
function isTextReturningCall(node) {
const { callee } = node;
if (TEXT_RETURNING_CALLEES.has(calleeName(callee))) {
return node.arguments.length > 0;
}
if (callee.type === 'MemberExpression' && !callee.computed) {
return STRINGIFY_METHODS.has(calleeName(callee.property));
}
return STRINGIFY_METHODS.has(calleeName(callee));
}
/**
* True when the node renders no visible text. An empty or whitespace-only value
* produces no DOM text node, so Google Translate has nothing to wrap and React
* nothing to lose.
*/
function isBlankText(node) {
if (node.type === 'Literal' || node.type === 'JSXText') {
return typeof node.value === 'string' && node.value.trim() === '';
}
if (node.type === 'TemplateLiteral') {
return (
node.expressions.length === 0 &&
node.quasis.every((quasi) => (quasi.value.cooked ?? '').trim() === '')
);
}
return false;
}
function isConditionallyRendered(node) {
const parent = node.parent;
return (
parent?.type === 'ConditionalExpression' ||
parent?.type === 'LogicalExpression'
);
}
function isRenderedConditional(node) {
return (
node.type === 'JSXExpressionContainer' &&
(node.expression?.type === 'ConditionalExpression' ||
node.expression?.type === 'LogicalExpression')
);
}
/** Children that produce output, i.e. everything but formatting whitespace. */
function renderedChildren(node) {
const children = node?.children;
if (!children) {
return null;
}
return children.filter((child) => !isBlankText(child));
}
/** True when `node` is a JSX child rendered alongside at least one other child. */
function hasSiblings(node) {
if (!(node?.parent?.children?.length > 1)) {
return false;
}
return renderedChildren(node.parent).some((child) => child !== node);
}
function isPrecededByConditional(node) {
const children = renderedChildren(node?.parent);
if (!children) {
return false;
}
return children.some(
(child) => child.start < node.start && isRenderedConditional(child),
);
}
/** Walk out of nested conditionals so nested branches report against the outer container. */
function getOutermostConditional(node) {
let current = node;
while (isConditionallyRendered(current)) {
current = current.parent;
}
return current;
}
/** True when `node` is a conditional branch rendered directly beside other JSX children. */
function isProblematicConditional(node) {
if (!isConditionallyRendered(node)) {
return false;
}
const container = getOutermostConditional(node);
return (
container.parent?.type === 'JSXExpressionContainer' &&
container.parent.parent?.type === 'JSXElement' &&
hasSiblings(container.parent)
);
}
/** True when `node` renders after a sibling conditional, i.e. the DOM order Translate breaks. */
function followsConditionalSibling(node) {
return (
node.parent?.parent?.type === 'JSXElement' &&
hasSiblings(node.parent) &&
isPrecededByConditional(node.parent)
);
}
/**
* `A && B` and the test of a ternary are conditions, not rendered output.
*/
function isCondition(node) {
let current = node;
while (current.parent?.type === 'LogicalExpression') {
if (current.parent.left === current) {
return true;
}
current = current.parent;
}
if (current.parent?.type === 'ConditionalExpression') {
return current.parent.test === current;
}
return false;
}
function isConditionOperand(node) {
if (node.parent?.type === 'BinaryExpression') {
return isCondition(node.parent);
}
return isCondition(node);
}
// A string may only be inlined as JSX text when it needs no escaping and no
// whitespace of its own: JSX collapses leading and trailing whitespace, and
// these characters would either close the element or start an entity.
const NEEDS_BRACES = /['"{}<>&\r\n]/;
// `display: contents`, declared in src/styles.scss. React owns the element so
// Translate's `<font>` swap is absorbed, while the box tree stays as it was and
// a flex or grid parent still sees one contiguous text run.
const OPEN = '<span className="translate-safe">';
const CLOSE = '</span>';
/** Wraps the reported expression so React owns an element Translate cannot replace. */
function wrapExpression(fixer, sourceCode, node) {
// A call reported on its own already sits in a container. Replacing the
// container yields `<span …>{expr}</span>` rather than `{<span …>{expr}</span>}`.
const target =
node.parent?.type === 'JSXExpressionContainer' ? node.parent : node;
if (
node.type === 'Literal' &&
typeof node.value === 'string' &&
!NEEDS_BRACES.test(node.value) &&
node.value.trim() === node.value
) {
return fixer.replaceText(target, `${OPEN}${node.value}${CLOSE}`);
}
return fixer.replaceText(
target,
`${OPEN}{${sourceCode.getText(node)}}${CLOSE}`,
);
}
/**
* Wraps static JSX text. Only the visible run is wrapped: the node also spans
* the formatting whitespace around it, which has to stay outside the element.
*/
function wrapJsxText(fixer, node) {
const raw = node.value;
const leading = raw.length - raw.trimStart().length;
const trailing = raw.length - raw.trimEnd().length;
return fixer.replaceTextRange(
[node.start + leading, node.end - trailing],
`${OPEN}${raw.trim()}${CLOSE}`,
);
}
export default {
meta: {
type: 'problem',
docs: {
description:
'Conditionally rendered text nodes should be wrapped in an element (for example a `<span>`), otherwise Google Translate can cause a browser error.',
url: 'https://github.com/getcouped/eslint-plugin-react-google-translate#eslint-plugin-react-google-translate',
},
schema: [],
// Wrapping adds a DOM element, which can turn into a flex/grid item or
// break `> *` and `:nth-child` selectors, so it is offered as a suggestion
// (`--fix-suggestions`) rather than applied by a bare `--fix`.
hasSuggestions: true,
messages: {
'conditional-text-node':
'Conditionally rendered text nodes with siblings, rendered as a direct child of a JSX element, must be wrapped so Google Translate cannot break React\'s DOM: `<span className="translate-safe">{value}</span>`. Translate replaces the bare text node with a `<font>` element and React then throws on `removeChild`. This also applies to values returned from functions, so `getString()` becomes `<span className="translate-safe">{getString()}</span>`.',
'text-node-preceded-by-conditional':
'Text nodes which are preceded by a conditional expression, rendered as a direct child of a JSX element, must be wrapped so Google Translate cannot break React\'s DOM: `<span className="translate-safe">text</span>`. Translate replaces the bare text node with a `<font>` element and React then throws on `removeChild`.',
},
},
createOnce(context) {
const suggestWrap = (build) => [{ desc: 'Wrap in a <span>', fix: build }];
const wrap = (fixer, node) => wrapExpression(fixer, context.sourceCode, node);
const reportConditional = (node) => {
context.report({
node,
messageId: 'conditional-text-node',
suggest: suggestWrap((fixer) => wrap(fixer, node)),
});
};
const reportPreceded = (node, build) => {
context.report({
node,
messageId: 'text-node-preceded-by-conditional',
suggest: suggestWrap(build),
});
};
return {
// String and numeric branches: `{flag ? 'yes' : 'no'}`
Literal(node) {
if (node.value === null || typeof node.value === 'boolean') {
return;
}
if (isBlankText(node)) {
return;
}
if (isProblematicConditional(node)) {
reportConditional(node);
}
},
TemplateLiteral(node) {
if (isBlankText(node)) {
return;
}
if (isProblematicConditional(node)) {
reportConditional(node);
}
},
// Static text rendered after a conditional: `{flag && <b/>}trailing`
JSXText(node) {
if (isBlankText(node)) {
return;
}
if (hasSiblings(node) && isPrecededByConditional(node)) {
reportPreceded(node, (fixer) => wrapJsxText(fixer, node));
}
},
CallExpression(node) {
if (isCondition(node) || !isTextReturningCall(node)) {
return;
}
if (isProblematicConditional(node)) {
reportConditional(node);
}
if (followsConditionalSibling(node)) {
reportPreceded(node, (fixer) => wrap(fixer, node));
}
},
// Values read off an object: `{flag ? user.name : 'anonymous'}`
MemberExpression(node) {
if (isConditionOperand(node)) {
return;
}
if (isProblematicConditional(node)) {
reportConditional(node);
}
},
// Optional chaining wraps the member expression: `{flag ? a?.b?.c : 'x'}`
ChainExpression(node) {
if (isConditionOperand(node)) {
return;
}
if (isProblematicConditional(node)) {
reportConditional(node);
}
},
};
},
};

View File

@@ -1,115 +0,0 @@
/**
* Rule: no-return-text-nodes
*
* React components should not return a bare text node. Google Translate keeps
* displaying the stale translated text after a state change and nothing throws,
* which makes the bug very hard to track down. Numbers count too: JSX renders
* them as text.
*
* Adapted from https://github.com/getcouped/eslint-plugin-react-google-translate
* (v1.0.4). Upstream walks the function body statement by statement; this
* version visits `ReturnStatement` directly and walks up to the enclosing
* function, which covers the same constructs without enumerating them.
*/
const FUNCTION_TYPES = new Set([
'FunctionDeclaration',
'FunctionExpression',
'ArrowFunctionExpression',
]);
function isTextNode(node) {
if (!node) {
return false;
}
if (node.type === 'TemplateLiteral') {
return true;
}
return (
node.type === 'Literal' &&
(typeof node.value === 'string' || typeof node.value === 'number')
);
}
function getEnclosingFunction(node) {
let current = node.parent;
while (current) {
if (FUNCTION_TYPES.has(current.type)) {
return current;
}
current = current.parent;
}
return null;
}
function isComponentName(name) {
return (
typeof name === 'string' && name !== '' && name[0] === name[0].toUpperCase()
);
}
// The suggestion introduces JSX, which only parses in a JSX-enabled file.
function allowsJsx(filename) {
return filename.endsWith('.tsx') || filename.endsWith('.jsx');
}
export default {
meta: {
type: 'problem',
docs: {
description:
'React components should avoid returning text nodes directly (or numerical values which will be rendered as text). When a React component returns values other than JSX / null, Google Translate can continue to display stale values after state changes, without any error being thrown. Since this is very hard to debug it is better to avoid it altogether.',
url: 'https://github.com/getcouped/eslint-plugin-react-google-translate#eslint-plugin-react-google-translate',
},
schema: [],
// Wrapping changes what the component renders, so it is offered as a
// suggestion (`--fix-suggestions`) rather than applied by a bare `--fix`.
hasSuggestions: true,
messages: {
'return-value-is-text-node':
'React components should avoid returning text nodes directly (or numerical values which will be rendered as text). When a React component returns values other than JSX / null, Google Translate can continue to display stale values after state changes, without any error being thrown. Since this is very hard to debug it is better to avoid it altogether.',
},
},
createOnce(context) {
const buildSuggestion = (node) => {
if (!allowsJsx(context.filename)) {
return undefined;
}
return [
{
desc: 'Wrap in a <span>',
fix: (fixer) =>
fixer.replaceText(
node.argument,
`<span className="translate-safe">{${context.sourceCode.getText(node.argument)}}</span>`,
),
},
];
};
return {
ReturnStatement(node) {
if (!isTextNode(node.argument)) {
return;
}
// Only named function declarations are recognised as components, so a
// text return from a nested helper or a class method is left alone.
const fn = getEnclosingFunction(node);
if (fn?.type !== 'FunctionDeclaration') {
return;
}
if (!isComponentName(fn.id?.name)) {
return;
}
context.report({
node,
messageId: 'return-value-is-text-node',
suggest: buildSuggestion(node),
});
},
};
},
};

View File

@@ -13,8 +13,6 @@ import noAntdComponents from './rules/no-antd-components.mjs';
import noSignozhqUiBarrel from './rules/no-signozhq-ui-barrel.mjs';
import noCssModuleBracketAccess from './rules/no-css-module-bracket-access.mjs';
import noDashboardFetchOutsideRoot from './rules/no-dashboard-fetch-outside-root.mjs';
import noConditionalTextNodesWithSiblings from './rules/no-conditional-text-nodes-with-siblings.mjs';
import noReturnTextNodes from './rules/no-return-text-nodes.mjs';
export default {
meta: {
@@ -29,7 +27,5 @@ export default {
'no-signozhq-ui-barrel': noSignozhqUiBarrel,
'no-css-module-bracket-access': noCssModuleBracketAccess,
'no-dashboard-fetch-outside-root': noDashboardFetchOutsideRoot,
'no-conditional-text-nodes-with-siblings': noConditionalTextNodesWithSiblings,
'no-return-text-nodes': noReturnTextNodes,
},
};

View File

@@ -294,9 +294,6 @@ importers:
timestamp-nano:
specifier: ^1.0.0
version: 1.0.1
translation-resilience:
specifier: ^0.2.0
version: 0.2.0
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -8478,9 +8475,6 @@ packages:
resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==}
engines: {node: '>=12'}
translation-resilience@0.2.0:
resolution: {integrity: sha512-IxTjhpHGp1SJxVEEPBu/YbBaHnymMRJdYxUY1i5qtYADuz9b8fdWhZSE/EeRS2aVIiuQjRqtYDk69ruS/3fzTg==}
trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
@@ -18238,8 +18232,6 @@ snapshots:
dependencies:
punycode: 2.3.1
translation-resilience@0.2.0: {}
trim-lines@3.0.1: {}
trough@2.1.0: {}

View File

@@ -24,8 +24,6 @@
"tooltip_opsgenie_api_key": "Learn how to obtain the API key from your OpsGenie account [here](https://support.atlassian.com/opsgenie/docs/integrate-opsgenie-with-prometheus/).",
"tooltip_email_to": "Enter email addresses separated by commas.",
"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_slack_recipient": "Recipient",
"field_slack_title": "Title",

View File

@@ -24,8 +24,6 @@
"tooltip_opsgenie_api_key": "Learn how to obtain the API key from your OpsGenie account [here](https://support.atlassian.com/opsgenie/docs/integrate-opsgenie-with-prometheus/).",
"tooltip_email_to": "Enter email addresses separated by commas.",
"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_slack_recipient": "Recipient",
"field_slack_title": "Title",
"field_slack_description": "Description",

View File

@@ -354,16 +354,6 @@ function App(): JSX.Element {
tunnel: window.signozBootData.settings.sentry.tunnel,
environment: process.env.ENVIRONMENT,
release: process.env.VERSION,
// A tab that outlived a deploy requests hashed assets the new build no longer
// has. `lazyRetry` recovers by reloading once, so this class is not worth
// reporting. The stylesheet message is Vite's own; the module ones are the
// same failure worded differently by Chromium, Firefox and Safari.
ignoreErrors: [
/Unable to preload CSS for/,
/Failed to fetch dynamically imported module/,
/error loading dynamically imported module/,
/Importing a module script failed/,
],
integrations: [
// Kept for the `transaction` tag used in routing, even though
// tracing is disabled. Ref: https://github.com/SigNoz/platform-pod/issues/2393#issuecomment-4603658055
@@ -376,23 +366,7 @@ function App(): JSX.Element {
tracesSampleRate: 0, // Ref: https://github.com/SigNoz/platform-pod/issues/2393#issuecomment-4603658055
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
beforeSend(event, hint) {
const error = hint?.originalException as
| { name?: string; code?: string | number }
| undefined;
// Ignore benign aborted/cancelled requests (axios + fetch).
if (error?.code === 'ERR_CANCELED' || error?.code === 'ECONNABORTED') {
return null;
}
if (error?.name === 'AbortError') {
return null;
}
// Ignore benign Monaco cancellation errors (name 'Canceled').
if (error?.name === 'Canceled') {
return null;
}
beforeSend(event) {
// Drop the event if its level is 'warning' or 'info'
if (event.level === 'warning' || event.level === 'info') {
return null;

View File

@@ -527,13 +527,6 @@ const routes: AppRoutes[] = [
key: 'AI_OBSERVABILITY_OVERVIEW',
isPrivate: true,
},
{
path: ROUTES.AI_OBSERVABILITY_EXPLORER,
exact: true,
component: LLMObservabilityPage,
key: 'AI_OBSERVABILITY_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.AI_OBSERVABILITY_CONFIGURATION,
exact: true,

View File

@@ -1,490 +0,0 @@
/**
* ! 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 {
CreateSavedView201,
DeleteSavedViewPathParameters,
GetSavedView200,
GetSavedViewPathParameters,
ListSavedViews200,
ListSavedViewsParams,
RenderErrorResponseDTO,
SavedviewtypesPostableSavedViewDTO,
SavedviewtypesUpdatableSavedViewDTO,
UpdateSavedViewPathParameters,
} from '../sigNoz.schemas';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* Returns saved views, optionally filtered by source and name.
* @summary List saved views
*/
export const listSavedViews = (
params?: ListSavedViewsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListSavedViews200>({
url: `/api/v2/saved_views`,
method: 'GET',
params,
signal,
});
};
export const getListSavedViewsQueryKey = (params?: ListSavedViewsParams) => {
return [`/api/v2/saved_views`, ...(params ? [params] : [])] as const;
};
export const getListSavedViewsQueryOptions = <
TData = Awaited<ReturnType<typeof listSavedViews>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params?: ListSavedViewsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listSavedViews>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListSavedViewsQueryKey(params);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listSavedViews>>> = ({
signal,
}) => listSavedViews(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listSavedViews>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListSavedViewsQueryResult = NonNullable<
Awaited<ReturnType<typeof listSavedViews>>
>;
export type ListSavedViewsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List saved views
*/
export function useListSavedViews<
TData = Awaited<ReturnType<typeof listSavedViews>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params?: ListSavedViewsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listSavedViews>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListSavedViewsQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List saved views
*/
export const invalidateListSavedViews = async (
queryClient: QueryClient,
params?: ListSavedViewsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListSavedViewsQueryKey(params) },
options,
);
return queryClient;
};
/**
* Persists a saved view for the explore page. Returns the id of the created view.
* @summary Create saved view
*/
export const createSavedView = (
savedviewtypesPostableSavedViewDTO?: BodyType<SavedviewtypesPostableSavedViewDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateSavedView201>({
url: `/api/v2/saved_views`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: savedviewtypesPostableSavedViewDTO,
signal,
});
};
export const getCreateSavedViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createSavedView>>,
TError,
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createSavedView>>,
TError,
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> },
TContext
> => {
const mutationKey = ['createSavedView'];
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 createSavedView>>,
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> }
> = (props) => {
const { data } = props ?? {};
return createSavedView(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateSavedViewMutationResult = NonNullable<
Awaited<ReturnType<typeof createSavedView>>
>;
export type CreateSavedViewMutationBody =
| BodyType<SavedviewtypesPostableSavedViewDTO>
| undefined;
export type CreateSavedViewMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create saved view
*/
export const useCreateSavedView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createSavedView>>,
TError,
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createSavedView>>,
TError,
{ data?: BodyType<SavedviewtypesPostableSavedViewDTO> },
TContext
> => {
return useMutation(getCreateSavedViewMutationOptions(options));
};
/**
* Deletes a saved view by id.
* @summary Delete saved view
*/
export const deleteSavedView = (
{ id }: DeleteSavedViewPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/saved_views/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteSavedViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteSavedView>>,
TError,
{ pathParams: DeleteSavedViewPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteSavedView>>,
TError,
{ pathParams: DeleteSavedViewPathParameters },
TContext
> => {
const mutationKey = ['deleteSavedView'];
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 deleteSavedView>>,
{ pathParams: DeleteSavedViewPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteSavedView(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteSavedViewMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteSavedView>>
>;
export type DeleteSavedViewMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete saved view
*/
export const useDeleteSavedView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteSavedView>>,
TError,
{ pathParams: DeleteSavedViewPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteSavedView>>,
TError,
{ pathParams: DeleteSavedViewPathParameters },
TContext
> => {
return useMutation(getDeleteSavedViewMutationOptions(options));
};
/**
* Returns a saved view by id.
* @summary Get saved view
*/
export const getSavedView = (
{ id }: GetSavedViewPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetSavedView200>({
url: `/api/v2/saved_views/${id}`,
method: 'GET',
signal,
});
};
export const getGetSavedViewQueryKey = ({ id }: GetSavedViewPathParameters) => {
return [`/api/v2/saved_views/${id}`] as const;
};
export const getGetSavedViewQueryOptions = <
TData = Awaited<ReturnType<typeof getSavedView>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetSavedViewPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getSavedView>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetSavedViewQueryKey({ id });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getSavedView>>> = ({
signal,
}) => getSavedView({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getSavedView>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetSavedViewQueryResult = NonNullable<
Awaited<ReturnType<typeof getSavedView>>
>;
export type GetSavedViewQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get saved view
*/
export function useGetSavedView<
TData = Awaited<ReturnType<typeof getSavedView>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetSavedViewPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getSavedView>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetSavedViewQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get saved view
*/
export const invalidateGetSavedView = async (
queryClient: QueryClient,
{ id }: GetSavedViewPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetSavedViewQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* Replaces a saved view's name and query.
* @summary Update saved view
*/
export const updateSavedView = (
{ id }: UpdateSavedViewPathParameters,
savedviewtypesUpdatableSavedViewDTO?: BodyType<SavedviewtypesUpdatableSavedViewDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/saved_views/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: savedviewtypesUpdatableSavedViewDTO,
signal,
});
};
export const getUpdateSavedViewMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateSavedView>>,
TError,
{
pathParams: UpdateSavedViewPathParameters;
data?: BodyType<SavedviewtypesUpdatableSavedViewDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateSavedView>>,
TError,
{
pathParams: UpdateSavedViewPathParameters;
data?: BodyType<SavedviewtypesUpdatableSavedViewDTO>;
},
TContext
> => {
const mutationKey = ['updateSavedView'];
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 updateSavedView>>,
{
pathParams: UpdateSavedViewPathParameters;
data?: BodyType<SavedviewtypesUpdatableSavedViewDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateSavedView(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateSavedViewMutationResult = NonNullable<
Awaited<ReturnType<typeof updateSavedView>>
>;
export type UpdateSavedViewMutationBody =
| BodyType<SavedviewtypesUpdatableSavedViewDTO>
| undefined;
export type UpdateSavedViewMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update saved view
*/
export const useUpdateSavedView = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateSavedView>>,
TError,
{
pathParams: UpdateSavedViewPathParameters;
data?: BodyType<SavedviewtypesUpdatableSavedViewDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateSavedView>>,
TError,
{
pathParams: UpdateSavedViewPathParameters;
data?: BodyType<SavedviewtypesUpdatableSavedViewDTO>;
},
TContext
> => {
return useMutation(getUpdateSavedViewMutationOptions(options));
};

View File

@@ -2818,7 +2818,6 @@ export enum CloudintegrationtypesServiceIDDTO {
computeengine = 'computeengine',
gke = 'gke',
cloudstorage = 'cloudstorage',
cloudsql_mysql = 'cloudsql_mysql',
}
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
/**
@@ -5649,47 +5648,6 @@ export interface InframonitoringtypesChecksDTO {
type: InframonitoringtypesCheckTypeDTO;
}
export enum InframonitoringtypesNodeConditionDTO {
ready = 'ready',
not_ready = 'not_ready',
no_data = 'no_data',
}
export enum InframonitoringtypesPodStatusDTO {
pending = 'pending',
running = 'running',
failed = 'failed',
unknown = 'unknown',
crashloopbackoff = 'crashloopbackoff',
imagepullbackoff = 'imagepullbackoff',
errimagepull = 'errimagepull',
createcontainerconfigerror = 'createcontainerconfigerror',
containercreating = 'containercreating',
oomkilled = 'oomkilled',
completed = 'completed',
error = 'error',
containercannotrun = 'containercannotrun',
evicted = 'evicted',
nodeaffinity = 'nodeaffinity',
nodelost = 'nodelost',
shutdown = 'shutdown',
unexpectedadmissionerror = 'unexpectedadmissionerror',
no_data = 'no_data',
}
export interface InframonitoringtypesClusterFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByNodeReadiness?: InframonitoringtypesNodeConditionDTO[] | null;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export type InframonitoringtypesClusterRecordDTOCounts = {
/**
* @type integer
@@ -5965,6 +5923,21 @@ export interface InframonitoringtypesContainerCountsByStatusDTO {
waiting: number;
}
export enum InframonitoringtypesContainerReadyDTO {
ready = 'ready',
not_ready = 'not_ready',
no_data = 'no_data',
}
export type InframonitoringtypesContainerRecordDTOMetaAnyOf = {
[key: string]: string;
};
/**
* @nullable
*/
export type InframonitoringtypesContainerRecordDTOMeta =
InframonitoringtypesContainerRecordDTOMetaAnyOf | null;
export enum InframonitoringtypesContainerStatusDTO {
running = 'running',
waiting = 'waiting',
@@ -5981,32 +5954,6 @@ export enum InframonitoringtypesContainerStatusDTO {
unknown = 'unknown',
no_data = 'no_data',
}
export interface InframonitoringtypesContainerFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByContainerStatus?: InframonitoringtypesContainerStatusDTO[] | null;
}
export enum InframonitoringtypesContainerReadyDTO {
ready = 'ready',
not_ready = 'not_ready',
no_data = 'no_data',
}
export type InframonitoringtypesContainerRecordDTOMetaAnyOf = {
[key: string]: string;
};
/**
* @nullable
*/
export type InframonitoringtypesContainerRecordDTOMeta =
InframonitoringtypesContainerRecordDTOMetaAnyOf | null;
export interface InframonitoringtypesContainerRecordDTO {
containerCountsByReady: InframonitoringtypesContainerCountsByReadyDTO;
containerCountsByStatus: InframonitoringtypesContainerCountsByStatusDTO;
@@ -6078,17 +6025,6 @@ export interface InframonitoringtypesContainersDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesDaemonSetFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export type InframonitoringtypesDaemonSetRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6174,17 +6110,6 @@ export interface InframonitoringtypesDaemonSetsDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesDeploymentFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export type InframonitoringtypesDeploymentRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6347,17 +6272,6 @@ export interface InframonitoringtypesHostsDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesJobFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export type InframonitoringtypesJobRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6443,17 +6357,6 @@ export interface InframonitoringtypesJobsDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesNamespaceFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export type InframonitoringtypesNamespaceRecordDTOCounts = {
/**
* @type integer
@@ -6530,21 +6433,11 @@ export interface InframonitoringtypesNamespacesDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesNodeFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByNodeReadiness?: InframonitoringtypesNodeConditionDTO[] | null;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
export enum InframonitoringtypesNodeConditionDTO {
ready = 'ready',
not_ready = 'not_ready',
no_data = 'no_data',
}
export type InframonitoringtypesNodeRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6606,17 +6499,6 @@ export interface InframonitoringtypesNodesDTO {
warning?: Querybuildertypesv5QueryWarnDataDTO;
}
export interface InframonitoringtypesPodFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
[key: string]: string;
};
@@ -6627,6 +6509,27 @@ export type InframonitoringtypesPodRecordDTOMetaAnyOf = {
export type InframonitoringtypesPodRecordDTOMeta =
InframonitoringtypesPodRecordDTOMetaAnyOf | null;
export enum InframonitoringtypesPodStatusDTO {
pending = 'pending',
running = 'running',
failed = 'failed',
unknown = 'unknown',
crashloopbackoff = 'crashloopbackoff',
imagepullbackoff = 'imagepullbackoff',
errimagepull = 'errimagepull',
createcontainerconfigerror = 'createcontainerconfigerror',
containercreating = 'containercreating',
oomkilled = 'oomkilled',
completed = 'completed',
error = 'error',
containercannotrun = 'containercannotrun',
evicted = 'evicted',
nodeaffinity = 'nodeaffinity',
nodelost = 'nodelost',
shutdown = 'shutdown',
unexpectedadmissionerror = 'unexpectedadmissionerror',
no_data = 'no_data',
}
export interface InframonitoringtypesPodRecordDTO {
/**
* @type object,null
@@ -6703,7 +6606,7 @@ export interface InframonitoringtypesPostableClustersDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesClusterFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6730,7 +6633,7 @@ export interface InframonitoringtypesPostableContainersDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesContainerFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6757,7 +6660,7 @@ export interface InframonitoringtypesPostableDaemonSetsDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesDaemonSetFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6784,7 +6687,7 @@ export interface InframonitoringtypesPostableDeploymentsDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesDeploymentFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6838,7 +6741,7 @@ export interface InframonitoringtypesPostableJobsDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesJobFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6865,7 +6768,7 @@ export interface InframonitoringtypesPostableNamespacesDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesNamespaceFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6892,7 +6795,7 @@ export interface InframonitoringtypesPostableNodesDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesNodeFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6919,7 +6822,7 @@ export interface InframonitoringtypesPostablePodsDTO {
* @format int64
*/
end: number;
filter?: InframonitoringtypesPodFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -6940,24 +6843,13 @@ export interface InframonitoringtypesPostablePodsDTO {
start: number;
}
export interface InframonitoringtypesStatefulSetFilterDTO {
/**
* @type string
*/
expression?: string;
/**
* @type array,null
*/
filterByPodStatus?: InframonitoringtypesPodStatusDTO[] | null;
}
export interface InframonitoringtypesPostableStatefulSetsDTO {
/**
* @type integer
* @format int64
*/
end: number;
filter?: InframonitoringtypesStatefulSetFilterDTO;
filter?: Querybuildertypesv5FilterDTO;
/**
* @type array,null
*/
@@ -8966,111 +8858,6 @@ export interface RuletypesRuleDTO {
export enum RuletypesThresholdKindDTO {
basic = 'basic',
}
export interface SavedviewtypesDisplayDTO {
/**
* @type string
*/
color?: string;
/**
* @type string
*/
fontSize?: string;
/**
* @type string
*/
format?: string;
/**
* @type integer
*/
maxLines?: number;
}
export enum SavedviewtypesPanelTypeDTO {
value = 'value',
graph = 'graph',
table = 'table',
list = 'list',
trace = 'trace',
}
export enum SavedviewtypesSchemaVersionDTO {
v2 = 'v2',
}
export enum SavedviewtypesSourceDTO {
traces = 'traces',
logs = 'logs',
metrics = 'metrics',
meter = 'meter',
}
export interface SavedviewtypesSavedViewSpecDTO {
display?: SavedviewtypesDisplayDTO;
/**
* @type string
*/
displayName: string;
panelType: SavedviewtypesPanelTypeDTO;
/**
* @type array
*/
queries: Querybuildertypesv5QueryEnvelopeDTO[];
requestType: Querybuildertypesv5RequestTypeDTO;
/**
* @type array
*/
selectedFields?: TelemetrytypesTelemetryFieldKeyDTO[];
}
export interface SavedviewtypesPostableSavedViewDTO {
/**
* @type boolean
*/
generateName?: boolean;
/**
* @type string
*/
name?: string;
schemaVersion: SavedviewtypesSchemaVersionDTO;
source: SavedviewtypesSourceDTO;
spec: SavedviewtypesSavedViewSpecDTO;
}
export interface SavedviewtypesSavedViewDTO {
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name?: string;
schemaVersion: SavedviewtypesSchemaVersionDTO;
source?: SavedviewtypesSourceDTO;
spec: SavedviewtypesSavedViewSpecDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface SavedviewtypesUpdatableSavedViewDTO {
schemaVersion: SavedviewtypesSchemaVersionDTO;
source: SavedviewtypesSourceDTO;
spec: SavedviewtypesSavedViewSpecDTO;
}
export interface ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO {
/**
* @type string
@@ -9923,6 +9710,10 @@ export interface TelemetrytypesGettableFieldValuesDTO {
values: TelemetrytypesTelemetryFieldValuesDTO;
}
export enum TelemetrytypesQueryTypeDTO {
'' = '',
builder_ai_query = 'builder_ai_query',
}
export interface TypesChangePasswordRequestDTO {
/**
* @type string
@@ -10057,21 +9848,6 @@ export interface TypesOrganizationDTO {
updatedAt?: string;
}
export interface TypesPostableForgotPasswordDTO {
/**
* @type string
*/
email: string;
/**
* @type string
*/
frontendBaseURL?: string;
/**
* @type string
*/
orgId: string;
}
export interface TypesPostableInviteDTO {
/**
* @type string
@@ -10091,6 +9867,28 @@ export interface TypesPostableInviteDTO {
role?: string;
}
export interface TypesPostableBulkInviteRequestDTO {
/**
* @type array
*/
invites: TypesPostableInviteDTO[];
}
export interface TypesPostableForgotPasswordDTO {
/**
* @type string
*/
email: string;
/**
* @type string
*/
frontendBaseURL?: string;
/**
* @type string
*/
orgId: string;
}
export interface TypesPostableResetPasswordDTO {
/**
* @type string
@@ -10632,6 +10430,10 @@ export type GetFieldsKeysParams = {
* @description undefined
*/
source?: TelemetrytypesSourceDTO;
/**
* @description undefined
*/
type?: TelemetrytypesQueryTypeDTO;
/**
* @type integer
* @description undefined
@@ -10691,6 +10493,10 @@ export type GetFieldsValuesParams = {
* @description undefined
*/
source?: TelemetrytypesSourceDTO;
/**
* @description undefined
*/
type?: TelemetrytypesQueryTypeDTO;
/**
* @type integer
* @description undefined
@@ -11201,6 +11007,31 @@ export type ListUsersDeprecated200 = {
status: string;
};
export type DeleteUserDeprecatedPathParameters = {
id: string;
};
export type GetUserDeprecatedPathParameters = {
id: string;
};
export type GetUserDeprecated200 = {
data: TypesDeprecatedUserDTO;
/**
* @type string
*/
status: string;
};
export type UpdateUserDeprecatedPathParameters = {
id: string;
};
export type UpdateUserDeprecated200 = {
data: TypesDeprecatedUserDTO;
/**
* @type string
*/
status: string;
};
export type GetMyUserDeprecated200 = {
data: TypesDeprecatedUserDTO;
/**
@@ -11973,6 +11804,10 @@ export type GetRuleHistoryFilterKeysParams = {
* @description undefined
*/
source?: TelemetrytypesSourceDTO;
/**
* @description undefined
*/
type?: TelemetrytypesQueryTypeDTO;
/**
* @type integer
* @description undefined
@@ -12035,6 +11870,10 @@ export type GetRuleHistoryFilterValuesParams = {
* @description undefined
*/
source?: TelemetrytypesSourceDTO;
/**
* @description undefined
*/
type?: TelemetrytypesQueryTypeDTO;
/**
* @type integer
* @description undefined
@@ -12237,54 +12076,6 @@ export type TestRule200 = {
status: string;
};
export type ListSavedViewsParams = {
/**
* @description undefined
*/
source?: SavedviewtypesSourceDTO;
/**
* @type string
* @description undefined
*/
name?: string;
};
export type ListSavedViews200 = {
/**
* @type array,null
*/
data: SavedviewtypesSavedViewDTO[] | null;
/**
* @type string
*/
status: string;
};
export type CreateSavedView201 = {
data: TypesIdentifiableDTO;
/**
* @type string
*/
status: string;
};
export type DeleteSavedViewPathParameters = {
id: string;
};
export type GetSavedViewPathParameters = {
id: string;
};
export type GetSavedView200 = {
data: SavedviewtypesSavedViewDTO;
/**
* @type string
*/
status: string;
};
export type UpdateSavedViewPathParameters = {
id: string;
};
export type GetSessionContext200 = {
data: AuthtypesSessionContextDTO;
/**

View File

@@ -25,6 +25,7 @@ import type {
CreateResetPasswordTokenPathParameters,
CreateUser201,
CreateUserRole201,
DeleteUserDeprecatedPathParameters,
DeleteUserPathParameters,
DeleteUserRolePathParameters,
GetMyUser200,
@@ -36,6 +37,8 @@ import type {
GetRolesByUserID200,
GetRolesByUserIDPathParameters,
GetUser200,
GetUserDeprecated200,
GetUserDeprecatedPathParameters,
GetUserPathParameters,
GetUserRole200,
GetUserRolePathParameters,
@@ -47,12 +50,16 @@ import type {
RenderErrorResponseDTO,
SetRoleByUserIDPathParameters,
TypesChangePasswordRequestDTO,
TypesDeprecatedUserDTO,
TypesPostableBulkInviteRequestDTO,
TypesPostableForgotPasswordDTO,
TypesPostableInviteDTO,
TypesPostableResetPasswordDTO,
TypesPostableRoleDTO,
TypesPostableVerifyResetPasswordTokenDTO,
TypesUpdatableUserDTO,
UpdateUserDeprecated200,
UpdateUserDeprecatedPathParameters,
UpdateUserPathParameters,
} from '../sigNoz.schemas';
@@ -254,11 +261,95 @@ export const useCreateInvite = <
return useMutation(getCreateInviteMutationOptions(options));
};
/**
* This endpoint resets the password by token
* This endpoint creates a bulk invite for a user
* @deprecated
* @summary Create bulk invite
*/
export const createBulkInvite = (
typesPostableBulkInviteRequestDTO?: BodyType<TypesPostableBulkInviteRequestDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/invite/bulk`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesPostableBulkInviteRequestDTO,
signal,
});
};
export const getCreateBulkInviteMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createBulkInvite>>,
TError,
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createBulkInvite>>,
TError,
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> },
TContext
> => {
const mutationKey = ['createBulkInvite'];
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 createBulkInvite>>,
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> }
> = (props) => {
const { data } = props ?? {};
return createBulkInvite(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateBulkInviteMutationResult = NonNullable<
Awaited<ReturnType<typeof createBulkInvite>>
>;
export type CreateBulkInviteMutationBody =
| BodyType<TypesPostableBulkInviteRequestDTO>
| undefined;
export type CreateBulkInviteMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Create bulk invite
*/
export const useCreateBulkInvite = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createBulkInvite>>,
TError,
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createBulkInvite>>,
TError,
{ data?: BodyType<TypesPostableBulkInviteRequestDTO> },
TContext
> => {
return useMutation(getCreateBulkInviteMutationOptions(options));
};
/**
* This endpoint resets the password by token
* @summary Reset password
*/
export const resetPasswordDeprecated = (
export const resetPassword = (
typesPostableResetPasswordDTO?: BodyType<TypesPostableResetPasswordDTO>,
signal?: AbortSignal,
) => {
@@ -271,23 +362,23 @@ export const resetPasswordDeprecated = (
});
};
export const getResetPasswordDeprecatedMutationOptions = <
export const getResetPasswordMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
> => {
const mutationKey = ['resetPasswordDeprecated'];
const mutationKey = ['resetPassword'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
@@ -297,47 +388,45 @@ export const getResetPasswordDeprecatedMutationOptions = <
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
Awaited<ReturnType<typeof resetPassword>>,
{ data?: BodyType<TypesPostableResetPasswordDTO> }
> = (props) => {
const { data } = props ?? {};
return resetPasswordDeprecated(data);
return resetPassword(data);
};
return { mutationFn, ...mutationOptions };
};
export type ResetPasswordDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof resetPasswordDeprecated>>
export type ResetPasswordMutationResult = NonNullable<
Awaited<ReturnType<typeof resetPassword>>
>;
export type ResetPasswordDeprecatedMutationBody =
export type ResetPasswordMutationBody =
| BodyType<TypesPostableResetPasswordDTO>
| undefined;
export type ResetPasswordDeprecatedMutationError =
ErrorType<RenderErrorResponseDTO>;
export type ResetPasswordMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Reset password
*/
export const useResetPasswordDeprecated = <
export const useResetPassword = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof resetPasswordDeprecated>>,
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
> => {
return useMutation(getResetPasswordDeprecatedMutationOptions(options));
return useMutation(getResetPasswordMutationOptions(options));
};
/**
* This endpoint lists all users
@@ -426,6 +515,295 @@ export const invalidateListUsersDeprecated = async (
return queryClient;
};
/**
* This endpoint deletes the user by id
* @deprecated
* @summary Delete user
*/
export const deleteUserDeprecated = (
{ id }: DeleteUserDeprecatedPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/user/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteUserDeprecatedMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteUserDeprecated>>,
TError,
{ pathParams: DeleteUserDeprecatedPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteUserDeprecated>>,
TError,
{ pathParams: DeleteUserDeprecatedPathParameters },
TContext
> => {
const mutationKey = ['deleteUserDeprecated'];
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 deleteUserDeprecated>>,
{ pathParams: DeleteUserDeprecatedPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteUserDeprecated(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteUserDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteUserDeprecated>>
>;
export type DeleteUserDeprecatedMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Delete user
*/
export const useDeleteUserDeprecated = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteUserDeprecated>>,
TError,
{ pathParams: DeleteUserDeprecatedPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteUserDeprecated>>,
TError,
{ pathParams: DeleteUserDeprecatedPathParameters },
TContext
> => {
return useMutation(getDeleteUserDeprecatedMutationOptions(options));
};
/**
* This endpoint returns the user by id
* @deprecated
* @summary Get user
*/
export const getUserDeprecated = (
{ id }: GetUserDeprecatedPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetUserDeprecated200>({
url: `/api/v1/user/${id}`,
method: 'GET',
signal,
});
};
export const getGetUserDeprecatedQueryKey = ({
id,
}: GetUserDeprecatedPathParameters) => {
return [`/api/v1/user/${id}`] as const;
};
export const getGetUserDeprecatedQueryOptions = <
TData = Awaited<ReturnType<typeof getUserDeprecated>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetUserDeprecatedPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getUserDeprecated>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetUserDeprecatedQueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getUserDeprecated>>
> = ({ signal }) => getUserDeprecated({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getUserDeprecated>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetUserDeprecatedQueryResult = NonNullable<
Awaited<ReturnType<typeof getUserDeprecated>>
>;
export type GetUserDeprecatedQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Get user
*/
export function useGetUserDeprecated<
TData = Awaited<ReturnType<typeof getUserDeprecated>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetUserDeprecatedPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getUserDeprecated>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetUserDeprecatedQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @deprecated
* @summary Get user
*/
export const invalidateGetUserDeprecated = async (
queryClient: QueryClient,
{ id }: GetUserDeprecatedPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetUserDeprecatedQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint updates the user by id
* @deprecated
* @summary Update user
*/
export const updateUserDeprecated = (
{ id }: UpdateUserDeprecatedPathParameters,
typesDeprecatedUserDTO?: BodyType<TypesDeprecatedUserDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<UpdateUserDeprecated200>({
url: `/api/v1/user/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: typesDeprecatedUserDTO,
signal,
});
};
export const getUpdateUserDeprecatedMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateUserDeprecated>>,
TError,
{
pathParams: UpdateUserDeprecatedPathParameters;
data?: BodyType<TypesDeprecatedUserDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateUserDeprecated>>,
TError,
{
pathParams: UpdateUserDeprecatedPathParameters;
data?: BodyType<TypesDeprecatedUserDTO>;
},
TContext
> => {
const mutationKey = ['updateUserDeprecated'];
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 updateUserDeprecated>>,
{
pathParams: UpdateUserDeprecatedPathParameters;
data?: BodyType<TypesDeprecatedUserDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateUserDeprecated(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateUserDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof updateUserDeprecated>>
>;
export type UpdateUserDeprecatedMutationBody =
| BodyType<TypesDeprecatedUserDTO>
| undefined;
export type UpdateUserDeprecatedMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Update user
*/
export const useUpdateUserDeprecated = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateUserDeprecated>>,
TError,
{
pathParams: UpdateUserDeprecatedPathParameters;
data?: BodyType<TypesDeprecatedUserDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateUserDeprecated>>,
TError,
{
pathParams: UpdateUserDeprecatedPathParameters;
data?: BodyType<TypesDeprecatedUserDTO>;
},
TContext
> => {
return useMutation(getUpdateUserDeprecatedMutationOptions(options));
};
/**
* This endpoint returns the user I belong to
* @deprecated
@@ -596,89 +974,6 @@ export const useForgotPassword = <
> => {
return useMutation(getForgotPasswordMutationOptions(options));
};
/**
* This endpoint resets the password using a single use reset password token
* @summary Reset password
*/
export const resetPassword = (
typesPostableResetPasswordDTO?: BodyType<TypesPostableResetPasswordDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/factor_password/reset`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesPostableResetPasswordDTO,
signal,
});
};
export const getResetPasswordMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
> => {
const mutationKey = ['resetPassword'];
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 resetPassword>>,
{ data?: BodyType<TypesPostableResetPasswordDTO> }
> = (props) => {
const { data } = props ?? {};
return resetPassword(data);
};
return { mutationFn, ...mutationOptions };
};
export type ResetPasswordMutationResult = NonNullable<
Awaited<ReturnType<typeof resetPassword>>
>;
export type ResetPasswordMutationBody =
| BodyType<TypesPostableResetPasswordDTO>
| undefined;
export type ResetPasswordMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Reset password
*/
export const useResetPassword = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data?: BodyType<TypesPostableResetPasswordDTO> },
TContext
> => {
return useMutation(getResetPasswordMutationOptions(options));
};
/**
* This endpoint verifies whether a reset password token exists and is not expired
* @summary Verify a reset password token

View File

@@ -24,3 +24,19 @@ export const Logout = async (): Promise<void> => {
window.dispatchEvent(new CustomEvent('LOGOUT'));
history.push(ROUTES.LOGIN);
};
export const UnderscoreToDotMap: Record<string, string> = {
k8s_cluster_name: 'k8s.cluster.name',
k8s_cluster_uid: 'k8s.cluster.uid',
k8s_namespace_name: 'k8s.namespace.name',
k8s_node_name: 'k8s.node.name',
k8s_node_uid: 'k8s.node.uid',
k8s_pod_name: 'k8s.pod.name',
k8s_pod_uid: 'k8s.pod.uid',
k8s_deployment_name: 'k8s.deployment.name',
k8s_daemonset_name: 'k8s.daemonset.name',
k8s_statefulset_name: 'k8s.statefulset.name',
k8s_cronjob_name: 'k8s.cronjob.name',
k8s_job_name: 'k8s.job.name',
k8s_persistentvolumeclaim_name: 'k8s.persistentvolumeclaim.name',
};

View File

@@ -0,0 +1,31 @@
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/user/resetPassword';
/**
* @deprecated Use the generated `useResetPassword` hook (or `resetPassword` fetcher) from
* `api/generated/services/users` 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 resetPassword = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {
try {
const response = await axios.post<PayloadProps>(`/resetPassword`, {
...props,
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default resetPassword;

View File

@@ -0,0 +1,28 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { UsersProps } from 'types/api/user/inviteUsers';
/**
* @deprecated Use the generated `useCreateBulkInvite` hook (or `createBulkInvite` fetcher) from
* `api/generated/services/users` 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 inviteUsers = async (
users: UsersProps,
): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.post(`/invite/bulk`, users);
return {
httpStatusCode: response.status,
data: null,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default inviteUsers;

View File

@@ -0,0 +1,31 @@
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/user/setInvite';
/**
* @deprecated Use the generated `useCreateInvite` hook (or `createInvite` fetcher) from
* `api/generated/services/users` 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 sendInvite = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {
try {
const response = await axios.post<PayloadProps>(`/invite`, {
...props,
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default sendInvite;

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="#9CA3AF" fill-rule="evenodd" style="flex:none;line-height:1" viewBox="0 0 24 24"><title>AWS</title><path d="M6.763 11.212q.002.446.088.71c.064.176.144.368.256.576.04.063.056.127.056.183q.002.12-.152.24l-.503.335a.4.4 0 0 1-.208.072q-.12-.002-.239-.112a2.5 2.5 0 0 1-.287-.375 6 6 0 0 1-.248-.471q-.934 1.101-2.347 1.101c-.67 0-1.205-.191-1.596-.574-.39-.384-.59-.894-.59-1.533 0-.678.24-1.23.726-1.644.487-.415 1.133-.623 1.955-.623.272 0 .551.024.846.064.296.04.6.104.918.176v-.583q-.001-.908-.375-1.277c-.255-.248-.686-.367-1.3-.367-.28 0-.568.031-.863.103s-.583.16-.862.272a2 2 0 0 1-.28.104.5.5 0 0 1-.127.023q-.168.002-.168-.247v-.391c0-.128.016-.224.056-.28a.6.6 0 0 1 .224-.167 4.6 4.6 0 0 1 1.005-.36 4.8 4.8 0 0 1 1.246-.151c.95 0 1.644.216 2.091.647q.661.646.662 1.963v2.586zm-3.24 1.214c.263 0 .534-.048.822-.144a1.8 1.8 0 0 0 .758-.51 1.3 1.3 0 0 0 .272-.512c.047-.191.08-.423.08-.694v-.335a7 7 0 0 0-.735-.136 6 6 0 0 0-.75-.048c-.535 0-.926.104-1.19.32-.263.215-.39.518-.39.917 0 .375.095.655.295.846.191.2.47.296.838.296m6.41.862c-.144 0-.24-.024-.304-.08-.064-.048-.12-.16-.168-.311L7.586 6.726a1.4 1.4 0 0 1-.072-.32c0-.128.064-.2.191-.2h.783q.227-.001.31.08c.065.048.113.16.16.312l1.342 5.284 1.245-5.284q.058-.24.151-.312a.55.55 0 0 1 .32-.08h.638c.152 0 .256.025.32.08.063.048.12.16.151.312l1.261 5.348 1.381-5.348q.074-.24.16-.312a.52.52 0 0 1 .311-.08h.743c.127 0 .2.065.2.2 0 .04-.009.08-.017.128a1 1 0 0 1-.056.2l-1.923 6.17q-.072.24-.168.311a.5.5 0 0 1-.303.08h-.687c-.15 0-.255-.024-.32-.08-.063-.056-.119-.16-.15-.32L12.32 7.747l-1.23 5.14c-.04.16-.087.264-.15.32-.065.056-.177.08-.32.08zm10.256.215c-.415 0-.83-.048-1.229-.143-.399-.096-.71-.2-.918-.32-.128-.071-.215-.151-.247-.223a.6.6 0 0 1-.048-.224v-.407c0-.167.064-.247.183-.247q.072 0 .144.024c.048.016.12.048.2.08q.408.181.878.279c.32.064.63.096.95.096.502 0 .894-.088 1.165-.264a.86.86 0 0 0 .415-.758.78.78 0 0 0-.215-.559c-.144-.151-.416-.287-.807-.415l-1.157-.36c-.583-.183-1.014-.454-1.277-.813a1.9 1.9 0 0 1-.4-1.158q0-.502.216-.886c.144-.255.335-.479.575-.654.24-.184.51-.32.83-.415.32-.096.655-.136 1.006-.136.175 0 .36.008.535.032.183.024.35.056.518.088q.24.058.455.127.216.072.336.144a.7.7 0 0 1 .24.2.43.43 0 0 1 .071.263v.375q-.002.254-.184.256a.8.8 0 0 1-.303-.096 3.65 3.65 0 0 0-1.532-.311c-.455 0-.815.071-1.062.223s-.375.383-.375.71c0 .224.08.416.24.567.16.152.454.304.877.44l1.134.358c.574.184.99.44 1.237.767s.367.702.367 1.117c0 .343-.072.655-.207.926a2.2 2.2 0 0 1-.583.703c-.248.2-.543.343-.886.447-.36.111-.734.167-1.142.167"/><path fill="#f90" d="M.378 15.475c3.384 1.963 7.56 3.153 11.877 3.153 2.914 0 6.114-.607 9.06-1.852.44-.2.814.287.383.607-2.626 1.94-6.442 2.969-9.722 2.969-4.598 0-8.74-1.7-11.87-4.526-.247-.223-.024-.527.272-.351m23.531-.2c.287.36-.08 2.826-1.485 4.007-.215.184-.423.088-.327-.151l.175-.439c.343-.88.802-2.198.52-2.555-.336-.43-2.22-.207-3.074-.103-.255.032-.295-.192-.063-.36 1.5-1.053 3.967-.75 4.254-.399"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" fill-rule="evenodd" style="flex:none;line-height:1" viewBox="0 0 24 24"><title>AWS</title><path d="M6.763 11.212q.002.446.088.71c.064.176.144.368.256.576.04.063.056.127.056.183q.002.12-.152.24l-.503.335a.4.4 0 0 1-.208.072q-.12-.002-.239-.112a2.5 2.5 0 0 1-.287-.375 6 6 0 0 1-.248-.471q-.934 1.101-2.347 1.101c-.67 0-1.205-.191-1.596-.574-.39-.384-.59-.894-.59-1.533 0-.678.24-1.23.726-1.644.487-.415 1.133-.623 1.955-.623.272 0 .551.024.846.064.296.04.6.104.918.176v-.583q-.001-.908-.375-1.277c-.255-.248-.686-.367-1.3-.367-.28 0-.568.031-.863.103s-.583.16-.862.272a2 2 0 0 1-.28.104.5.5 0 0 1-.127.023q-.168.002-.168-.247v-.391c0-.128.016-.224.056-.28a.6.6 0 0 1 .224-.167 4.6 4.6 0 0 1 1.005-.36 4.8 4.8 0 0 1 1.246-.151c.95 0 1.644.216 2.091.647q.661.646.662 1.963v2.586zm-3.24 1.214c.263 0 .534-.048.822-.144a1.8 1.8 0 0 0 .758-.51 1.3 1.3 0 0 0 .272-.512c.047-.191.08-.423.08-.694v-.335a7 7 0 0 0-.735-.136 6 6 0 0 0-.75-.048c-.535 0-.926.104-1.19.32-.263.215-.39.518-.39.917 0 .375.095.655.295.846.191.2.47.296.838.296m6.41.862c-.144 0-.24-.024-.304-.08-.064-.048-.12-.16-.168-.311L7.586 6.726a1.4 1.4 0 0 1-.072-.32c0-.128.064-.2.191-.2h.783q.227-.001.31.08c.065.048.113.16.16.312l1.342 5.284 1.245-5.284q.058-.24.151-.312a.55.55 0 0 1 .32-.08h.638c.152 0 .256.025.32.08.063.048.12.16.151.312l1.261 5.348 1.381-5.348q.074-.24.16-.312a.52.52 0 0 1 .311-.08h.743c.127 0 .2.065.2.2 0 .04-.009.08-.017.128a1 1 0 0 1-.056.2l-1.923 6.17q-.072.24-.168.311a.5.5 0 0 1-.303.08h-.687c-.15 0-.255-.024-.32-.08-.063-.056-.119-.16-.15-.32L12.32 7.747l-1.23 5.14c-.04.16-.087.264-.15.32-.065.056-.177.08-.32.08zm10.256.215c-.415 0-.83-.048-1.229-.143-.399-.096-.71-.2-.918-.32-.128-.071-.215-.151-.247-.223a.6.6 0 0 1-.048-.224v-.407c0-.167.064-.247.183-.247q.072 0 .144.024c.048.016.12.048.2.08q.408.181.878.279c.32.064.63.096.95.096.502 0 .894-.088 1.165-.264a.86.86 0 0 0 .415-.758.78.78 0 0 0-.215-.559c-.144-.151-.416-.287-.807-.415l-1.157-.36c-.583-.183-1.014-.454-1.277-.813a1.9 1.9 0 0 1-.4-1.158q0-.502.216-.886c.144-.255.335-.479.575-.654.24-.184.51-.32.83-.415.32-.096.655-.136 1.006-.136.175 0 .36.008.535.032.183.024.35.056.518.088q.24.058.455.127.216.072.336.144a.7.7 0 0 1 .24.2.43.43 0 0 1 .071.263v.375q-.002.254-.184.256a.8.8 0 0 1-.303-.096 3.65 3.65 0 0 0-1.532-.311c-.455 0-.815.071-1.062.223s-.375.383-.375.71c0 .224.08.416.24.567.16.152.454.304.877.44l1.134.358c.574.184.99.44 1.237.767s.367.702.367 1.117c0 .343-.072.655-.207.926a2.2 2.2 0 0 1-.583.703c-.248.2-.543.343-.886.447-.36.111-.734.167-1.142.167"/><path fill="#f90" d="M.378 15.475c3.384 1.963 7.56 3.153 11.877 3.153 2.914 0 6.114-.607 9.06-1.852.44-.2.814.287.383.607-2.626 1.94-6.442 2.969-9.722 2.969-4.598 0-8.74-1.7-11.87-4.526-.247-.223-.024-.527.272-.351m23.531-.2c.287.36-.08 2.826-1.485 4.007-.215.184-.423.088-.327-.151l.175-.439c.343-.88.802-2.198.52-2.555-.336-.43-2.22-.207-3.074-.103-.255.032-.295-.192-.063-.36 1.5-1.053 3.967-.75 4.254-.399"/></svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -1,3 +1,3 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#29F1FB" d="M8.932 20.806c-.369 0-.738.007-1.109 0-.35-.007-.587-.206-.623-.5a.587.587 0 0 1 .53-.636c.79-.062 1.582-.063 2.372-.003a.548.548 0 0 1 .522.602c-.024.326-.253.526-.616.54zM1.792 8.345c-.392 0-.782.008-1.173.002-.327-.006-.577-.22-.614-.512-.037-.293.146-.544.499-.615.192-.032.388-.045.583-.039a81.515 81.515 0 0 1 1.597 0c.163 0 .325.019.483.056.288.073.445.318.411.617-.034.298-.214.477-.515.487-.424.014-.848.004-1.272.004zm7.588 8.417H4.292a2.464 2.464 0 0 1-.326-.007c-.294-.04-.48-.209-.508-.506-.029-.298.11-.501.391-.606.179-.065.365-.051.549-.051 3.347 0 6.695.005 10.042-.006 1.174-.004 2.187-.439 2.993-1.3.69-.738 1.053-1.63 1.16-2.635.085-.788-.027-1.513-.516-2.156-.544-.718-1.28-1.078-2.163-1.082-3.163-.013-6.328-.005-9.487-.01-.336 0-.673-.027-1.007-.058-.29-.027-.45-.201-.469-.492-.021-.317.141-.545.429-.6a1.55 1.55 0 0 1 .29-.015h10.177c1.71.004 3.187 1.038 3.726 2.654.383 1.147.246 2.304-.182 3.416-.824 2.135-2.762 3.448-5.055 3.454-1.652.005-3.304 0-4.956 0zm2.906-13.568c1.533 0 3.066-.008 4.598 0 2.935.018 5.629 1.892 6.653 4.626.442 1.181.538 2.403.412 3.657-.185 1.842-.735 3.552-1.776 5.084-1.608 2.365-3.873 3.68-6.679 4.118-.95.148-1.905.13-2.86.13-.397 0-.61-.181-.633-.51-.025-.351.196-.621.587-.645.434-.026.87-.004 1.305-.016 2.641-.072 4.928-.982 6.74-2.935 1.269-1.37 1.912-3.039 2.13-4.878.151-1.275.135-2.544-.37-3.752-.773-1.85-2.159-2.983-4.068-3.509-.74-.204-1.5-.243-2.26-.247-2.837-.017-5.675-.007-8.511-.007-.12 0-.24.004-.359-.006a.57.57 0 0 1-.517-.536.557.557 0 0 1 .456-.557c.13-.018.261-.024.392-.019h4.762Z"/>
<path fill="currentColor" d="M8.932 20.806c-.369 0-.738.007-1.109 0-.35-.007-.587-.206-.623-.5a.587.587 0 0 1 .53-.636c.79-.062 1.582-.063 2.372-.003a.548.548 0 0 1 .522.602c-.024.326-.253.526-.616.54zM1.792 8.345c-.392 0-.782.008-1.173.002-.327-.006-.577-.22-.614-.512-.037-.293.146-.544.499-.615.192-.032.388-.045.583-.039a81.515 81.515 0 0 1 1.597 0c.163 0 .325.019.483.056.288.073.445.318.411.617-.034.298-.214.477-.515.487-.424.014-.848.004-1.272.004zm7.588 8.417H4.292a2.464 2.464 0 0 1-.326-.007c-.294-.04-.48-.209-.508-.506-.029-.298.11-.501.391-.606.179-.065.365-.051.549-.051 3.347 0 6.695.005 10.042-.006 1.174-.004 2.187-.439 2.993-1.3.69-.738 1.053-1.63 1.16-2.635.085-.788-.027-1.513-.516-2.156-.544-.718-1.28-1.078-2.163-1.082-3.163-.013-6.328-.005-9.487-.01-.336 0-.673-.027-1.007-.058-.29-.027-.45-.201-.469-.492-.021-.317.141-.545.429-.6a1.55 1.55 0 0 1 .29-.015h10.177c1.71.004 3.187 1.038 3.726 2.654.383 1.147.246 2.304-.182 3.416-.824 2.135-2.762 3.448-5.055 3.454-1.652.005-3.304 0-4.956 0zm2.906-13.568c1.533 0 3.066-.008 4.598 0 2.935.018 5.629 1.892 6.653 4.626.442 1.181.538 2.403.412 3.657-.185 1.842-.735 3.552-1.776 5.084-1.608 2.365-3.873 3.68-6.679 4.118-.95.148-1.905.13-2.86.13-.397 0-.61-.181-.633-.51-.025-.351.196-.621.587-.645.434-.026.87-.004 1.305-.016 2.641-.072 4.928-.982 6.74-2.935 1.269-1.37 1.912-3.039 2.13-4.878.151-1.275.135-2.544-.37-3.752-.773-1.85-2.159-2.983-4.068-3.509-.74-.204-1.5-.243-2.26-.247-2.837-.017-5.675-.007-8.511-.007-.12 0-.24.004-.359-.006a.57.57 0 0 1-.517-.536.557.557 0 0 1 .456-.557c.13-.018.261-.024.392-.019h4.762Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,3 +0,0 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#D22128" d="M17.805 2.197v.066h.156v.44h.072v-.44h.156v-.066zm.9 0l-.175.353-.172-.353h-.087v.506h.067V2.3l.172.35h.045l.172-.35v.404h.066v-.506zm-4.257 1c-.204.31-.424.66-.66 1.06l-.04.062a44.457 44.457 0 00-1.265 2.29c-.187.36-.38.742-.577 1.146l2.267-.25c.66-.302.955-.578 1.242-.976a15.5 15.5 0 00.23-.342c.23-.363.46-.763.663-1.16.197-.386.37-.767.505-1.11.083-.22.15-.422.198-.6.042-.158.074-.307.1-.45-.884.15-1.965.295-2.668.33zM11.894 7.78l-.077.16c-.078.16-.157.32-.236.488-.086.18-.172.364-.26.552l-.132.287a75.265 75.265 0 00-1.427 3.3c-.163.397-.327.807-.493 1.23-.15.38-.297.765-.45 1.164l-.02.06c-.15.396-.3.802-.453 1.22l-.01.027.72-.08a.213.213 0 01-.042-.006c.863-.106 2.01-.75 2.75-1.547.342-.367.652-.8.94-1.306.213-.377.413-.795.604-1.258.168-.405.328-.843.48-1.318-.196.105-.423.18-.673.235a2.184 2.184 0 01-.273.046c.806-.31 1.314-.905 1.683-1.64a2.816 2.816 0 01-.968.428c-.06.012-.116.022-.174.03l-.043.006h.002c.278-.118.514-.248.718-.403a2.571 2.571 0 00.637-.698l.063-.104.077-.154a8.107 8.107 0 00.367-.85l.03-.088a3.04 3.04 0 00.123-.463.733.733 0 01-.094.065c-.243.145-.66.277-.996.34l.663-.074-.664.073h-.017l-.1.017c.006-.003.01-.006.017-.008l-2.265.25-.013.022zM8.27 16.45c-.117.323-.236.654-.355.992l-.005.015c-.016.046-.032.094-.05.142-.08.227-.15.432-.31.9.264.12.475.435.675.793a1.44 1.44 0 00-.466-.99c1.293.06 2.41-.27 2.99-1.217.05-.084.096-.173.14-.268-.26.333-.59.474-1.2.44 0 0-.004 0-.005.002l.004-.002c.9-.404 1.354-.79 1.754-1.433.094-.153.186-.32.28-.503-.788.81-1.702 1.04-2.664.865l-.72.078a6.43 6.43 0 00-.067.183zM15.42.112c-.376.222-1 .85-1.748 1.763l.686 1.294c.48-.687.97-1.307 1.462-1.836l.058-.062c-.02.02-.04.04-.057.062-.16.176-.644.74-1.375 1.863.703-.035 1.784-.18 2.666-.33.262-1.47-.258-2.142-.258-2.142s-.66-1.07-1.436-.61zm-3.084 6.402a40.253 40.253 0 011.306-2.26l.04-.064c.224-.352.45-.693.677-1.02l-.685-1.293-.157.192c-.197.245-.403.51-.613.79a39.853 39.853 0 00-2.016 2.97l-.022.038.893 1.763c.19-.378.38-.752.575-1.118zm-3.73 8.32c.158-.406.319-.81.483-1.225.156-.394.32-.79.484-1.19a91.133 91.133 0 011.6-3.604l.205-.424c.12-.243.237-.485.36-.724a.125.125 0 01.02-.04l-.895-1.763-.044.07c-.207.34-.414.687-.617 1.042a38.056 38.056 0 00-1.092 2.04l-.094.193a24.573 24.573 0 00-1.258 3.087 18.492 18.492 0 00-.52 1.997l.896 1.77c.117-.317.24-.638.364-.963zm-1.376-.476a13.38 13.38 0 00-.234 1.692c0 .02-.004.04-.005.06-.28-.45-1.03-.888-1.026-.884.537.778.944 1.55 1.005 2.31-.29.058-.684-.027-1.14-.195.475.436.83.556.97.588-.434.03-.89.328-1.346.67.668-.27 1.21-.38 1.596-.29-.61 1.74-1.23 3.655-1.843 5.69a.538.538 0 00.364-.354c.11-.368.84-2.786 1.978-5.965l.097-.27.028-.078c.12-.332.246-.672.374-1.02l.09-.237v-.004L7.24 14.3c-.003.02-.01.04-.012.06z"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#EB5424" d="M21.98 7.448 19.62 0H4.347L2.02 7.448c-1.352 4.312.03 9.206 3.815 12.015L12.007 24l6.157-4.552c3.755-2.81 5.182-7.688 3.815-12.015l-6.16 4.58 2.343 7.45-6.157-4.597-6.158 4.58 2.358-7.433-6.188-4.55 7.63-.045L12.008 0l2.356 7.404 7.615.044z"/></svg>

Before

Width:  |  Height:  |  Size: 333 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#9CA3AF" height="34" viewBox="0 0 131 34">
<path fill="#9CA3AF" d="M.36 8.6h16.7v5.6H6.04c-.2 0-.35.16-.35.35v4.9c0 .2.16.35.35.35h11.02v5.6h-5.33c-.2 0-.35.16-.35.35v4.9c0 .2.16.35.35.35h4.98c.2 0 .35-.15.35-.35V25.4h5.34c.2 0 .35-.16.35-.35v-4.9c0-.2-.16-.35-.35-.35h-5.34v-5.6h5.34c.2 0 .35-.16.35-.35v-4.9c0-.2-.16-.35-.35-.35h-5.34V3.35c0-.2-.16-.35-.35-.35H.36c-.2 0-.36.16-.36.35v4.9c0 .2.16.35.36.35ZM44.41 14.7c-.5-.5-1.1-.9-1.76-1.18a5.62 5.62 0 0 0-4.6.17c-.73.37-1.32.91-1.75 1.62h-.17V8.59H34.1v16.83h2.04v-1.81h.17c.21.36.47.67.77.94.31.25.65.48 1.01.67.37.18.77.31 1.18.39a6.2 6.2 0 0 0 3.39-.24 5.36 5.36 0 0 0 3.02-3.1c.29-.75.44-1.62.44-2.6v-.47c0-.96-.16-1.83-.47-2.58-.3-.75-.7-1.4-1.23-1.9v-.01Zm-5.87.66a3.9 3.9 0 0 1 4.34.84c.36.35.64.8.83 1.3.2.5.3 1.07.3 1.7v.47c0 .64-.1 1.23-.3 1.74a3.75 3.75 0 0 1-2.06 2.15 4.27 4.27 0 0 1-3.12-.03 3.86 3.86 0 0 1-2.09-2.2c-.2-.52-.3-1.11-.3-1.75v-.29c0-.62.1-1.2.3-1.7v-.01c.21-.53.5-.99.84-1.36.36-.37.78-.66 1.26-.86ZM97.04 8.59H95v4.86h-2.94v1.86H95v8.17c0 .56.17 1.03.53 1.4.37.35.84.54 1.4.54h4.18v-1.87h-3.5c-.2 0-.33-.05-.43-.15-.1-.1-.14-.27-.14-.49v-7.6h4.65v-1.86h-4.65V8.59ZM114.61 15a5.48 5.48 0 0 0-1.8-1.33 5.6 5.6 0 0 0-2.57-.56 6.17 6.17 0 0 0-4.26 1.7 5.6 5.6 0 0 0-1.72 4.2v.57c0 .9.15 1.75.44 2.5a5.58 5.58 0 0 0 5.5 3.67c1.55 0 2.8-.35 3.72-1.04a5.35 5.35 0 0 0 1.91-2.73l.03-.07-1.94-.52-.02.07c-.11.33-.27.64-.46.94-.17.27-.4.52-.7.74-.28.22-.63.39-1.04.51-.41.13-.9.19-1.46.19a3.8 3.8 0 0 1-2.84-1.05 4.07 4.07 0 0 1-1.1-2.7h9.68v-1.6c0-.54-.11-1.12-.34-1.75a5.04 5.04 0 0 0-1.03-1.74Zm-8.25 3.21a3.8 3.8 0 0 1 1.22-2.25 4.19 4.19 0 0 1 3.99-.7c.44.16.83.38 1.17.66.34.27.62.62.82 1.02.21.38.34.8.38 1.27h-7.58ZM129.09 14.42a4.47 4.47 0 0 0-3.37-1.3c-.93 0-1.73.2-2.4.59-.64.39-1.15.97-1.52 1.74h-.17v-2h-2.04v11.97h2.04v-6.23c0-1.26.32-2.28.95-3.02a3.31 3.31 0 0 1 2.65-1.14c.94 0 1.7.3 2.24.9.56.6.83 1.52.83 2.74v6.75h2.04v-7.13c0-1.71-.42-3.02-1.25-3.87ZM88.1 15a5.48 5.48 0 0 0-1.78-1.33 5.6 5.6 0 0 0-2.58-.56 6.17 6.17 0 0 0-4.27 1.7 5.59 5.59 0 0 0-1.71 4.2v.56c0 .92.14 1.76.44 2.51a5.6 5.6 0 0 0 5.5 3.67c1.55 0 2.8-.35 3.72-1.04a5.36 5.36 0 0 0 1.91-2.73l.03-.07-1.94-.52-.03.07c-.1.32-.26.64-.45.94-.17.27-.4.52-.7.74-.29.21-.64.39-1.05.51-.4.12-.9.19-1.45.19a3.8 3.8 0 0 1-2.85-1.05 4.07 4.07 0 0 1-1.09-2.7h9.68v-1.61c0-.53-.12-1.12-.34-1.74A5.03 5.03 0 0 0 88.1 15Zm-8.24 3.21a3.83 3.83 0 0 1 1.22-2.25 4.2 4.2 0 0 1 3.99-.7c.44.16.83.38 1.16.66.35.27.62.62.83 1.02.2.38.33.8.37 1.27h-7.57ZM73.65 19.42a6.11 6.11 0 0 0-3.23-1.02 6.63 6.63 0 0 1-2.68-.58c-.47-.3-.7-.7-.7-1.25 0-.27.08-.5.21-.7.14-.2.33-.38.56-.52a4.05 4.05 0 0 1 1.78-.42c.85 0 1.54.21 2.06.63.53.41.83 1 .91 1.73l.01.1 1.95-.47-.01-.07c-.07-.45-.22-.91-.45-1.36a3.46 3.46 0 0 0-.94-1.2 4.6 4.6 0 0 0-1.52-.84 6.05 6.05 0 0 0-2.1-.34c-.58 0-1.13.07-1.65.22-.52.14-1 .36-1.43.65-.41.3-.74.66-.99 1.1a2.9 2.9 0 0 0-.37 1.49v.14c0 1.06.38 1.87 1.14 2.42a6.2 6.2 0 0 0 3.24.97c1.18.08 2.04.26 2.56.54.5.28.75.72.75 1.36 0 .6-.25 1.06-.78 1.4-.52.32-1.22.48-2.1.48a3.68 3.68 0 0 1-2.46-.79 3.13 3.13 0 0 1-1.04-2.14v-.09l-1.93.46h-.02v.07a4.4 4.4 0 0 0 3.1 4c.7.24 1.52.36 2.46.36.7 0 1.35-.09 1.93-.27.6-.16 1.12-.4 1.53-.72A3.38 3.38 0 0 0 74.8 22v-.14c0-1.07-.39-1.9-1.14-2.44ZM60.25 23.4c-.1-.1-.14-.27-.14-.49v-9.46h-2.05v1.85h-.16a3.78 3.78 0 0 0-1.61-1.63 4.62 4.62 0 0 0-2.26-.56c-.77 0-1.5.14-2.19.41a5.27 5.27 0 0 0-3.02 3.12c-.29.75-.44 1.63-.44 2.6v.38c0 .99.15 1.87.44 2.63.3.75.7 1.4 1.2 1.93a5.48 5.48 0 0 0 4.05 1.57c.8 0 1.51-.2 2.2-.58.69-.38 1.24-.97 1.63-1.75h.16v.06c0 .56.18 1.03.53 1.4.37.35.85.54 1.41.54h1.36v-1.87h-.68c-.2 0-.34-.05-.43-.15Zm-4.46.13c-.46.2-.97.3-1.52.3a3.68 3.68 0 0 1-2.75-1.09 4.42 4.42 0 0 1-1.05-3.12v-.38c0-.62.1-1.2.29-1.71a3.65 3.65 0 0 1 5-2.17c.47.2.88.49 1.21.86.35.37.62.83.8 1.36.2.5.3 1.08.3 1.7v.3c0 .63-.1 1.23-.3 1.76-.18.5-.45.96-.78 1.33-.33.37-.73.66-1.2.86Z"/>
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" height="34" viewBox="0 0 131 34">
<path fill="currentColor" d="M.36 8.6h16.7v5.6H6.04c-.2 0-.35.16-.35.35v4.9c0 .2.16.35.35.35h11.02v5.6h-5.33c-.2 0-.35.16-.35.35v4.9c0 .2.16.35.35.35h4.98c.2 0 .35-.15.35-.35V25.4h5.34c.2 0 .35-.16.35-.35v-4.9c0-.2-.16-.35-.35-.35h-5.34v-5.6h5.34c.2 0 .35-.16.35-.35v-4.9c0-.2-.16-.35-.35-.35h-5.34V3.35c0-.2-.16-.35-.35-.35H.36c-.2 0-.36.16-.36.35v4.9c0 .2.16.35.36.35ZM44.41 14.7c-.5-.5-1.1-.9-1.76-1.18a5.62 5.62 0 0 0-4.6.17c-.73.37-1.32.91-1.75 1.62h-.17V8.59H34.1v16.83h2.04v-1.81h.17c.21.36.47.67.77.94.31.25.65.48 1.01.67.37.18.77.31 1.18.39a6.2 6.2 0 0 0 3.39-.24 5.36 5.36 0 0 0 3.02-3.1c.29-.75.44-1.62.44-2.6v-.47c0-.96-.16-1.83-.47-2.58-.3-.75-.7-1.4-1.23-1.9v-.01Zm-5.87.66a3.9 3.9 0 0 1 4.34.84c.36.35.64.8.83 1.3.2.5.3 1.07.3 1.7v.47c0 .64-.1 1.23-.3 1.74a3.75 3.75 0 0 1-2.06 2.15 4.27 4.27 0 0 1-3.12-.03 3.86 3.86 0 0 1-2.09-2.2c-.2-.52-.3-1.11-.3-1.75v-.29c0-.62.1-1.2.3-1.7v-.01c.21-.53.5-.99.84-1.36.36-.37.78-.66 1.26-.86ZM97.04 8.59H95v4.86h-2.94v1.86H95v8.17c0 .56.17 1.03.53 1.4.37.35.84.54 1.4.54h4.18v-1.87h-3.5c-.2 0-.33-.05-.43-.15-.1-.1-.14-.27-.14-.49v-7.6h4.65v-1.86h-4.65V8.59ZM114.61 15a5.48 5.48 0 0 0-1.8-1.33 5.6 5.6 0 0 0-2.57-.56 6.17 6.17 0 0 0-4.26 1.7 5.6 5.6 0 0 0-1.72 4.2v.57c0 .9.15 1.75.44 2.5a5.58 5.58 0 0 0 5.5 3.67c1.55 0 2.8-.35 3.72-1.04a5.35 5.35 0 0 0 1.91-2.73l.03-.07-1.94-.52-.02.07c-.11.33-.27.64-.46.94-.17.27-.4.52-.7.74-.28.22-.63.39-1.04.51-.41.13-.9.19-1.46.19a3.8 3.8 0 0 1-2.84-1.05 4.07 4.07 0 0 1-1.1-2.7h9.68v-1.6c0-.54-.11-1.12-.34-1.75a5.04 5.04 0 0 0-1.03-1.74Zm-8.25 3.21a3.8 3.8 0 0 1 1.22-2.25 4.19 4.19 0 0 1 3.99-.7c.44.16.83.38 1.17.66.34.27.62.62.82 1.02.21.38.34.8.38 1.27h-7.58ZM129.09 14.42a4.47 4.47 0 0 0-3.37-1.3c-.93 0-1.73.2-2.4.59-.64.39-1.15.97-1.52 1.74h-.17v-2h-2.04v11.97h2.04v-6.23c0-1.26.32-2.28.95-3.02a3.31 3.31 0 0 1 2.65-1.14c.94 0 1.7.3 2.24.9.56.6.83 1.52.83 2.74v6.75h2.04v-7.13c0-1.71-.42-3.02-1.25-3.87ZM88.1 15a5.48 5.48 0 0 0-1.78-1.33 5.6 5.6 0 0 0-2.58-.56 6.17 6.17 0 0 0-4.27 1.7 5.59 5.59 0 0 0-1.71 4.2v.56c0 .92.14 1.76.44 2.51a5.6 5.6 0 0 0 5.5 3.67c1.55 0 2.8-.35 3.72-1.04a5.36 5.36 0 0 0 1.91-2.73l.03-.07-1.94-.52-.03.07c-.1.32-.26.64-.45.94-.17.27-.4.52-.7.74-.29.21-.64.39-1.05.51-.4.12-.9.19-1.45.19a3.8 3.8 0 0 1-2.85-1.05 4.07 4.07 0 0 1-1.09-2.7h9.68v-1.61c0-.53-.12-1.12-.34-1.74A5.03 5.03 0 0 0 88.1 15Zm-8.24 3.21a3.83 3.83 0 0 1 1.22-2.25 4.2 4.2 0 0 1 3.99-.7c.44.16.83.38 1.16.66.35.27.62.62.83 1.02.2.38.33.8.37 1.27h-7.57ZM73.65 19.42a6.11 6.11 0 0 0-3.23-1.02 6.63 6.63 0 0 1-2.68-.58c-.47-.3-.7-.7-.7-1.25 0-.27.08-.5.21-.7.14-.2.33-.38.56-.52a4.05 4.05 0 0 1 1.78-.42c.85 0 1.54.21 2.06.63.53.41.83 1 .91 1.73l.01.1 1.95-.47-.01-.07c-.07-.45-.22-.91-.45-1.36a3.46 3.46 0 0 0-.94-1.2 4.6 4.6 0 0 0-1.52-.84 6.05 6.05 0 0 0-2.1-.34c-.58 0-1.13.07-1.65.22-.52.14-1 .36-1.43.65-.41.3-.74.66-.99 1.1a2.9 2.9 0 0 0-.37 1.49v.14c0 1.06.38 1.87 1.14 2.42a6.2 6.2 0 0 0 3.24.97c1.18.08 2.04.26 2.56.54.5.28.75.72.75 1.36 0 .6-.25 1.06-.78 1.4-.52.32-1.22.48-2.1.48a3.68 3.68 0 0 1-2.46-.79 3.13 3.13 0 0 1-1.04-2.14v-.09l-1.93.46h-.02v.07a4.4 4.4 0 0 0 3.1 4c.7.24 1.52.36 2.46.36.7 0 1.35-.09 1.93-.27.6-.16 1.12-.4 1.53-.72A3.38 3.38 0 0 0 74.8 22v-.14c0-1.07-.39-1.9-1.14-2.44ZM60.25 23.4c-.1-.1-.14-.27-.14-.49v-9.46h-2.05v1.85h-.16a3.78 3.78 0 0 0-1.61-1.63 4.62 4.62 0 0 0-2.26-.56c-.77 0-1.5.14-2.19.41a5.27 5.27 0 0 0-3.02 3.12c-.29.75-.44 1.63-.44 2.6v.38c0 .99.15 1.87.44 2.63.3.75.7 1.4 1.2 1.93a5.48 5.48 0 0 0 4.05 1.57c.8 0 1.51-.2 2.2-.58.69-.38 1.24-.97 1.63-1.75h.16v.06c0 .56.18 1.03.53 1.4.37.35.85.54 1.41.54h1.36v-1.87h-.68c-.2 0-.34-.05-.43-.15Zm-4.46.13c-.46.2-.97.3-1.52.3a3.68 3.68 0 0 1-2.75-1.09 4.42 4.42 0 0 1-1.05-3.12v-.38c0-.62.1-1.2.29-1.71a3.65 3.65 0 0 1 5-2.17c.47.2.88.49 1.21.86.35.37.62.83.8 1.36.2.5.3 1.08.3 1.7v.3c0 .63-.1 1.23-.3 1.76-.18.5-.45.96-.78 1.33-.33.37-.73.66-1.2.86Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill="#355146" fill-rule="evenodd" d="M6.82 11.908c.525 0 1.57-.03 3.013-.639 1.682-.71 5.029-2 7.443-3.323 1.689-.926 2.429-2.151 2.429-3.8 0-2.29-1.81-4.146-4.043-4.146H6.307C3.1 0 .5 2.666.5 5.954s2.434 5.954 6.32 5.954" clip-rule="evenodd"/><path fill="#d18ee2" fill-rule="evenodd" d="M8.402 16.01c0-1.611.947-3.064 2.399-3.682l2.946-1.254c2.98-1.268 6.26.977 6.26 4.286 0 2.563-2.027 4.64-4.527 4.64l-3.19-.002c-2.147 0-3.888-1.785-3.888-3.987" clip-rule="evenodd"/><path fill="#ff7759" d="M3.848 12.691C1.998 12.691.5 14.228.5 16.124v.444C.5 18.464 1.999 20 3.848 20s3.347-1.536 3.347-3.432v-.444c0-1.896-1.499-3.433-3.347-3.433"/></svg>

Before

Width:  |  Height:  |  Size: 709 B

View File

@@ -1,9 +1,9 @@
<svg width="109" height="24" viewBox="0 0 109 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_125_22125)">
<path d="M0 -2.08616e-07V24H17.9352C22.9911 24 26.0999 21.0858 26.0999 17.04V6.96C26.0999 2.91432 22.9911 -2.08616e-07 17.9352 -2.08616e-07H0ZM6.76413 5.82864H19.1992V18.1714H6.76413V5.82864Z" fill="#9CA3AF"/>
<path d="M46.7659 18.6172H35.0824V14.16H46.7659V18.6172ZM46.595 5.38296V9.5658H35.0824V5.38296H46.595ZM50.2846 12.1373V11.5886C52.5734 10.5258 53.7008 8.8458 53.7008 6.13728C53.7008 2.64012 50.9337 0.000116183 45.5361 0.000116183H28.3184V24H45.7752C51.1728 24 53.9399 21.8401 53.9399 18.0685C53.9399 15.0172 52.6418 13.2001 50.2846 12.1373Z" fill="#9CA3AF"/>
<path d="M62.397 18.1714H74.8319V5.82864H62.397V18.1714ZM63.6609 24C58.6049 24 55.4961 21.0858 55.4961 17.04V6.96012C55.4961 2.91432 58.6049 0.000116183 63.6609 0.000116183H73.568C78.6238 0.000116183 81.7326 2.91432 81.7326 6.96012V17.04C81.7326 21.0858 78.6238 24 73.568 24H63.6609Z" fill="#9CA3AF"/>
<path d="M101.66 15.12L90.8995 14.3658C85.5361 13.9886 83.418 11.1772 83.418 7.47432V6.96012C83.418 2.91432 86.5266 0.000116183 91.5827 0.000116183H100.157C105.214 0.000116183 108.323 2.91432 108.323 6.96012V7.98864H101.968V5.14284H90.2504V8.43432L100.601 9.18864C105.999 9.56568 108.493 12.8572 108.493 16.5257V17.04C108.493 20.7428 105.384 24 100.328 24H91.5827C86.5266 24 83.418 20.7428 83.418 17.04V16.0115H89.7722V18.8572H101.66V15.12Z" fill="#9CA3AF"/>
<path d="M0 -2.08616e-07V24H17.9352C22.9911 24 26.0999 21.0858 26.0999 17.04V6.96C26.0999 2.91432 22.9911 -2.08616e-07 17.9352 -2.08616e-07H0ZM6.76413 5.82864H19.1992V18.1714H6.76413V5.82864Z" fill="currentColor"/>
<path d="M46.7659 18.6172H35.0824V14.16H46.7659V18.6172ZM46.595 5.38296V9.5658H35.0824V5.38296H46.595ZM50.2846 12.1373V11.5886C52.5734 10.5258 53.7008 8.8458 53.7008 6.13728C53.7008 2.64012 50.9337 0.000116183 45.5361 0.000116183H28.3184V24H45.7752C51.1728 24 53.9399 21.8401 53.9399 18.0685C53.9399 15.0172 52.6418 13.2001 50.2846 12.1373Z" fill="currentColor"/>
<path d="M62.397 18.1714H74.8319V5.82864H62.397V18.1714ZM63.6609 24C58.6049 24 55.4961 21.0858 55.4961 17.04V6.96012C55.4961 2.91432 58.6049 0.000116183 63.6609 0.000116183H73.568C78.6238 0.000116183 81.7326 2.91432 81.7326 6.96012V17.04C81.7326 21.0858 78.6238 24 73.568 24H63.6609Z" fill="currentColor"/>
<path d="M101.66 15.12L90.8995 14.3658C85.5361 13.9886 83.418 11.1772 83.418 7.47432V6.96012C83.418 2.91432 86.5266 0.000116183 91.5827 0.000116183H100.157C105.214 0.000116183 108.323 2.91432 108.323 6.96012V7.98864H101.968V5.14284H90.2504V8.43432L100.601 9.18864C105.999 9.56568 108.493 12.8572 108.493 16.5257V17.04C108.493 20.7428 105.384 24 100.328 24H91.5827C86.5266 24 83.418 20.7428 83.418 17.04V16.0115H89.7722V18.8572H101.66V15.12Z" fill="currentColor"/>
</g>
<defs>
<clipPath id="clip0_125_22125">

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,9 +0,0 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="#EF4136" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">
<rect x="2.2" y="2.2" width="19.6" height="19.6" rx="0.6"/>
<path d="M12 2.2V4.7a1.75 1.75 0 1 0 0 3.5V12"/>
<path d="M12 12v2.5a1.75 1.75 0 1 1 0 3.5v3.8"/>
<path d="M2.2 12h2.5a1.75 1.75 0 1 0 3.5 0H12"/>
<path d="M12 12h2.8a1.75 1.75 0 1 1 3.5 0h3.5"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 463 B

View File

@@ -1,3 +0,0 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#00BFB3" d="M13.394 0C8.683 0 4.609 2.716 2.644 6.667h15.641a4.77 4.77 0 0 0 3.073-1.11c.446-.375.864-.785 1.247-1.243l.001-.002A11.974 11.974 0 0 0 13.394 0zM1.804 8.889a12.009 12.009 0 0 0 0 6.222h14.7a3.111 3.111 0 1 0 0-6.222zm.84 8.444C4.61 21.283 8.684 24 13.395 24c3.701 0 7.011-1.677 9.212-4.312l-.001-.002a9.958 9.958 0 0 0-1.247-1.243 4.77 4.77 0 0 0-3.073-1.11z"/>
</svg>

Before

Width:  |  Height:  |  Size: 469 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 128 128"><path fill="#ea4535" d="M80.6 40.3h.4l-.2-.2 14-14v-.3c-11.8-10.4-28.1-14-43.2-9.5C36.5 20.8 24.9 32.8 20.7 48c.2-.1.5-.2.8-.2 5.2-3.4 11.4-5.4 17.9-5.4 2.2 0 4.3.2 6.4.6.1-.1.2-.1.3-.1 9-9.9 24.2-11.1 34.6-2.6h-.1z"/><path fill="#557ebf" d="M108.1 47.8c-2.3-8.5-7.1-16.2-13.8-22.1L80 39.9c6 4.9 9.5 12.3 9.3 20v2.5c16.9 0 16.9 25.2 0 25.2H63.9v20h-.1l.1.2h25.4c14.6.1 27.5-9.3 31.8-23.1 4.3-13.8-1-28.8-13-36.9z"/><path fill="#36a852" d="M39 107.9h26.3V87.7H39c-1.9 0-3.7-.4-5.4-1.1l-15.2 14.6v.2c6 4.3 13.2 6.6 20.7 6.6z"/><path fill="#f9bc15" d="M40.2 41.9c-14.9.1-28.1 9.3-32.9 22.8-4.8 13.6 0 28.5 11.8 37.3l15.6-14.9c-8.6-3.7-10.6-14.5-4-20.8 6.6-6.4 17.8-4.4 21.7 3.8L68 55.2C61.4 46.9 51.1 42 40.2 42.1z"/></svg>

Before

Width:  |  Height:  |  Size: 805 B

View File

@@ -1 +1 @@
<svg fill="#F55036" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Groq</title><path d="M12.036 2c-3.853-.035-7 3-7.036 6.781-.035 3.782 3.055 6.872 6.908 6.907h2.42v-2.566h-2.292c-2.407.028-4.38-1.866-4.408-4.23-.029-2.362 1.901-4.298 4.308-4.326h.1c2.407 0 4.358 1.915 4.365 4.278v6.305c0 2.342-1.944 4.25-4.323 4.279a4.375 4.375 0 01-3.033-1.252l-1.851 1.818A7 7 0 0012.029 22h.092c3.803-.056 6.858-3.083 6.879-6.816v-6.5C18.907 4.963 15.817 2 12.036 2z"></path></svg>
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Groq</title><path d="M12.036 2c-3.853-.035-7 3-7.036 6.781-.035 3.782 3.055 6.872 6.908 6.907h2.42v-2.566h-2.292c-2.407.028-4.38-1.866-4.408-4.23-.029-2.362 1.901-4.298 4.308-4.326h.1c2.407 0 4.358 1.915 4.365 4.278v6.305c0 2.342-1.944 4.25-4.323 4.279a4.375 4.375 0 01-3.033-1.252l-1.851 1.818A7 7 0 0012.029 22h.092c3.803-.056 6.858-3.083 6.879-6.816v-6.5C18.907 4.963 15.817 2 12.036 2z"></path></svg>

Before

Width:  |  Height:  |  Size: 563 B

After

Width:  |  Height:  |  Size: 568 B

View File

@@ -1,74 +0,0 @@
<svg role="img" viewBox="0 0 102.04 102.04" xmlns="http://www.w3.org/2000/svg">
<rect x="45.34" y="23.92" width=".41" height="15.95" transform="translate(-9.7 38.24) rotate(-41.55)" fill="#106DA9"/>
<rect x="32.04" y="45.06" width="11.92" height="11.92" transform="translate(-13.28 88.67) rotate(-89.6)" fill="#106DA9"/>
<rect x="45.04" y="32.06" width="11.92" height="11.92" transform="translate(12.63 88.76) rotate(-89.6)" fill="#106DA9"/>
<rect x="45.04" y="58.06" width="11.92" height="11.92" transform="translate(-13.37 114.58) rotate(-89.6)" fill="#106DA9"/>
<rect x="58.04" y="45.06" width="11.92" height="11.92" transform="translate(12.54 114.67) rotate(-89.6)" fill="#106DA9"/>
<rect x="57.03" y="22.05" width="7.94" height="7.94" transform="translate(34.56 86.84) rotate(-89.6)" fill="#106DA9"/>
<rect x="36.03" y="22.05" width="7.94" height="7.94" transform="translate(13.7 65.84) rotate(-89.6)" fill="#106DA9"/>
<rect x="22.03" y="37.05" width="7.94" height="7.94" transform="translate(-15.2 66.73) rotate(-89.6)" fill="#106DA9"/>
<rect x="22.03" y="58.05" width="7.94" height="7.94" transform="translate(-36.2 87.59) rotate(-89.6)" fill="#106DA9"/>
<rect x="72.03" y="58.05" width="7.94" height="7.94" transform="translate(13.45 137.58) rotate(-89.6)" fill="#106DA9"/>
<rect x="72.03" y="37.05" width="7.94" height="7.94" transform="translate(34.45 116.73) rotate(-89.6)" fill="#106DA9"/>
<rect x="15.02" y="26.04" width="5.96" height="5.96" transform="translate(-11.15 46.82) rotate(-89.6)" fill="#106DA9"/>
<rect x="25.02" y="14.04" width="5.96" height="5.96" transform="translate(10.79 44.9) rotate(-89.61)" fill="#106DA9"/>
<rect x="40.02" y="9.04" width="5.96" height="5.96" transform="translate(30.68 54.94) rotate(-89.6)" fill="#106DA9"/>
<rect x="11.02" y="41.04" width="5.96" height="5.96" transform="translate(-30.12 57.71) rotate(-89.6)" fill="#106DA9"/>
<rect x="81.98" y="26" width="5.04" height="5.04" transform="translate(-.2 .59) rotate(-.4)" fill="#106DA9"/>
<rect x="70.98" y="15" width="5.04" height="5.04" transform="translate(-.12 .51) rotate(-.4)" fill="#106DA9"/>
<rect x="55.98" y="9" width="5.04" height="5.04" transform="translate(-.08 .41) rotate(-.4)" fill="#106DA9"/>
<rect x="84.98" y="41" width="5.04" height="5.03" transform="translate(-.3 .6) rotate(-.4)" fill="#106DA9"/>
<rect x="36.03" y="72.05" width="7.94" height="7.94" transform="translate(-36.3 115.49) rotate(-89.6)" fill="#106DA9"/>
<rect x="57.03" y="72.05" width="7.94" height="7.94" transform="translate(-15.44 136.49) rotate(-89.6)" fill="#106DA9"/>
<rect x="81.02" y="70.04" width="5.96" height="5.96" transform="translate(10.39 156.51) rotate(-89.6)" fill="#106DA9"/>
<rect x="70.02" y="82.04" width="5.96" height="5.96" transform="translate(-12.52 157.43) rotate(-89.6)" fill="#106DA9"/>
<rect x="56.02" y="87.04" width="5.96" height="5.96" transform="translate(-31.44 148.37) rotate(-89.59)" fill="#106DA9"/>
<rect x="84.02" y="55.04" width="5.96" height="5.96" transform="translate(28.35 144.6) rotate(-89.59)" fill="#106DA9"/>
<rect x="14.98" y="71" width="5.04" height="5.04" transform="translate(-.51 .12) rotate(-.4)" fill="#106DA9"/>
<rect x="25.98" y="82" width="5.03" height="5.04" transform="translate(-.61 .21) rotate(-.41)" fill="#106DA9"/>
<rect x="40.98" y="87" width="5.04" height="5.04" transform="translate(-.62 .3) rotate(-.4)" fill="#106DA9"/>
<rect x="11.98" y="55" width="5.04" height="5.04" transform="translate(-.4 .1) rotate(-.4)" fill="#106DA9"/>
<rect x="18.01" y="12.03" width="2.98" height="2.98" transform="translate(5.85 32.93) rotate(-89.61)" fill="#106DA9"/>
<rect x=".01" y="45.03" width="2.98" height="2.98" transform="translate(-45.03 47.71) rotate(-89.61)" fill="#106DA9"/>
<rect x="2.01" y="35.03" width="2.98" height="2.98" transform="translate(-33.04 39.77) rotate(-89.61)" fill="#106DA9"/>
<rect x="35.01" y="2.03" width="2.98" height="2.98" transform="translate(32.73 40) rotate(-89.61)" fill="#106DA9"/>
<rect x="6.01" y="26.03" width="2.98" height="2.98" transform="translate(-20.07 34.83) rotate(-89.61)" fill="#106DA9"/>
<rect x="11.01" y="18.03" width="2.98" height="2.98" transform="translate(-7.11 31.89) rotate(-89.61)" fill="#106DA9"/>
<rect x="25.99" y="6.01" width="3.02" height="3.02" transform="translate(-.05 .19) rotate(-.39)" fill="#106DA9"/>
<rect x="44.01" y=".03" width="2.98" height="2.98" transform="translate(0 .31) rotate(-.39)" fill="#106DA9"/>
<rect x="93.01" y="26.03" width="2.98" height="2.98" transform="translate(66.34 121.83) rotate(-89.61)" fill="#106DA9"/>
<rect x="97.01" y="35.03" width="2.98" height="2.98" transform="translate(61.31 134.77) rotate(-89.61)" fill="#106DA9"/>
<rect x="88.01" y="18.03" width="2.98" height="2.98" transform="translate(69.37 108.89) rotate(-89.61)" fill="#106DA9"/>
<rect x="98.01" y="45.03" width="2.98" height="2.98" transform="translate(52.3 145.7) rotate(-89.61)" fill="#106DA9"/>
<rect x="53.99" y=".01" width="3.02" height="3.02" transform="translate(53.6 57.01) rotate(-89.61)" fill="#106DA9"/>
<rect x="64.01" y="2.03" width="2.98" height="2.98" transform="translate(61.53 69) rotate(-89.61)" fill="#106DA9"/>
<rect x="18.01" y="12.03" width="2.98" height="2.98" transform="translate(5.85 32.93) rotate(-89.61)" fill="#106DA9"/>
<rect x=".01" y="45.03" width="2.98" height="2.98" transform="translate(-45.03 47.71) rotate(-89.61)" fill="#106DA9"/>
<rect x="81.01" y="12.03" width="2.98" height="2.98" transform="translate(68.42 95.93) rotate(-89.61)" fill="#106DA9"/>
<rect x="2.01" y="35.03" width="2.98" height="2.98" transform="translate(-33.04 39.77) rotate(-89.61)" fill="#106DA9"/>
<rect x="35.01" y="2.03" width="2.98" height="2.98" transform="translate(32.73 40) rotate(-89.61)" fill="#106DA9"/>
<rect x="6.01" y="26.03" width="2.98" height="2.98" transform="translate(-20.07 34.83) rotate(-89.61)" fill="#106DA9"/>
<rect x="11.01" y="18.03" width="2.98" height="2.98" transform="translate(-7.11 31.89) rotate(-89.61)" fill="#106DA9"/>
<rect x="25.99" y="6.01" width="3.02" height="3.02" transform="translate(-.05 .19) rotate(-.39)" fill="#106DA9"/>
<rect x="44.01" y=".03" width="2.98" height="2.98" transform="translate(0 .31) rotate(-.39)" fill="#106DA9"/>
<rect x="73.01" y="6.03" width="2.98" height="2.98" transform="translate(66.47 81.97) rotate(-89.61)" fill="#106DA9"/>
<rect x="98.01" y="45.03" width="2.98" height="2.98" transform="translate(52.3 145.7) rotate(-89.61)" fill="#106DA9"/>
<rect x="53.99" y=".01" width="3.02" height="3.02" transform="translate(53.6 57.01) rotate(-89.61)" fill="#106DA9"/>
<rect x="64.01" y="2.03" width="2.98" height="2.98" transform="translate(61.53 69) rotate(-89.61)" fill="#106DA9"/>
<rect x="81.01" y="88.03" width="2.98" height="2.98" transform="translate(-7.58 171.41) rotate(-89.61)" fill="#106DA9"/>
<rect x="98.01" y="54.03" width="2.98" height="2.98" transform="translate(43.31 154.64) rotate(-89.61)" fill="#106DA9"/>
<rect x="18.01" y="88.03" width="2.98" height="2.98" transform="translate(-70.15 108.42) rotate(-89.61)" fill="#106DA9"/>
<rect x="97.01" y="64.03" width="2.98" height="2.98" transform="translate(32.32 163.58) rotate(-89.61)" fill="#106DA9"/>
<rect x="64.01" y="97.04" width="2.97" height="2.97" transform="translate(-33.67 163.03) rotate(-89.43)" fill="#106DA9"/>
<rect x="93.01" y="73.03" width="2.98" height="2.98" transform="translate(19.31 168.49) rotate(-89.59)" fill="#106DA9"/>
<rect x="88.01" y="81.03" width="2.98" height="2.98" transform="translate(6.37 171.45) rotate(-89.61)" fill="#106DA9"/>
<rect x="72.99" y="93.01" width="3.02" height="3.02" transform="translate(-.65 .51) rotate(-.39)" fill="#106DA9"/>
<rect x="53.99" y="99.01" width="3.02" height="3.02" transform="translate(-.68 .38) rotate(-.39)" fill="#106DA9"/>
<rect x="26.01" y="93.03" width="2.98" height="2.98" transform="translate(-67.21 121.37) rotate(-89.61)" fill="#106DA9"/>
<rect x="6.01" y="73.03" width="2.98" height="2.98" transform="translate(-67.07 81.51) rotate(-89.61)" fill="#106DA9"/>
<rect x="2.01" y="64.03" width="2.98" height="2.98" transform="translate(-62.04 68.57) rotate(-89.61)" fill="#106DA9"/>
<rect x="11.01" y="81.03" width="2.98" height="2.98" transform="translate(-70.1 94.46) rotate(-89.61)" fill="#106DA9"/>
<rect x=".01" y="54.03" width="2.98" height="2.98" transform="translate(-54.03 56.65) rotate(-89.61)" fill="#106DA9"/>
<rect x="45.01" y="99.03" width="2.98" height="2.98" transform="translate(-54.33 146.34) rotate(-89.61)" fill="#106DA9"/>
<rect x="35.01" y="97.03" width="2.98" height="2.98" transform="translate(-62.27 134.34) rotate(-89.61)" fill="#106DA9"/>
</svg>

Before

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#FFEC6E" d="m0 0 11.955 24L24 0zm13.366 4.827h1.393v1.38h-1.393zm-2.77 5.569H9.22V8.993h1.389zm0-2.087H9.22V6.906h1.389zm0-2.086H9.22V4.819h1.389zm2.087 6.263h-1.377V11.08h1.388zm0-2.09h-1.377V8.993h1.388zm0-2.087h-1.377V6.906h1.388zm0-2.086h-1.377V4.819h1.388zm.683.683h1.393v1.389h-1.393zm0 3.475V8.993h1.389v1.388Z"/></svg>

Before

Width:  |  Height:  |  Size: 398 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#9CA3AF" d="M9.755 1.52h-.001c-.31 0-.608.124-.828.343L4.037 6.752a1.17 1.17 0 0 1-.827.343H1.17A1.17 1.17 0 0 0 0 8.295l.052 1.984a1.17 1.17 0 0 0 1.17 1.14h2.37c.31 0 .607-.124.827-.344l4.93-4.93c.22-.22.517-.343.827-.343h2.874a1.17 1.17 0 0 0 1.17-1.17V2.69a1.17 1.17 0 0 0-1.17-1.17zm9.78 2.503c-.31 0-.608.123-.828.343l-4.889 4.889a1.17 1.17 0 0 1-.827.342h-2.756c-.31 0-.608.124-.827.344L4.15 15.197a1.17 1.17 0 0 1-.827.343H1.32a1.17 1.17 0 0 0-1.17 1.17v1.996c0 .646.524 1.17 1.17 1.17h2.017c.302 0 .592-.116.81-.325l5.535-5.304a1.17 1.17 0 0 1 .81-.326h2.88c.31 0 .607-.123.827-.342l4.93-4.93c.22-.22.517-.344.827-.344h2.873A1.17 1.17 0 0 0 24 7.135V5.193a1.17 1.17 0 0 0-1.17-1.17h-3.294zm0 8.559c-.31 0-.608.123-.828.343l-4.889 4.889a1.17 1.17 0 0 1-.827.343h-2.04a1.17 1.17 0 0 0-1.17 1.2l.052 1.984a1.17 1.17 0 0 0 1.17 1.14h2.37c.31 0 .607-.124.827-.343l4.93-4.93c.22-.22.517-.343.827-.343h2.873a1.17 1.17 0 0 0 1.17-1.17v-1.943a1.17 1.17 0 0 0-1.17-1.17h-3.294Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,5 +0,0 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect x="2.34" y="2.34" width="19.27" height="19.27" rx="4.69" fill="#fff"/>
<circle cx="10.41" cy="11.95" r="2.77" fill="none" stroke="#000" stroke-width="1.36"/>
<rect x="15" y="8.53" width="1.36" height="6.94" fill="#000"/>
</svg>

Before

Width:  |  Height:  |  Size: 312 B

View File

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

Before

Width:  |  Height:  |  Size: 321 B

After

Width:  |  Height:  |  Size: 326 B

View File

@@ -1,3 +0,0 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#FF6600" d="M23.035 9.601h-7.677a.956.956 0 01-.962-.962V.962a.956.956 0 00-.962-.956H10.56a.956.956 0 00-.962.956V8.64a.956.956 0 01-.962.962H5.762a.956.956 0 01-.961-.962V.962A.956.956 0 003.839 0H.959a.956.956 0 00-.956.962v22.076A.956.956 0 00.965 24h22.07a.956.956 0 00.962-.962V10.58a.956.956 0 00-.962-.98zm-3.86 8.152a1.437 1.437 0 01-1.437 1.443h-1.924a1.437 1.437 0 01-1.436-1.443v-1.917a1.437 1.437 0 011.436-1.443h1.924a1.437 1.437 0 011.437 1.443z"/>
</svg>

Before

Width:  |  Height:  |  Size: 557 B

View File

@@ -1,3 +1,3 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="#9CA3AF" d="M.113 10.27A13.026 13.026 0 000 11.48h18.23c-.064-.125-.15-.237-.235-.347-3.117-4.027-4.793-3.677-7.19-3.78-.8-.034-1.34-.048-4.524-.048-1.704 0-3.555.005-5.358.01-.234.63-.459 1.24-.567 1.737h9.342v1.216H.113v.002zm18.26 2.426H.009c.02.326.05.645.094.961h16.955c.754 0 1.179-.429 1.315-.96zm-17.318 4.28s2.81 6.902 10.93 7.024c4.855 0 9.027-2.883 10.92-7.024H1.056zM11.988 0C7.5 0 3.593 2.466 1.531 6.108l4.75-.005v-.002c3.71 0 3.849.016 4.573.047l.448.016c1.563.052 3.485.22 4.996 1.364.82.621 2.007 1.99 2.712 2.965.654.902.842 1.94.396 2.934-.408.914-1.289 1.458-2.353 1.458H.391s.099.42.249.886h22.748A12.026 12.026 0 0024 12.005C24 5.377 18.621 0 11.988 0z"/>
<path fill="currentColor" d="M.113 10.27A13.026 13.026 0 000 11.48h18.23c-.064-.125-.15-.237-.235-.347-3.117-4.027-4.793-3.677-7.19-3.78-.8-.034-1.34-.048-4.524-.048-1.704 0-3.555.005-5.358.01-.234.63-.459 1.24-.567 1.737h9.342v1.216H.113v.002zm18.26 2.426H.009c.02.326.05.645.094.961h16.955c.754 0 1.179-.429 1.315-.96zm-17.318 4.28s2.81 6.902 10.93 7.024c4.855 0 9.027-2.883 10.92-7.024H1.056zM11.988 0C7.5 0 3.593 2.466 1.531 6.108l4.75-.005v-.002c3.71 0 3.849.016 4.573.047l.448.016c1.563.052 3.485.22 4.996 1.364.82.621 2.007 1.99 2.712 2.965.654.902.842 1.94.396 2.934-.408.914-1.289 1.458-2.353 1.458H.391s.099.42.249.886h22.748A12.026 12.026 0 0024 12.005C24 5.377 18.621 0 11.988 0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 771 B

After

Width:  |  Height:  |  Size: 776 B

View File

@@ -1,13 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 590 270">
<path d="M30.36,109.14v.48h0A3.73,3.73,0,0,1,30.36,109.14Z" fill="#de3423" fill-rule="evenodd"/>
<path d="M30.36,109.14v.48h0A3.73,3.73,0,0,1,30.36,109.14Z" fill="currentColor" fill-rule="evenodd"/>
<path d="M138.66,28.78C107.2,37.87,57.29,43,30.4,43h0V94.35a.8.8,0,0,0,.19.48c18.35,0,75-6,109.18-15.4a129,129,0,0,0,17.49-5.81c4.18-1.88,6.88-3.86,6.88-5.92V15.91C164.1,20.79,151.39,25.11,138.66,28.78Z" fill="#de3423" fill-rule="evenodd"/>
<path d="M138.66,95.37c-18.83,5.43-44.24,9.47-67.39,11.83-15.54,1.59-30.06,2.42-40.87,2.42h0v51.31a.8.8,0,0,0,.19.48c18.35,0,75-6,109.18-15.39a130.38,130.38,0,0,0,17.49-5.81c4.18-1.89,6.88-3.86,6.88-5.92V82.5C164.1,87.37,151.39,91.69,138.66,95.37Z" fill="#de3423" fill-rule="evenodd"/>
<path d="M138.66,162c-18.83,5.43-44.24,9.46-67.39,11.83-15.56,1.59-30.1,2.42-40.91,2.42V228c18.16,0,75.1-5.95,109.37-15.39,12.63-3.48,24.37-7.44,24.37-11.74V149.08C164.1,154,151.39,158.28,138.66,162Z" fill="#de3423" fill-rule="evenodd"/>
<path d="M30.55,94.83C32.4,97.38,48,102.19,71.27,107.2c23.27,4.46,47.47,22.07,66.29,16.64,12.73-3.68,26.54-36.47,26.54-41.34V82c0-3.4-2.55-6.13-6.88-8.4-17.75-9.07-21.11-12.41-27.69-10.6C95.37,72.43,35.06,67.61,30.55,94.83Z" fill="#de3423" fill-rule="evenodd"/>
<path d="M30.55,161.41C32.4,164,48,168.77,71.27,173.79c26,4.74,48.61,20.19,67.44,14.75,12.73-3.68,25.39-34.58,25.39-39.46v-.48c0-3.39-2.55-6.13-6.88-8.39-13.54-7.2-31.43-15.13-38-13.32C85,136.3,39.26,138.37,30.55,161.41Z" fill="#de3423" fill-rule="evenodd"/>
<path d="M200.7,142.39c6,11.79,15.6,17.6,29.05,17.6,14.44,0,19.59-7.64,19.59-15.11,0-5.15-1.83-8.63-6.64-11.79-4.82-3.32-8.3-4.81-16.93-8-10.63-4-16.77-7-23.41-12.29-6.64-5.48-9.79-13-9.79-22.74a28.28,28.28,0,0,1,10.29-22.58c7-5.81,15.44-8.63,25.56-8.63,15.77,0,27.72,6.31,35.69,18.76L249.34,87.78c-4.48-6.81-11.29-10.3-20.59-10.3-9.13,0-15.77,5.15-15.77,12.29,0,4.81,2,7.14,4.82,10,1.82,1.33,6.47,3.32,8.63,4.48l6,2.32,6.8,2.66c11,4.48,18.76,9.3,23.57,14.44s7.31,12.12,7.31,20.75c0,20.42-14.11,34.2-40.51,34.2-21.41,0-37.18-10-44.48-26.4Z" fill="#de3423"/>
<path d="M354.25,104.71,342,117.49a28.14,28.14,0,0,0-21.24-9.13,25,25,0,0,0-18.43,7.47,27.76,27.76,0,0,0,0,37.52,25,25,0,0,0,18.43,7.47A28.14,28.14,0,0,0,342,151.69l12.29,12.78c-9,9.63-20.09,14.44-33.53,14.44-12.79,0-23.58-4.15-32.37-12.62s-13.12-19.09-13.12-31.7,4.32-23.08,13.12-31.54,19.58-12.78,32.37-12.78C334.16,90.27,345.28,95.08,354.25,104.71Z" fill="#de3423"/>
<path d="M393.88,125.62C408,124.3,413,122.47,413,116c0-5.15-4.64-9.13-13.94-9.13q-13.44,0-22.41,10.95l-12.28-10.46c8.13-11.45,19.58-17.09,34.36-17.09,20.75,0,33.7,10,33.7,27.05v37c0,5.81,2.15,6.48,7,6.48h.5v15.43c-2,1.17-5.15,1.83-9.3,1.83-4.48,0-8-1.33-10.62-4a14.06,14.06,0,0,1-3-5.48c-5.81,6.8-15.27,10.29-28.39,10.29-18.42,0-30.87-10.13-30.87-25.4C357.7,136.41,369.15,127.78,393.88,125.62ZM391.56,162c13.28,0,21.41-6,21.41-16.6v-9.3a9.75,9.75,0,0,1-4.14,2.49c-3.82,1.33-6.31,1.66-14.28,2.49-11.62,1.33-17.43,5-17.43,10.79C377.12,158.33,382.43,162,391.56,162Z" fill="#de3423"/>
<path d="M444.84,60.88h19.92V149.2c0,8.13,2.66,11.62,10,11.62a21.15,21.15,0,0,0,6-.67v17.76a35.56,35.56,0,0,1-9.47,1c-17.59,0-26.39-9-26.39-27.06Z" fill="#de3423"/>
<path d="M521.71,125.62c14.11-1.32,19.09-3.15,19.09-9.62,0-5.15-4.64-9.13-13.94-9.13q-13.44,0-22.41,10.95l-12.28-10.46c8.13-11.45,19.58-17.09,34.36-17.09,20.75,0,33.7,10,33.7,27.05v37c0,5.81,2.15,6.48,7,6.48h.5v15.43c-2,1.17-5.15,1.83-9.3,1.83-4.48,0-8-1.33-10.62-4a13.94,13.94,0,0,1-3-5.48c-5.81,6.8-15.27,10.29-28.39,10.29-18.42,0-30.87-10.13-30.87-25.4C485.53,136.41,497,127.78,521.71,125.62ZM519.39,162c13.28,0,21.41-6,21.41-16.6v-9.3a9.73,9.73,0,0,1-4.15,2.49c-3.81,1.33-6.3,1.66-14.27,2.49-11.62,1.33-17.43,5-17.43,10.79C505,158.33,510.26,162,519.39,162Z" fill="#de3423"/>
<path d="M30.55,94.83C32.4,97.38,48,102.19,71.27,107.2c23.27,4.46,47.47,22.07,66.29,16.64,12.73-3.68,26.54-36.47,26.54-41.34V82c0-3.4-2.55-6.13-6.88-8.4-17.75-9.07-21.11-12.41-27.69-10.6C95.37,72.43,35.06,67.61,30.55,94.83Z" fill="currentColor" fill-rule="evenodd"/>
<path d="M30.55,161.41C32.4,164,48,168.77,71.27,173.79c26,4.74,48.61,20.19,67.44,14.75,12.73-3.68,25.39-34.58,25.39-39.46v-.48c0-3.39-2.55-6.13-6.88-8.39-13.54-7.2-31.43-15.13-38-13.32C85,136.3,39.26,138.37,30.55,161.41Z" fill="currentColor" fill-rule="evenodd"/>
<path d="M200.7,142.39c6,11.79,15.6,17.6,29.05,17.6,14.44,0,19.59-7.64,19.59-15.11,0-5.15-1.83-8.63-6.64-11.79-4.82-3.32-8.3-4.81-16.93-8-10.63-4-16.77-7-23.41-12.29-6.64-5.48-9.79-13-9.79-22.74a28.28,28.28,0,0,1,10.29-22.58c7-5.81,15.44-8.63,25.56-8.63,15.77,0,27.72,6.31,35.69,18.76L249.34,87.78c-4.48-6.81-11.29-10.3-20.59-10.3-9.13,0-15.77,5.15-15.77,12.29,0,4.81,2,7.14,4.82,10,1.82,1.33,6.47,3.32,8.63,4.48l6,2.32,6.8,2.66c11,4.48,18.76,9.3,23.57,14.44s7.31,12.12,7.31,20.75c0,20.42-14.11,34.2-40.51,34.2-21.41,0-37.18-10-44.48-26.4Z" fill="currentColor"/>
<path d="M354.25,104.71,342,117.49a28.14,28.14,0,0,0-21.24-9.13,25,25,0,0,0-18.43,7.47,27.76,27.76,0,0,0,0,37.52,25,25,0,0,0,18.43,7.47A28.14,28.14,0,0,0,342,151.69l12.29,12.78c-9,9.63-20.09,14.44-33.53,14.44-12.79,0-23.58-4.15-32.37-12.62s-13.12-19.09-13.12-31.7,4.32-23.08,13.12-31.54,19.58-12.78,32.37-12.78C334.16,90.27,345.28,95.08,354.25,104.71Z" fill="currentColor"/>
<path d="M393.88,125.62C408,124.3,413,122.47,413,116c0-5.15-4.64-9.13-13.94-9.13q-13.44,0-22.41,10.95l-12.28-10.46c8.13-11.45,19.58-17.09,34.36-17.09,20.75,0,33.7,10,33.7,27.05v37c0,5.81,2.15,6.48,7,6.48h.5v15.43c-2,1.17-5.15,1.83-9.3,1.83-4.48,0-8-1.33-10.62-4a14.06,14.06,0,0,1-3-5.48c-5.81,6.8-15.27,10.29-28.39,10.29-18.42,0-30.87-10.13-30.87-25.4C357.7,136.41,369.15,127.78,393.88,125.62ZM391.56,162c13.28,0,21.41-6,21.41-16.6v-9.3a9.75,9.75,0,0,1-4.14,2.49c-3.82,1.33-6.31,1.66-14.28,2.49-11.62,1.33-17.43,5-17.43,10.79C377.12,158.33,382.43,162,391.56,162Z" fill="currentColor"/>
<path d="M444.84,60.88h19.92V149.2c0,8.13,2.66,11.62,10,11.62a21.15,21.15,0,0,0,6-.67v17.76a35.56,35.56,0,0,1-9.47,1c-17.59,0-26.39-9-26.39-27.06Z" fill="currentColor"/>
<path d="M521.71,125.62c14.11-1.32,19.09-3.15,19.09-9.62,0-5.15-4.64-9.13-13.94-9.13q-13.44,0-22.41,10.95l-12.28-10.46c8.13-11.45,19.58-17.09,34.36-17.09,20.75,0,33.7,10,33.7,27.05v37c0,5.81,2.15,6.48,7,6.48h.5v15.43c-2,1.17-5.15,1.83-9.3,1.83-4.48,0-8-1.33-10.62-4a13.94,13.94,0,0,1-3-5.48c-5.81,6.8-15.27,10.29-28.39,10.29-18.42,0-30.87-10.13-30.87-25.4C485.53,136.41,497,127.78,521.71,125.62ZM519.39,162c13.28,0,21.41-6,21.41-16.6v-9.3a9.73,9.73,0,0,1-4.15,2.49c-3.81,1.33-6.3,1.66-14.27,2.49-11.62,1.33-17.43,5-17.43,10.79C505,158.33,510.26,162,519.39,162Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -23,13 +23,6 @@
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.cloud-service-data-collected-table-heading-info {
display: inline-flex;
align-items: center;
color: var(--l3-foreground);
cursor: help;
}
}
.cloud-service-data-collected-table-logs {
@@ -39,9 +32,3 @@
}
}
}
.cloud-service-data-collected-table-tooltip {
max-width: 280px;
white-space: normal;
word-break: break-word;
}

View File

@@ -3,19 +3,16 @@ import {
CloudintegrationtypesCollectedLogAttributeDTO,
CloudintegrationtypesCollectedMetricDTO,
} from 'api/generated/services/sigNoz.schemas';
import { BarChart, Info, ScrollText } from '@signozhq/icons';
import { TooltipProvider, TooltipSimple } from '@signozhq/ui/tooltip';
import { BarChart, ScrollText } from '@signozhq/icons';
import './CloudServiceDataCollected.styles.scss';
function CloudServiceDataCollected({
logsData,
metricsData,
metricsInfoTooltip,
}: {
logsData: CloudintegrationtypesCollectedLogAttributeDTO[] | null | undefined;
metricsData: CloudintegrationtypesCollectedMetricDTO[] | null | undefined;
metricsInfoTooltip?: string;
}): JSX.Element {
const logsColumns = [
{
@@ -87,25 +84,6 @@ function CloudServiceDataCollected({
<div className="cloud-service-data-collected-table-heading">
<BarChart size={14} />
Metrics
{metricsInfoTooltip && (
<TooltipProvider>
<TooltipSimple
title={metricsInfoTooltip}
side="top"
tooltipContentProps={{
className: 'cloud-service-data-collected-table-tooltip',
}}
>
<span
className="cloud-service-data-collected-table-heading-info"
aria-label="About the metrics listed below"
data-testid="data-collected-metrics-info"
>
<Info size={12} />
</span>
</TooltipSimple>
</TooltipProvider>
)}
</div>
<Table
columns={metricsColumns}
@@ -119,8 +97,4 @@ function CloudServiceDataCollected({
);
}
CloudServiceDataCollected.defaultProps = {
metricsInfoTooltip: undefined,
};
export default CloudServiceDataCollected;

View File

@@ -5,9 +5,10 @@ import {
useCreateResetPasswordToken,
useDeleteUser,
useGetResetPasswordToken,
useCreateUserRole,
useDeleteUserRole,
useGetRolesByUserID,
useGetUser,
useRemoveUserRoleByUserIDAndRoleID,
useSetRoleByUserID,
useUpdateMyUserV2,
useUpdateUser,
} from 'api/generated/services/users';
@@ -24,14 +25,15 @@ import EditMemberDrawer, { EditMemberDrawerProps } from '../EditMemberDrawer';
jest.mock('api/generated/services/users', () => ({
useDeleteUser: jest.fn(),
useGetUser: jest.fn(),
useDeleteUserRole: jest.fn(),
useGetRolesByUserID: jest.fn(),
useRemoveUserRoleByUserIDAndRoleID: jest.fn(),
useUpdateUser: jest.fn(),
useUpdateMyUserV2: jest.fn(),
useCreateUserRole: jest.fn(),
useSetRoleByUserID: jest.fn(),
useGetResetPasswordToken: jest.fn(),
useCreateResetPasswordToken: jest.fn(),
getGetUserQueryKey: ({ id }: { id: string }): string[] => [
`/api/v2/users/${id}`,
getGetRolesByUserIDQueryKey: ({ id }: { id: string }): string[] => [
`/api/v2/users/${id}/roles`,
],
}));
@@ -192,7 +194,11 @@ describe('EditMemberDrawer', () => {
isLoading: false,
refetch: jest.fn(),
});
(useDeleteUserRole as jest.Mock).mockReturnValue({
(useGetRolesByUserID as jest.Mock).mockReturnValue({
data: { data: [managedRoles[0]] },
isLoading: false,
});
(useRemoveUserRoleByUserIDAndRoleID as jest.Mock).mockReturnValue({
mutateAsync: mockRemoveMutateAsync.mockResolvedValue({}),
isLoading: false,
});
@@ -204,7 +210,7 @@ describe('EditMemberDrawer', () => {
mutateAsync: jest.fn().mockResolvedValue({}),
isLoading: false,
});
(useCreateUserRole as jest.Mock).mockReturnValue({
(useSetRoleByUserID as jest.Mock).mockReturnValue({
mutateAsync: jest.fn().mockResolvedValue({}),
isLoading: false,
});
@@ -306,12 +312,12 @@ describe('EditMemberDrawer', () => {
expect(onClose).not.toHaveBeenCalled();
});
it('adding a new role creates a user role without removing existing ones', async () => {
it('adding a new role calls setRole without removing existing ones', async () => {
const onComplete = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockSet = jest.fn().mockResolvedValue({});
(useCreateUserRole as jest.Mock).mockReturnValue({
(useSetRoleByUserID as jest.Mock).mockReturnValue({
mutateAsync: mockSet,
isLoading: false,
});
@@ -328,14 +334,15 @@ describe('EditMemberDrawer', () => {
await waitFor(() => {
expect(mockSet).toHaveBeenCalledWith({
data: { userId: 'user-1', roleId: managedRoles[1].id },
pathParams: { id: 'user-1' },
data: { name: 'signoz-editor' },
});
expect(mockRemoveMutateAsync).not.toHaveBeenCalled();
expect(onComplete).toHaveBeenCalled();
});
});
it('deselecting a role deletes the user role by its assignment id', async () => {
it('deselecting a role calls removeRole with the role id', async () => {
const onComplete = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
@@ -354,7 +361,7 @@ describe('EditMemberDrawer', () => {
await waitFor(() => {
expect(mockRemoveMutateAsync).toHaveBeenCalledWith({
pathParams: { id: 'ur-1' },
pathParams: { id: 'user-1', roleId: managedRoles[0].id },
});
expect(onComplete).toHaveBeenCalled();
});

View File

@@ -80,7 +80,7 @@ function InviteMembers({
weight="semibold"
className={styles.headerCellRole}
>
Roles
Role
</Typography.Text>
<div className={styles.headerCellAction} />
</div>
@@ -108,10 +108,11 @@ function InviteMembers({
<div className={styles.cellRole}>
<RolesSelect
mode="multiple"
value={row.roleIds}
onChange={(roleIds): void => updateRole(row.id, roleIds)}
placeholder="Select roles"
mode="single"
value={row.roleId || undefined}
onChange={(roleId): void => updateRole(row.id, roleId)}
placeholder="Select role"
allowClear={false}
id={`invite-role-${row.id}`}
/>
</div>

View File

@@ -68,8 +68,8 @@ describe('InviteMembers - Edge Cases', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], VALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
await expect(
@@ -100,8 +100,8 @@ describe('InviteMembers - Edge Cases', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], VALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
await expect(
@@ -132,17 +132,17 @@ describe('InviteMembers - Edge Cases', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], VALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
await expect(
screen.findByTestId('invite-api-error'),
).resolves.toBeInTheDocument();
const viewerElements = screen.getAllByTitle('Viewer');
const viewerElements = screen.getAllByText('Viewer');
await user.click(viewerElements[0]);
const editorOptions = await screen.findAllByTitle('Editor');
const editorOptions = await screen.findAllByText('Editor');
await user.click(editorOptions[editorOptions.length - 1]);
await waitFor(() => {
@@ -189,8 +189,8 @@ describe('InviteMembers - Edge Cases', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], VALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
const submitBtn = screen.getByTestId('submit-btn');
await user.click(submitBtn);
@@ -226,8 +226,8 @@ describe('InviteMembers - Edge Cases', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], ' alice@signoz.io ');
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));

View File

@@ -32,14 +32,14 @@ describe('InviteMembers - Rendering', () => {
render(<InviteMembers />);
expect(screen.getByText('Email address')).toBeInTheDocument();
expect(screen.getByText('Roles')).toBeInTheDocument();
expect(screen.getByText('Role')).toBeInTheDocument();
});
it('hides header when showHeader is false', () => {
render(<InviteMembers showHeader={false} />);
expect(screen.queryByText('Email address')).not.toBeInTheDocument();
expect(screen.queryByText('Roles')).not.toBeInTheDocument();
expect(screen.queryByText('Role')).not.toBeInTheDocument();
});
it('renders add button by default', () => {
@@ -89,7 +89,7 @@ describe('InviteMembers - Rendering', () => {
it('renders role select for each row', () => {
render(<InviteMembers initialRowCount={2} />);
const roleSelects = screen.getAllByText('Select roles');
const roleSelects = screen.getAllByText('Select role');
expect(roleSelects).toHaveLength(2);
});
});

View File

@@ -40,8 +40,8 @@ describe('InviteMembers - Submission', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], 'alice@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
@@ -73,17 +73,17 @@ describe('InviteMembers - Submission', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], 'alice@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.type(emailInputs[1], 'bob@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
const editorOptions = await screen.findAllByTitle('Editor');
await user.click(screen.getAllByText('Select role')[0]);
const editorOptions = await screen.findAllByText('Editor');
await user.click(editorOptions[editorOptions.length - 1]);
await user.type(emailInputs[2], 'charlie@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
const adminOptions = await screen.findAllByTitle('Admin');
await user.click(screen.getAllByText('Select role')[0]);
const adminOptions = await screen.findAllByText('Admin');
await user.click(adminOptions[adminOptions.length - 1]);
await user.click(screen.getByTestId('submit-btn'));
@@ -125,8 +125,8 @@ describe('InviteMembers - Submission', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], VALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
@@ -154,8 +154,8 @@ describe('InviteMembers - Submission', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], VALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
@@ -218,12 +218,12 @@ describe('InviteMembers - Submission', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], 'alice@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.type(emailInputs[1], 'bob@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
const editorOptions = await screen.findAllByTitle('Editor');
await user.click(screen.getAllByText('Select role')[0]);
const editorOptions = await screen.findAllByText('Editor');
await user.click(editorOptions[editorOptions.length - 1]);
await user.click(screen.getByTestId('submit-btn'));
@@ -276,8 +276,8 @@ describe('InviteMembers - Submission', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], VALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
@@ -303,8 +303,8 @@ describe('InviteMembers - Submission', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], VALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));

View File

@@ -35,8 +35,8 @@ describe('InviteMembers - Validation', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], INVALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
@@ -60,8 +60,8 @@ describe('InviteMembers - Validation', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], INVALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
@@ -85,8 +85,8 @@ describe('InviteMembers - Validation', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], INVALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));
await expect(
@@ -149,8 +149,8 @@ describe('InviteMembers - Validation', () => {
screen.findByText('Please select roles for team members'),
).resolves.toBeInTheDocument();
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await waitFor(() => {
expect(
@@ -204,8 +204,8 @@ describe('InviteMembers - Validation', () => {
const emailInputs = screen.getAllByPlaceholderText('e.g. john@signoz.io');
await user.type(emailInputs[0], VALID_EMAIL);
await user.click(screen.getAllByText('Select roles')[0]);
await user.click(await screen.findByTitle('Viewer'));
await user.click(screen.getAllByText('Select role')[0]);
await user.click(await screen.findByText('Viewer'));
await user.click(screen.getByTestId('submit-btn'));

View File

@@ -3,7 +3,7 @@ import { ReactNode } from 'react';
export interface InviteMemberRow {
id: string;
email: string;
roleIds: string[];
roleId: string;
}
export interface InviteResult {
@@ -38,7 +38,7 @@ export interface UseInviteMembersReturn {
addRow: () => void;
removeRow: (id: string) => void;
updateEmail: (id: string, email: string) => void;
updateRole: (id: string, roleIds: string[]) => void;
updateRole: (id: string, roleId: string | undefined) => void;
reset: () => void;
submit: () => Promise<InviteResult[]>;

View File

@@ -18,11 +18,11 @@ import {
const createEmptyRow = (): InviteMemberRow => ({
id: uuid(),
email: '',
roleIds: [],
roleId: '',
});
const isRowTouched = (row: InviteMemberRow): boolean =>
row.email.trim() !== '' || row.roleIds.length > 0;
row.email.trim() !== '' || row.roleId !== '';
export function useInviteMembers(
options: UseInviteMembersOptions = {},
@@ -78,7 +78,7 @@ export function useInviteMembers(
touched.forEach((row) => {
const emailValid = EMAIL_REGEX.test(row.email);
const roleValid = row.roleIds.length > 0;
const roleValid = row.roleId !== '';
if (!emailValid || !row.email) {
isValid = false;
@@ -139,12 +139,12 @@ export function useInviteMembers(
);
const updateRole = useCallback(
(id: string, roleIds: string[]): void => {
(id: string, roleId: string | undefined): void => {
setRows((prev) => {
const updated = cloneDeep(prev);
const row = updated.find((r) => r.id === id);
if (row) {
row.roleIds = roleIds;
row.roleId = roleId ?? '';
}
return updated;
});
@@ -187,7 +187,7 @@ export function useInviteMembers(
await createUser({
email: row.email.trim(),
frontendBaseUrl: getBaseUrl(),
userRoles: row.roleIds.map((id) => ({ id })),
userRoles: [{ id: row.roleId }],
});
results.push({ email: row.email, success: true });
} catch (err) {

View File

@@ -1,49 +0,0 @@
.header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
gap: 8px;
}
.tooltipContent {
--tooltip-z-index: 2100;
}
.dropdownContent {
--dropdown-menu-content-z-index: 2100;
}
.leftSection {
display: flex;
align-items: center;
gap: 8px;
}
.divider {
height: 16px;
margin: 0;
}
.timestamp {
font-family: 'Geist Mono', monospace;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-normal);
color: var(--l1-foreground);
letter-spacing: -0.07px;
}
.actions {
display: flex;
align-items: center;
gap: 8px;
}
.arrows {
display: flex;
align-items: center;
gap: 2px;
padding: 2px 6px;
border-radius: 6px;
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.1);
}

View File

@@ -1,153 +0,0 @@
import { Button } from '@signozhq/ui/button';
import { Divider } from '@signozhq/ui/divider';
import { DropdownMenuSimple as Dropdown } from '@signozhq/ui/dropdown-menu';
import { Typography } from '@signozhq/ui/typography';
import { TooltipSimple } from '@signozhq/ui/tooltip';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
import { toast } from '@signozhq/ui/sonner';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import {
ChevronDown,
ChevronUp,
Compass,
Copy,
Ellipsis,
Link,
} from '@signozhq/icons';
import { useTimezone } from 'providers/Timezone';
import { ILog } from 'types/api/logs/log';
import { MouseEvent, MouseEventHandler } from 'react';
import { useCopyToClipboard } from 'react-use';
import styles from './LogDetailsHeader.module.scss';
const TOOLTIP_CONTENT_PROPS = { className: styles.tooltipContent };
interface LogDetailsHeaderProps {
log: ILog;
onNavigatePrev: () => void;
onNavigateNext: () => void;
isPrevDisabled: boolean;
isNextDisabled: boolean;
showOpenInExplorer?: boolean;
onOpenInExplorer?: MouseEventHandler;
}
function LogDetailsHeader({
log,
onNavigatePrev,
onNavigateNext,
isPrevDisabled,
isNextDisabled,
showOpenInExplorer = false,
onOpenInExplorer,
}: LogDetailsHeaderProps): JSX.Element {
const [, copyToClipboard] = useCopyToClipboard();
const { onLogCopy } = useCopyLogLink(log?.id);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const handleCopyLog = (): void => {
copyToClipboard(aggregateAttributesResourcesToString(log));
toast.success('Copied to clipboard', { position: 'bottom-right' });
};
const menuItems = [
{
key: 'copy-log',
label: 'Copy log',
icon: <Copy size={14} />,
onClick: handleCopyLog,
},
{
key: 'copy-link',
label: 'Copy link to log',
icon: <Link size={14} />,
onClick: (): void => onLogCopy(),
},
];
return (
<div className={styles.header} data-log-detail-ignore="true">
<div className={styles.leftSection}>
<Divider type="vertical" className={styles.divider} />
<Typography.Text
className={styles.timestamp}
data-testid="log-details-header-timestamp"
>
{formatTimezoneAdjustedTimestamp(
log.date ?? log.timestamp,
DATE_TIME_FORMATS.DASH_DATETIME,
)}
</Typography.Text>
</div>
<div className={styles.actions}>
{showOpenInExplorer && (
<Button
variant="outlined"
color="secondary"
prefix={<Compass size={16} />}
onClick={onOpenInExplorer}
>
Open in Explorer
</Button>
)}
<Dropdown
menu={{ items: menuItems }}
align="end"
className={styles.dropdownContent}
onClick={(e: MouseEvent): void => e.stopPropagation()}
>
<Button
variant="link"
color="secondary"
prefix={<Ellipsis size={16} />}
data-testid="log-details-header-menu"
/>
</Dropdown>
<div className={styles.arrows}>
<TooltipSimple
title="Move to previous log"
side="top"
open={isPrevDisabled ? false : undefined}
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
>
<Button
variant="outlined"
color="secondary"
prefix={<ChevronUp size={14} />}
disabled={isPrevDisabled}
onClick={onNavigatePrev}
data-testid="log-details-header-prev"
/>
</TooltipSimple>
<TooltipSimple
title="Move to next log"
side="top"
open={isNextDisabled ? false : undefined}
tooltipContentProps={TOOLTIP_CONTENT_PROPS}
>
<Button
variant="outlined"
color="secondary"
prefix={<ChevronDown size={14} />}
disabled={isNextDisabled}
onClick={onNavigateNext}
data-testid="log-details-header-next"
/>
</TooltipSimple>
</div>
</div>
</div>
);
}
LogDetailsHeader.defaultProps = {
showOpenInExplorer: false,
onOpenInExplorer: undefined,
};
export default LogDetailsHeader;

View File

@@ -1,52 +0,0 @@
import { useCallback, useMemo } from 'react';
import { ILog } from 'types/api/logs/log';
interface UseLogNavigationParams {
logs?: ILog[];
activeLogId: string;
onNavigateLog?: (log: ILog) => void;
onScrollToLog?: (id: string) => void;
}
interface UseLogNavigationReturn {
goToPrev: () => void;
goToNext: () => void;
isPrevDisabled: boolean;
isNextDisabled: boolean;
}
export function useLogNavigation({
logs,
activeLogId,
onNavigateLog,
onScrollToLog,
}: UseLogNavigationParams): UseLogNavigationReturn {
const currentIndex = useMemo(
() => logs?.findIndex((l) => l.id === activeLogId) ?? -1,
[logs, activeLogId],
);
const canNavigate = !!logs?.length && !!onNavigateLog && currentIndex !== -1;
const isPrevDisabled = !canNavigate || currentIndex <= 0;
const isNextDisabled = !canNavigate || currentIndex >= (logs?.length ?? 0) - 1;
const goToPrev = useCallback((): void => {
if (isPrevDisabled || !logs) {
return;
}
const prev = logs[currentIndex - 1];
onNavigateLog?.(prev);
onScrollToLog?.(prev.id);
}, [isPrevDisabled, logs, currentIndex, onNavigateLog, onScrollToLog]);
const goToNext = useCallback((): void => {
if (isNextDisabled || !logs) {
return;
}
const next = logs[currentIndex + 1];
onNavigateLog?.(next);
onScrollToLog?.(next.id);
}, [isNextDisabled, logs, currentIndex, onNavigateLog, onScrollToLog]);
return { goToPrev, goToNext, isPrevDisabled, isNextDisabled };
}

View File

@@ -1,46 +0,0 @@
.highlights {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px 16px;
padding: 12px 0;
// Constrain each KeyValueLabel (the grid items) to its cell.
:global(.key-value-label) {
width: auto;
min-width: 0;
overflow: hidden;
}
}
.valueBadge {
--badge-font-size: 13px;
box-sizing: border-box;
max-width: 100%;
min-width: 0;
}
// Truncating text inside a badge
.badgeText {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.serviceDot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent-forest);
flex-shrink: 0;
margin-right: 4px;
}
.traceLink {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--accent-primary);
}

View File

@@ -1,36 +0,0 @@
import KeyValueLabel from 'periscope/components/KeyValueLabel';
import { ILog } from 'types/api/logs/log';
import { LOG_HIGHLIGHTS } from './config';
import styles from './LogHighlights.module.scss';
interface LogHighlightsProps {
log: ILog;
}
function LogHighlights({ log }: LogHighlightsProps): JSX.Element | null {
const fields = LOG_HIGHLIGHTS.map((field) => ({
key: field.key,
label: field.label,
value: field.render(log),
})).filter((field) => field.value != null);
if (fields.length === 0) {
return null;
}
return (
<div className={styles.highlights} data-testid="log-details-highlights">
{fields.map((field) => (
<KeyValueLabel
key={field.key}
badgeKey={field.label}
badgeValue={field.value}
direction="column"
/>
))}
</div>
);
}
export default LogHighlights;

View File

@@ -1,23 +0,0 @@
import { Link } from 'react-router-dom';
import styles from './LogHighlights.module.scss';
interface TraceIdFieldProps {
traceId: string;
}
function TraceIdField({ traceId }: TraceIdFieldProps): JSX.Element {
return (
<Link
to={{ pathname: `/trace/${traceId}` }}
target="_blank"
rel="noreferrer"
className={styles.traceLink}
title={traceId}
>
{traceId}
</Link>
);
}
export default TraceIdField;

View File

@@ -1,102 +0,0 @@
import { ReactNode } from 'react';
import { Badge, BadgeColor } from '@signozhq/ui/badge';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import { getLogIndicatorType } from 'components/Logs/LogStateIndicator/utils';
import { ILog } from 'types/api/logs/log';
import styles from './LogHighlights.module.scss';
import TraceIdField from './TraceIdField';
// Severity badge color mirrors the LogStateIndicator bar
const SEVERITY_COLOR: Record<string, BadgeColor> = {
[LogType.TRACE]: 'forest',
[LogType.DEBUG]: 'aqua',
[LogType.INFO]: 'robin',
[LogType.WARN]: 'amber',
[LogType.ERROR]: 'cherry',
[LogType.FATAL]: 'sakura',
};
export interface LogHighlightConfig {
key: string;
label: string;
render: (log: ILog) => ReactNode | null;
}
// Resource/attribute lookup (keys like `service.name` live in resources_string,
// occasionally attributes_string). Typed loosely as these are string maps.
const getAttr = (log: ILog, key: string): string =>
(log.resources_string as unknown as Record<string, string>)?.[key] ||
(log.attributes_string as unknown as Record<string, string>)?.[key] ||
'';
const valueBadge = (
value: string,
options?: { prefix?: ReactNode; color?: BadgeColor },
): ReactNode => (
<Badge color={options?.color ?? 'vanilla'} className={styles.valueBadge}>
{options?.prefix}
<span className={styles.badgeText} title={value}>
{value}
</span>
</Badge>
);
export const LOG_HIGHLIGHTS: LogHighlightConfig[] = [
{
key: 'service',
label: 'SERVICE',
render: (log): ReactNode | null => {
const value = getAttr(log, 'service.name');
return value
? valueBadge(value, {
prefix: <span className={styles.serviceDot} />,
})
: null;
},
},
{
key: 'severity',
label: 'SEVERITY',
render: (log): ReactNode | null => {
if (!log.severity_text) {
return null;
}
return valueBadge(log.severity_text, {
color: SEVERITY_COLOR[getLogIndicatorType(log)] ?? 'vanilla',
});
},
},
{
key: 'namespace',
label: 'NAMESPACE',
render: (log): ReactNode | null => {
const value = getAttr(log, 'service.namespace');
return value ? valueBadge(value) : null;
},
},
{
key: 'environment',
label: 'ENVIRONMENT',
render: (log): ReactNode | null => {
const value = getAttr(log, 'deployment.environment');
return value ? valueBadge(value) : null;
},
},
{
key: 'traceId',
label: 'TRACE ID',
render: (log): ReactNode | null => {
const traceId = log.trace_id || log.traceId;
return traceId ? <TraceIdField traceId={traceId} /> : null;
},
},
{
key: 'spanId',
label: 'SPAN ID',
render: (log): ReactNode | null => {
const spanId = log.span_id || log.spanID;
return spanId ? valueBadge(spanId) : null;
},
},
];

View File

@@ -1,200 +0,0 @@
import { toast } from '@signozhq/ui/sonner';
import { LOCALSTORAGE } from 'constants/localStorage';
import { render, screen, userEvent } from 'tests/test-utils';
import { ILog } from 'types/api/logs/log';
import LogDetail from '..';
import { VIEW_TYPES } from '../constants';
import { LogDetailProps } from '../LogDetail.interfaces';
jest.mock('@signozhq/ui/sonner', () => ({
toast: { success: jest.fn(), error: jest.fn() },
}));
// The flag to be removed later
jest.mock('../constants', () => ({
...jest.requireActual('../constants'),
isLogDetailsV2: true,
}));
const mockLog: ILog = {
id: 'log-1',
timestamp: '2024-01-15T09:45:30Z',
date: '2024-01-15T09:45:30Z',
body: 'test log body',
severityText: 'INFO',
severityNumber: 9,
traceFlags: 0,
traceId: '',
spanID: '',
attributesString: {},
attributesInt: {},
attributesFloat: {},
resources_string: {},
scope_string: {},
attributes_string: {},
severity_text: 'INFO',
severity_number: 9,
};
const makeLog = (id: string): ILog => ({ ...mockLog, id });
function renderDrawer(props: Partial<LogDetailProps> = {}): void {
render(
<LogDetail
log={mockLog}
selectedTab={VIEW_TYPES.OVERVIEW}
onAddToQuery={jest.fn()}
onClickActionItem={jest.fn()}
onClose={jest.fn()}
{...props}
/>,
);
}
describe('LogDetail drawer — header (isLogDetailsV2)', () => {
afterEach(() => {
jest.clearAllMocks();
localStorage.clear();
});
it('renders the revamped header when a log is provided', () => {
renderDrawer();
expect(screen.getByTestId('log-details-header-menu')).toBeInTheDocument();
expect(screen.getByTestId('log-details-header-prev')).toBeInTheDocument();
expect(screen.getByTestId('log-details-header-next')).toBeInTheDocument();
});
it('shows the log timestamp formatted (DASH_DATETIME) in the header', () => {
// Pin the timezone to UTC so the formatted output is deterministic across
// machines/CI (Jest doesn't fix a TZ).
localStorage.setItem(LOCALSTORAGE.PREFERRED_TIMEZONE, 'UTC');
renderDrawer();
// mockLog date is 2024-01-15T09:45:30Z → DASH_DATETIME in UTC.
expect(screen.getByTestId('log-details-header-timestamp')).toHaveTextContent(
'Jan 15, 2024 ⎯ 09:45:30',
);
});
it('copies the log link from the ⋯ menu', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderDrawer();
await user.click(screen.getByTestId('log-details-header-menu'));
await user.click(await screen.findByText('Copy link to log'));
expect(toast.success).toHaveBeenCalled();
});
it('copies the log from the ⋯ menu', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderDrawer();
await user.click(screen.getByTestId('log-details-header-menu'));
await user.click(await screen.findByText('Copy log'));
expect(toast.success).toHaveBeenCalledWith('Copied to clipboard', {
position: 'bottom-right',
});
});
it('shows "Open in Explorer" when a handleOpenInExplorer handler is provided', () => {
renderDrawer({ handleOpenInExplorer: jest.fn() });
expect(screen.getByText('Open in Explorer')).toBeInTheDocument();
});
it('hides "Open in Explorer" when no handleOpenInExplorer handler is provided', () => {
renderDrawer();
expect(screen.queryByText('Open in Explorer')).not.toBeInTheDocument();
});
it('renders Highlights for fields present on the log, omitting absent ones', () => {
const logWithMeta = {
...mockLog,
severity_text: 'ERROR',
trace_id: 'trace-abc',
resources_string: {
'service.name': 'checkout',
'deployment.environment': 'production',
},
} as unknown as ILog;
renderDrawer({ log: logWithMeta });
const highlights = screen.getByTestId('log-details-highlights');
expect(highlights).toHaveTextContent('SEVERITY');
expect(highlights).toHaveTextContent('ERROR');
expect(highlights).toHaveTextContent('SERVICE');
expect(highlights).toHaveTextContent('checkout');
expect(highlights).toHaveTextContent('ENVIRONMENT');
expect(highlights).toHaveTextContent('production');
expect(highlights).toHaveTextContent('TRACE ID');
// Absent fields are omitted (no namespace / span id on this log).
expect(highlights).not.toHaveTextContent('NAMESPACE');
expect(highlights).not.toHaveTextContent('SPAN ID');
});
it('links the trace id highlight to the trace detail in a new tab', () => {
const logWithTrace = {
...mockLog,
trace_id: 'trace-abc',
} as unknown as ILog;
renderDrawer({ log: logWithTrace });
const link = screen.getByRole('link', { name: 'trace-abc' });
expect(link).toHaveAttribute('target', '_blank');
expect(link.getAttribute('href')).toContain('/trace/trace-abc');
});
it('navigates to the next / previous log with the Down / Up arrow keys', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const logs = [makeLog('log-0'), makeLog('log-1'), makeLog('log-2')];
const onNavigateLog = jest.fn();
const onScrollToLog = jest.fn();
// Active log is the middle one so both directions are available.
renderDrawer({ log: logs[1], logs, onNavigateLog, onScrollToLog });
await user.keyboard('{ArrowDown}');
expect(onNavigateLog).toHaveBeenLastCalledWith(logs[2]);
expect(onScrollToLog).toHaveBeenLastCalledWith('log-2');
await user.keyboard('{ArrowUp}');
expect(onNavigateLog).toHaveBeenLastCalledWith(logs[0]);
expect(onScrollToLog).toHaveBeenLastCalledWith('log-0');
});
it('does not navigate past the first log on ArrowUp', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const logs = [makeLog('log-0'), makeLog('log-1')];
const onNavigateLog = jest.fn();
renderDrawer({ log: logs[0], logs, onNavigateLog });
await user.keyboard('{ArrowUp}');
expect(onNavigateLog).not.toHaveBeenCalled();
});
it('navigates via the header up / down buttons and disables them at boundaries', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const logs = [makeLog('log-0'), makeLog('log-1')];
const onNavigateLog = jest.fn();
// Active log is the first one.
renderDrawer({ log: logs[0], logs, onNavigateLog });
expect(screen.getByTestId('log-details-header-prev')).toBeDisabled();
expect(screen.getByTestId('log-details-header-next')).toBeEnabled();
await user.click(screen.getByTestId('log-details-header-next'));
expect(onNavigateLog).toHaveBeenLastCalledWith(logs[1]);
});
});

View File

@@ -1,10 +1,3 @@
import getLocalStorage from 'api/browser/localstorage/get';
import { LOCALSTORAGE } from 'constants/localStorage';
// Temp feature flag before actual roll-out
export const isLogDetailsV2 =
getLocalStorage(LOCALSTORAGE.LOG_DETAILS_V2) === 'true';
export const VIEW_TYPES = {
OVERVIEW: 'OVERVIEW',
JSON: 'JSON',

View File

@@ -8,9 +8,7 @@ import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { Divider } from '@signozhq/ui/divider';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import LogStateIndicator, {
LogType,
} from 'components/Logs/LogStateIndicator/LogStateIndicator';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils';
import { FeatureKeys } from 'constants/features';
@@ -25,7 +23,6 @@ import {
} from 'container/LogDetailedView/utils';
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
import { useOptionsMenu } from 'container/OptionsMenu';
import { FontSize } from 'container/OptionsMenu/types';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
@@ -51,11 +48,8 @@ import { ILogBody } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { isLogDetailsV2, RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
import LogDetailsHeader from './LogDetailsHeader/LogDetailsHeader';
import { useLogNavigation } from './LogDetailsHeader/useLogNavigation';
import LogHighlights from './LogHighlights/LogHighlights';
import './LogDetails.styles.scss';
@@ -102,8 +96,7 @@ function LogDetailInner({
target.closest('[data-log-detail-ignore="true"]') ||
target.closest('.cm-tooltip-autocomplete') ||
target.closest('.drawer-popover') ||
target.closest('.query-status-popover') ||
target.closest('[data-radix-popper-content-wrapper]')
target.closest('.query-status-popover')
) {
return;
}
@@ -119,30 +112,49 @@ function LogDetailInner({
};
}, [onClose]);
const { goToPrev, goToNext, isPrevDisabled, isNextDisabled } =
useLogNavigation({
logs,
activeLogId: log.id,
onNavigateLog,
onScrollToLog,
});
// Keyboard navigation - handle up/down arrow keys. Only listen in the OVERVIEW
// tab so we don't hijack arrow keys from the JSON editor / context view.
// Keyboard navigation - handle up/down arrow keys
// Only listen when in OVERVIEW tab
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
if (selectedView !== VIEW_TYPES.OVERVIEW) {
return undefined;
if (
!logs ||
!onNavigateLog ||
logs.length === 0 ||
selectedView !== VIEW_TYPES.OVERVIEW
) {
return;
}
const handleKeyDown = (e: KeyboardEvent): void => {
const currentIndex = logs.findIndex((l) => l.id === log.id);
if (currentIndex === -1) {
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
e.stopPropagation();
goToPrev();
// Navigate to previous log
if (currentIndex > 0) {
const prevLog = logs[currentIndex - 1];
onNavigateLog(prevLog);
// Trigger scroll to the log element
if (onScrollToLog) {
onScrollToLog(prevLog.id);
}
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
e.stopPropagation();
goToNext();
// Navigate to next log
if (currentIndex < logs.length - 1) {
const nextLog = logs[currentIndex + 1];
onNavigateLog(nextLog);
// Trigger scroll to the log element
if (onScrollToLog) {
onScrollToLog(nextLog.id);
}
}
}
};
@@ -150,7 +162,7 @@ function LogDetailInner({
return (): void => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [selectedView, goToPrev, goToNext]);
}, [log.id, logs, onNavigateLog, onScrollToLog, selectedView]);
const listQuery = useMemo(() => {
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) {
@@ -291,6 +303,33 @@ function LogDetailInner({
};
const logType = log?.attributes_string?.log_level || LogType.INFO;
const currentLogIndex = logs ? logs.findIndex((l) => l.id === log.id) : -1;
const isPrevDisabled =
!logs || !onNavigateLog || logs.length === 0 || currentLogIndex <= 0;
const isNextDisabled =
!logs ||
!onNavigateLog ||
logs.length === 0 ||
currentLogIndex === logs.length - 1;
type HandleNavigateLogParams = {
direction: 'next' | 'previous';
};
const handleNavigateLog = ({ direction }: HandleNavigateLogParams): void => {
if (!logs || !onNavigateLog || currentLogIndex === -1) {
return;
}
if (direction === 'previous' && !isPrevDisabled) {
const prevLog = logs[currentLogIndex - 1];
onNavigateLog(prevLog);
onScrollToLog?.(prevLog.id);
} else if (direction === 'next' && !isNextDisabled) {
const nextLog = logs[currentLogIndex + 1];
onNavigateLog(nextLog);
onScrollToLog?.(nextLog.id);
}
};
return (
<Drawer
@@ -299,69 +338,57 @@ function LogDetailInner({
maskClosable={false}
getContainer={getContainer}
title={
isLogDetailsV2 ? (
<LogDetailsHeader
log={log}
onNavigatePrev={goToPrev}
onNavigateNext={goToNext}
isPrevDisabled={isPrevDisabled}
isNextDisabled={isNextDisabled}
showOpenInExplorer={!!handleOpenInExplorer}
onOpenInExplorer={handleOpenInExplorer}
/>
) : (
<div className="log-detail-drawer__title" data-log-detail-ignore="true">
<div className="log-detail-drawer__title-left">
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
<Typography.Text className="title">Log details</Typography.Text>
</div>
<div className="log-detail-drawer__title-right">
<div className="log-arrows">
<Tooltip
title={isPrevDisabled ? '' : 'Move to previous log'}
placement="top"
mouseLeaveDelay={0}
>
<Button
variant="outlined"
color="secondary"
prefix={<ChevronUp size={14} />}
className="log-arrow-btn log-arrow-btn-up"
disabled={isPrevDisabled}
onClick={goToPrev}
/>
</Tooltip>
<Tooltip
title={isNextDisabled ? '' : 'Move to next log'}
placement="top"
mouseLeaveDelay={0}
>
<Button
variant="outlined"
color="secondary"
prefix={<ChevronDown size={14} />}
className="log-arrow-btn log-arrow-btn-down"
disabled={isNextDisabled}
onClick={goToNext}
/>
</Tooltip>
</div>
{handleOpenInExplorer && (
<div>
<Button
variant="outlined"
color="secondary"
prefix={<Compass size={16} />}
className="open-in-explorer-btn"
onClick={handleOpenInExplorer}
>
Open in Explorer
</Button>
</div>
)}
</div>
<div className="log-detail-drawer__title" data-log-detail-ignore="true">
<div className="log-detail-drawer__title-left">
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
<Typography.Text className="title">Log details</Typography.Text>
</div>
)
<div className="log-detail-drawer__title-right">
<div className="log-arrows">
<Tooltip
title={isPrevDisabled ? '' : 'Move to previous log'}
placement="top"
mouseLeaveDelay={0}
>
<Button
variant="outlined"
color="secondary"
prefix={<ChevronUp size={14} />}
className="log-arrow-btn log-arrow-btn-up"
disabled={isPrevDisabled}
onClick={(): void => handleNavigateLog({ direction: 'previous' })}
/>
</Tooltip>
<Tooltip
title={isNextDisabled ? '' : 'Move to next log'}
placement="top"
mouseLeaveDelay={0}
>
<Button
variant="outlined"
color="secondary"
prefix={<ChevronDown size={14} />}
className="log-arrow-btn log-arrow-btn-down"
disabled={isNextDisabled}
onClick={(): void => handleNavigateLog({ direction: 'next' })}
/>
</Tooltip>
</div>
{handleOpenInExplorer && (
<div>
<Button
variant="outlined"
color="secondary"
prefix={<Compass size={16} />}
className="open-in-explorer-btn"
onClick={handleOpenInExplorer}
>
Open in Explorer
</Button>
</div>
)}
</div>
</div>
}
placement="right"
onClose={drawerCloseHandler}
@@ -380,15 +407,7 @@ function LogDetailInner({
data-testid="log-detail-drawer"
>
<div className="log-detail-drawer__log">
{isLogDetailsV2 ? (
<LogStateIndicator
severityText={log.severity_text}
severityNumber={log.severity_number}
fontSize={options?.fontSize ?? FontSize.MEDIUM}
/>
) : (
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
)}
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip
title={removeEscapeCharacters(logBody)}
placement="left"
@@ -400,8 +419,6 @@ function LogDetailInner({
<div className="log-overflow-shadow">&nbsp;</div>
</div>
{isLogDetailsV2 && <LogHighlights log={log} />}
<div className="tabs-and-search">
<ToggleGroupSimple
type="single"
@@ -466,25 +483,22 @@ function LogDetailInner({
</Tooltip>
)}
{/* V2 moves copy actions into the header ⋯ menu */}
{!isLogDetailsV2 && (
<Tooltip
title={selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'}
placement="topLeft"
aria-label={
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
}
mouseLeaveDelay={0}
>
<Button
variant="link"
color="secondary"
size="sm"
prefix={<Copy size={12} />}
onClick={selectedView === VIEW_TYPES.JSON ? handleJSONCopy : onLogCopy}
/>
</Tooltip>
)}
<Tooltip
title={selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'}
placement="topLeft"
aria-label={
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
}
mouseLeaveDelay={0}
>
<Button
variant="link"
color="secondary"
size="sm"
prefix={<Copy size={12} />}
onClick={selectedView === VIEW_TYPES.JSON ? handleJSONCopy : onLogCopy}
/>
</Tooltip>
</div>
</div>
{isFilterVisible && contextQuery?.builder.queryData[0] && (

View File

@@ -10,10 +10,6 @@ jest.mock('providers/Timezone', () => ({
}),
}));
jest.mock('providers/App/App', () => ({
useAppContext: (): { featureFlags: [] } => ({ featureFlags: [] }),
}));
const field = (name: string, type = ''): IField => ({
name,
type,

View File

@@ -2,15 +2,13 @@ import type { ReactElement } from 'react';
import { useMemo } from 'react';
import TanStackTable from 'components/TanStackTableView';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { FeatureKeys } from 'constants/features';
import {
getBodyDisplayString,
getSanitizedLogBody,
} from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import { buildCompositeKey } from 'container/OptionsMenu/utils';
import { getLogFieldValue } from 'lib/logs/flatLogData';
import { useAppContext } from 'providers/App/App';
import { FlatLogData } from 'lib/logs/flatLogData';
import { useTimezone } from 'providers/Timezone';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
@@ -28,10 +26,6 @@ export function useLogsTableColumns({
fontSize,
}: UseLogsTableColumnsProps): TableColumnDef<ILog>[] {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const { featureFlags } = useAppContext();
const isBodyJsonEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_JSON_BODY)
?.active || false;
return useMemo<TableColumnDef<ILog>[]>(() => {
const stateIndicatorCol: TableColumnDef<ILog> = {
@@ -94,8 +88,7 @@ export function useLogsTableColumns({
const makeUserFieldCol = (f: IField): TableColumnDef<ILog> => ({
id: buildCompositeKey(f.name, f.type),
header: f.name,
accessorFn: (log): unknown =>
getLogFieldValue(log, f.name, isBodyJsonEnabled),
accessorFn: (log): unknown => FlatLogData(log)[f.name],
enableRemove: true,
width: { min: 192 },
cell: ({ value }): ReactElement => (
@@ -122,5 +115,5 @@ export function useLogsTableColumns({
.filter((c): c is TableColumnDef<ILog> => c !== null);
return [stateIndicatorCol, ...fieldCols];
}, [fields, fontSize, formatTimezoneAdjustedTimestamp, isBodyJsonEnabled]);
}, [fields, fontSize, formatTimezoneAdjustedTimestamp]);
}

View File

@@ -59,7 +59,6 @@ import {
dedupeOptionsByLabel,
getFieldContextPrefix,
getRecentOptions,
isSupportedFunction,
renderRecentDeleteButton,
} from './utils';
@@ -184,14 +183,15 @@ function QuerySearch({
isProgrammaticChangeRef.current = true;
}
const changes = view.state.changes({
from: 0,
to: currentValue.length,
insert: value,
});
view.dispatch({
changes,
selection: { anchor: changes.newLength },
changes: {
from: 0,
to: currentValue.length,
insert: value,
},
selection: {
anchor: value.length,
},
});
},
[],
@@ -1276,13 +1276,11 @@ function QuerySearch({
}
if (queryContext.isInFunction) {
options = Object.values(QUERY_BUILDER_FUNCTIONS)
.filter((option) => isSupportedFunction(option, dataSource))
.map((option) => ({
label: option,
apply: `${option}()`,
type: 'function',
}));
options = Object.values(QUERY_BUILDER_FUNCTIONS).map((option) => ({
label: option,
apply: `${option}()`,
type: 'function',
}));
// Add space after selection for functions
const optionsWithSpace = addSpaceToOptions(options);

View File

@@ -1,12 +1,8 @@
import { QUERY_BUILDER_FUNCTIONS } from 'constants/antlrQueryConstants';
import { DataSource } from 'types/common/queryBuilder';
import {
combineInitialAndUserExpression,
dedupeOptionsByLabel,
getFieldContextPrefix,
getUserExpressionFromCombined,
isSupportedFunction,
} from '../utils';
describe('entityLogsExpression', () => {
@@ -122,19 +118,3 @@ describe('dedupeOptionsByLabel', () => {
expect(dedupeOptionsByLabel([])).toStrictEqual([]);
});
});
describe('isSupportedFunction', () => {
const { HASANY, SEARCH } = QUERY_BUILDER_FUNCTIONS;
it('allows the has family on every signal', () => {
[DataSource.LOGS, DataSource.TRACES, DataSource.METRICS].forEach((signal) => {
expect(isSupportedFunction(HASANY, signal)).toBe(true);
});
});
it('allows search on logs only', () => {
expect(isSupportedFunction(SEARCH, DataSource.LOGS)).toBe(true);
expect(isSupportedFunction(SEARCH, DataSource.TRACES)).toBe(false);
expect(isSupportedFunction(SEARCH, DataSource.METRICS)).toBe(false);
});
});

View File

@@ -1,7 +1,6 @@
import { closeCompletion, startCompletion } from '@codemirror/autocomplete';
import type { Completion } from '@codemirror/autocomplete';
import type { EditorView } from '@uiw/react-codemirror';
import { QUERY_BUILDER_FUNCTIONS } from 'constants/antlrQueryConstants';
import dayjs from 'dayjs';
import { normalizeFilterExpression } from 'lib/recentQueries/normalize';
import * as recentQueriesStore from 'lib/recentQueries/recentQueriesStore';
@@ -16,15 +15,6 @@ import {
RECENTS_SECTION,
} from './constants';
// search() lives in the logs condition builder only; traces and metrics reject it
// as an unsupported operator. Every other function is implemented for all signals.
export function isSupportedFunction(
functionName: string,
signal: SignalType,
): boolean {
return functionName !== QUERY_BUILDER_FUNCTIONS.SEARCH || signal === 'logs';
}
export interface FieldContextPrefixMatch {
context: string;
remainder: string;

View File

@@ -301,66 +301,6 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
dispatchSpy.mockRestore();
});
it('does not crash when the expression contains CRLF line breaks (issue #5869)', async () => {
const dispatchSpy = jest.spyOn(EditorView.prototype, 'dispatch');
const onChange = jest.fn() as jest.MockedFunction<(v: string) => void>;
const initialExpression = "service.name = 'frontend'";
// Filtering on a multi-line log value (CRLF) used to throw
// "RangeError: Selection points outside of document".
const crlfExpression = "body CONTAINS 'line1\r\nline2\r\nline3'";
const baseQueryData = {
...initialQueriesMap.logs.builder.queryData[0],
filter: { expression: initialExpression },
};
const { rerender } = render(
<QuerySearch
onChange={onChange}
queryData={baseQueryData}
dataSource={DataSource.LOGS}
/>,
);
await waitFor(
() => {
const editorContent = document.querySelector(
CM_EDITOR_SELECTOR,
) as HTMLElement;
expect(editorContent.textContent || '').toBe(initialExpression);
},
{ timeout: 3000 },
);
rerender(
<QuerySearch
onChange={onChange}
queryData={{ ...baseQueryData, filter: { expression: crlfExpression } }}
dataSource={DataSource.LOGS}
/>,
);
// The programmatic replace dispatched without throwing, and the selection anchor
// stayed within the CRLF-normalized document (the bug set it past the end).
await waitFor(() => {
const spec = dispatchSpy.mock.calls
.map(
(call) =>
call[0] as {
selection?: { anchor?: number };
changes?: { newLength?: number };
},
)
.find((s) => s?.selection?.anchor != null && s?.changes?.newLength != null);
expect(spec).toBeDefined();
expect(spec?.selection?.anchor).toBeLessThanOrEqual(
spec?.changes?.newLength as number,
);
});
dispatchSpy.mockRestore();
});
it('fetches key suggestions for metrics even without aggregateAttribute.key when showFilterSuggestionsWithoutMetric is true', async () => {
const mockedGetKeys = getKeySuggestions as jest.MockedFunction<
typeof getKeySuggestions

View File

@@ -1,280 +0,0 @@
import {
completionStatus,
currentCompletions,
startCompletion,
} from '@codemirror/autocomplete';
import { EditorView } from '@uiw/react-codemirror';
import { initialQueriesMap } from 'constants/queryBuilder';
import * as recentQueriesStore from 'lib/recentQueries/recentQueriesStore';
import { fireEvent, render, userEvent, waitFor } from 'tests/test-utils';
import { DataSource } from 'types/common/queryBuilder';
import { RECENTS_DISPLAY_CAP, RECENTS_SECTION } from '../QuerySearch/constants';
import QuerySearch from '../QuerySearch/QuerySearch';
import { mockCodeMirrorDomApis } from './codemirrorDomMocks';
const CM_ROOT_SELECTOR = '.cm-editor';
const CM_EDITOR_SELECTOR = '.cm-editor .cm-content';
const TOOLTIP_SELECTOR = '.cm-tooltip-autocomplete';
const COMPLETION_LABEL_SELECTOR = '.cm-completionLabel';
const DELETE_BUTTON_SELECTOR = '.cm-recent-delete';
const FRONTEND_FILTER = "service.name = 'frontend'";
const STATUS_CODE_FILTER = "http.status_code = '500'";
const TRACES_FILTER = "name = 'HTTP GET'";
beforeAll(() => {
mockCodeMirrorDomApis();
});
jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: (): boolean => false,
}));
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): { dashboardData: undefined } => ({
dashboardData: undefined,
}),
}));
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
getKeySuggestions: jest.fn().mockResolvedValue({
data: { data: { keys: {} } },
}),
}));
jest.mock('api/querySuggestions/getValueSuggestion', () => ({
getValueSuggestions: jest.fn().mockResolvedValue({
data: { data: { values: { stringValues: [], numberValues: [] } } },
}),
}));
function renderLogsSearch(onChange: (value: string) => void = jest.fn()): void {
render(
<QuerySearch
onChange={onChange}
queryData={initialQueriesMap.logs.builder.queryData[0]}
dataSource={DataSource.LOGS}
/>,
);
}
function saveLogsRecent(expression: string): void {
recentQueriesStore.save({ signal: 'logs', filter: { expression } });
}
function getEditorView(): EditorView | null {
const root = document.querySelector<HTMLElement>(CM_ROOT_SELECTOR);
return root ? EditorView.findFromDOM(root) : null;
}
function getDocText(): string {
return getEditorView()?.state.doc.toString() ?? '';
}
function isCompletionOpen(): boolean {
const view = getEditorView();
return !!view && completionStatus(view.state) === 'active';
}
// Reads recents from completion state, not the tooltip: the tooltip is a later render
// pass over this same state, so going to the source drops a layer of timing.
function getRecentLabels(): string[] {
const view = getEditorView();
if (!view) {
return [];
}
return currentCompletions(view.state)
.filter((completion) => completion.section === RECENTS_SECTION)
.map((completion) => completion.label);
}
async function renderAndFocus(
onChange: (value: string) => void = jest.fn(),
): Promise<HTMLElement> {
renderLogsSearch(onChange);
const editor = await waitFor(
() => {
const element = document.querySelector(CM_EDITOR_SELECTOR);
expect(element).toBeInTheDocument();
return element as HTMLElement;
},
{ timeout: 2000 },
);
await userEvent.click(editor);
return editor;
}
function openRecents(): Promise<void> {
return waitFor(
() => {
const view = getEditorView();
if (view && !isCompletionOpen()) {
startCompletion(view);
}
expect(getRecentLabels().length).toBeGreaterThan(0);
},
{ timeout: 3000 },
);
}
describe('QuerySearch recent searches', () => {
beforeEach(() => {
recentQueriesStore.useRecentQueriesStore.setState({ buckets: {} });
localStorage.clear();
});
it('shows a saved recent query under "Recent searches" on focus', async () => {
saveLogsRecent(FRONTEND_FILTER);
await renderAndFocus();
await openRecents();
await waitFor(
() => {
expect(getRecentLabels()).toStrictEqual([FRONTEND_FILTER]);
},
{ timeout: 3000 },
);
const view = getEditorView() as EditorView;
const [recent] = currentCompletions(view.state);
expect(recent.section).toBe(RECENTS_SECTION);
});
it('filters recents by substring as the user types', async () => {
saveLogsRecent(FRONTEND_FILTER);
saveLogsRecent(STATUS_CODE_FILTER);
const editor = await renderAndFocus();
await openRecents();
await userEvent.type(editor, 'status_code');
await waitFor(
() => {
expect(getRecentLabels()).toStrictEqual([STATUS_CODE_FILTER]);
},
{ timeout: 3000 },
);
});
it('does not surface recents saved under a different signal', async () => {
recentQueriesStore.save({
signal: 'traces',
filter: { expression: TRACES_FILTER },
});
saveLogsRecent(FRONTEND_FILTER);
await renderAndFocus();
await openRecents();
await waitFor(
() => {
expect(getRecentLabels()).toStrictEqual([FRONTEND_FILTER]);
},
{ timeout: 3000 },
);
});
it('excludes a recent that exactly matches the current editor text', async () => {
const supersetFilter = `${FRONTEND_FILTER} AND ${STATUS_CODE_FILTER}`;
saveLogsRecent(FRONTEND_FILTER);
saveLogsRecent(supersetFilter);
const editor = await renderAndFocus();
await openRecents();
await userEvent.type(editor, FRONTEND_FILTER);
await waitFor(
() => {
expect(getRecentLabels()).toStrictEqual([supersetFilter]);
},
{ timeout: 3000 },
);
});
it('caps the dropdown at RECENTS_DISPLAY_CAP entries, newest first', async () => {
const filters = Array.from(
{ length: RECENTS_DISPLAY_CAP + 1 },
(_, index) => `attribute_${index + 1} = 'v'`,
);
filters.forEach((filter) => saveLogsRecent(filter));
const expectedLabels = [...filters].reverse().slice(0, RECENTS_DISPLAY_CAP);
await renderAndFocus();
await openRecents();
await waitFor(
() => {
expect(getRecentLabels()).toStrictEqual(expectedLabels);
},
{ timeout: 3000 },
);
});
it('applies the full expression to the editor when a recent is clicked', async () => {
saveLogsRecent(FRONTEND_FILTER);
const onChange = jest.fn();
await renderAndFocus(onChange);
await openRecents();
const option = await waitFor(
() => {
const node = Array.from(
document.querySelectorAll<HTMLElement>(COMPLETION_LABEL_SELECTOR),
).find((element) => element.textContent === FRONTEND_FILTER);
expect(node).toBeDefined();
return node as HTMLElement;
},
{ timeout: 3000 },
);
await userEvent.click(option);
await waitFor(
() => {
expect(getDocText()).toBe(FRONTEND_FILTER);
},
{ timeout: 2000 },
);
expect(onChange).toHaveBeenCalledWith(FRONTEND_FILTER);
await waitFor(
() => {
expect(document.querySelector(TOOLTIP_SELECTOR)).not.toBeInTheDocument();
},
{ timeout: 2000 },
);
});
it('removes a recent from the dropdown and the store when delete is clicked', async () => {
saveLogsRecent(FRONTEND_FILTER);
await renderAndFocus();
await openRecents();
const deleteButton = await waitFor(
() => {
const button = document.querySelector(DELETE_BUTTON_SELECTOR);
expect(button).toBeInTheDocument();
return button as HTMLElement;
},
{ timeout: 3000 },
);
// fireEvent: the button preventDefaults pointerdown, which makes userEvent.click drop the mouse chain.
fireEvent.click(deleteButton);
await waitFor(
() => {
expect(recentQueriesStore.list('logs')).toHaveLength(0);
expect(getRecentLabels()).not.toContain(FRONTEND_FILTER);
},
{ timeout: 2000 },
);
expect(getDocText()).toBe('');
});
});

View File

@@ -41,7 +41,6 @@ export const QUERY_BUILDER_FUNCTIONS = {
HASANY: 'hasAny',
HASALL: 'hasAll',
HASTOKEN: 'hasToken',
SEARCH: 'search',
};
export function negateOperator(operatorOrFunction: string): string {

View File

@@ -7,6 +7,7 @@ export enum FeatureKeys {
GATEWAY = 'gateway',
PREMIUM_SUPPORT = 'premium_support',
ANOMALY_DETECTION = 'anomaly_detection',
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
USE_JSON_BODY = 'use_json_body',
ENABLE_AI_OBSERVABILITY = 'enable_ai_observability',
ENABLE_METRICS_REDUCTION = 'enable_metrics_reduction',

View File

@@ -1,32 +0,0 @@
// Code generated by scripts/semconv. DO NOT EDIT.
export type SemconvFamily = {
readonly current: string;
readonly old: readonly string[];
readonly kind: 'attribute' | 'metric';
readonly contexts: readonly string[];
readonly signals: readonly string[];
readonly applyToMetrics: readonly string[];
readonly valueMap: Readonly<Record<string, string>>;
};
export const SEMCONV_FAMILIES: readonly SemconvFamily[] = [
{
current: 'db.system.name',
old: ['db.system'],
kind: 'attribute',
contexts: [],
signals: [],
applyToMetrics: [],
valueMap: {},
},
{
current: 'deployment.environment.name',
old: ['deployment.environment'],
kind: 'attribute',
contexts: [],
signals: [],
applyToMetrics: [],
valueMap: {},
},
] as const;

View File

@@ -13,7 +13,6 @@ export enum LOCALSTORAGE {
TRACES_LIST_COLUMNS = 'TRACES_LIST_COLUMNS',
LOGS_LIST_COLUMNS = 'LOGS_LIST_COLUMNS',
LOGS_LIST_COLUMN_SIZING = 'LOGS_LIST_COLUMN_SIZING',
LOG_DETAILS_V2 = 'LOG_DETAILS_V2',
LOGGED_IN_USER_NAME = 'LOGGED_IN_USER_NAME',
LOGGED_IN_USER_EMAIL = 'LOGGED_IN_USER_EMAIL',
CHAT_SUPPORT = 'CHAT_SUPPORT',

View File

@@ -92,7 +92,6 @@ const ROUTES = {
AI_OBSERVABILITY_ATTRIBUTE_MAPPING: '/ai-observability/attribute-mapping',
AI_OBSERVABILITY_BASE: '/ai-observability',
AI_OBSERVABILITY_OVERVIEW: '/ai-observability/overview',
AI_OBSERVABILITY_EXPLORER: '/ai-observability/explorer',
AI_OBSERVABILITY_CONFIGURATION: '/ai-observability/configuration',
} as const;

View File

@@ -1,28 +1,18 @@
import CreateAlertChannels from 'container/CreateAlertChannels';
import { ChannelType } from 'container/CreateAlertChannels/config';
import { GoogleChatInitialConfig } from 'container/CreateAlertChannels/defaults';
import {
googleChatDescriptionDefaultValue,
googleChatTitleDefaultValue,
opsGenieDescriptionDefaultValue,
opsGenieMessageDefaultValue,
opsGeniePriorityDefaultValue,
pagerDutyAdditionalDetailsDefaultValue,
pagerDutyDescriptionDefaultValue,
pagerDutyDescriptionDefaultVaule,
pagerDutySeverityTextDefaultValue,
slackDescriptionDefaultValue,
slackTitleDefaultValue,
} from 'mocks-server/__mockdata__/alerts';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import {
act,
fireEvent,
render,
screen,
userEvent,
waitFor,
} from 'tests/test-utils';
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
import { testLabelInputAndHelpValue } from './testUtils';
@@ -235,7 +225,7 @@ describe('Create Alert Channel', () => {
);
expect(descriptionTextArea).toHaveTextContent(
pagerDutyDescriptionDefaultValue,
pagerDutyDescriptionDefaultVaule,
);
});
it('Should check if Severity label, info (help_pager_severity), and textbox are displayed properly', () => {
@@ -429,152 +419,5 @@ describe('Create Alert Channel', () => {
expect(descriptionTextArea).toHaveTextContent(slackDescriptionDefaultValue);
});
});
describe('Google Chat', () => {
const validWebhookUrl =
'https://chat.googleapis.com/v1/spaces/AAAA/messages?key=dummy_key&token=dummy_token';
beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.GoogleChat} />);
});
// paste instead of type: a per-keystroke re-render of the whole form
// pushes these tests past the 5s jest timeout on slower CI runners
async function fillField(
user: ReturnType<typeof userEvent.setup>,
testId: string,
value: string,
): Promise<void> {
await user.click(screen.getByTestId(testId));
await user.paste(value);
}
it('Should check if the selected item in the type dropdown has text "Google Chat"', () => {
expect(screen.getByText('Google Chat')).toBeInTheDocument();
});
it('Should check if Webhook URL label and input are displayed properly', () => {
testLabelInputAndHelpValue({
labelText: 'field_webhook_url',
testId: 'webhook-url-textbox',
});
});
it('Should check if Title contains the google chat template', () => {
expect(screen.getByTestId('title-textarea')).toHaveTextContent(
googleChatTitleDefaultValue,
);
});
it('Should check if Description contains the google chat template', () => {
expect(screen.getByTestId('description-textarea')).toHaveTextContent(
googleChatDescriptionDefaultValue,
);
});
it('Should check if saving with a webhook url outside chat.googleapis.com displays error notification', async () => {
const user = userEvent.setup();
await fillField(user, 'channel-name-textbox', 'gchat-channel');
await fillField(user, 'webhook-url-textbox', 'https://example.com/webhook');
await user.click(screen.getByTestId('save-channel-button'));
await waitFor(() =>
expect(errorNotification).toHaveBeenCalledWith({
message: 'Error',
description: 'google_chat_webhook_url_invalid',
}),
);
});
it('Should check if saving sends a googlechat_configs payload', async () => {
let requestBody: unknown;
server.use(
rest.post('http://localhost/api/v1/channels', async (req, res, ctx) => {
requestBody = await req.json();
return res(
ctx.status(201),
ctx.json({ status: 'success', data: 'channel created' }),
);
}),
);
const user = userEvent.setup();
await fillField(user, 'channel-name-textbox', 'gchat-channel');
await fillField(user, 'webhook-url-textbox', validWebhookUrl);
await user.click(screen.getByTestId('save-channel-button'));
await waitFor(() =>
expect(successNotification).toHaveBeenCalledWith({
message: 'Success',
description: 'channel_creation_done',
}),
);
expect(requestBody).toStrictEqual({
name: 'gchat-channel',
googlechat_configs: [
{
webhook_url: validWebhookUrl,
title: GoogleChatInitialConfig.title,
text: GoogleChatInitialConfig.text,
send_resolved: true,
},
],
});
});
});
describe('Changing the channel type', () => {
async function selectType(
user: ReturnType<typeof userEvent.setup>,
optionText: string,
): Promise<void> {
// the type dropdown opens on the inner search input of the antd select
await user.click(screen.getByRole('combobox'));
await user.click(await screen.findByTitle(optionText));
}
it('Should check if switching to Google Chat and back swaps the prefilled templates', async () => {
const user = userEvent.setup();
render(<CreateAlertChannels preType={ChannelType.Slack} />);
await selectType(user, 'Google Chat');
await waitFor(() =>
expect(screen.getByTestId('title-textarea')).toHaveTextContent(
googleChatTitleDefaultValue,
),
);
expect(screen.getByTestId('description-textarea')).toHaveTextContent(
googleChatDescriptionDefaultValue,
);
await selectType(user, 'Slack');
await waitFor(() =>
expect(screen.getByTestId('title-textarea')).toHaveTextContent(
slackTitleDefaultValue,
),
);
expect(screen.getByTestId('description-textarea')).toHaveTextContent(
slackDescriptionDefaultValue,
);
});
it('Should check if switching to Pagerduty prefills the pagerduty description and not the opsgenie one', async () => {
const user = userEvent.setup();
render(<CreateAlertChannels preType={ChannelType.Opsgenie} />);
await selectType(user, 'Pagerduty');
await waitFor(() =>
expect(screen.getByTestId('pager-description-textarea')).toHaveTextContent(
pagerDutyDescriptionDefaultValue,
),
);
});
});
});
});

View File

@@ -5,7 +5,7 @@ import {
opsGenieMessageDefaultValue,
opsGeniePriorityDefaultValue,
pagerDutyAdditionalDetailsDefaultValue,
pagerDutyDescriptionDefaultValue,
pagerDutyDescriptionDefaultVaule,
pagerDutySeverityTextDefaultValue,
slackDescriptionDefaultValue,
slackTitleDefaultValue,
@@ -150,7 +150,7 @@ describe('Create Alert Channel (Normal User)', () => {
);
expect(descriptionTextArea).toHaveTextContent(
pagerDutyDescriptionDefaultValue,
pagerDutyDescriptionDefaultVaule,
);
});
it('Should check if Severity label, info (help_pager_severity), and textbox are displayed properly', () => {

View File

@@ -37,6 +37,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { Exception, PayloadProps } from 'types/api/errors/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../constants/features';
import { useAppContext } from '../../providers/App/App';
import { FilterDropdownExtendsProps } from './types';
import {
extractFilterValues,
@@ -416,6 +418,11 @@ function AllErrors(): JSX.Element {
},
];
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const onChangeHandler: TableProps<Exception>['onChange'] = useCallback(
(
paginations: TablePaginationConfig,
@@ -451,7 +458,7 @@ function AllErrors(): JSX.Element {
useEffect(() => {
if (!isUndefined(errorCountResponse.data?.payload)) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === getResourceDeploymentKeys(),
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
)?.tagValue;
logEvent('Exception: List page visited', {

View File

@@ -104,7 +104,6 @@ export enum ChannelType {
Pagerduty = 'pagerduty',
Opsgenie = 'opsgenie',
MsTeams = 'msteams',
GoogleChat = 'googlechat',
}
// LabelFilterStatement will be used for preparing filter conditions / matchers
@@ -126,11 +125,3 @@ export interface MsTeamsChannel extends Channel {
title?: string;
text?: string;
}
export interface GoogleChatChannel extends Channel {
// incoming webhook url of the google chat space, must be an
// https url on chat.googleapis.com
webhook_url?: string;
title?: string;
text?: string;
}

View File

@@ -1,51 +1,4 @@
import {
ChannelType,
EmailChannel,
GoogleChatChannel,
MsTeamsChannel,
OpsgenieChannel,
PagerChannel,
SlackChannel,
WebhookChannel,
} from './config';
// shared by slack and ms teams, both render the same title / description boxes
export const SlackInitialConfig: Partial<SlackChannel> = {
text: `{{ range .Alerts -}}
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
*Summary:* {{ .Annotations.summary }}
*Description:* {{ .Annotations.description }}
*RelatedLogs:* {{ if gt (len .Annotations.related_logs) 0 -}} View in <{{ .Annotations.related_logs }}|logs explorer> {{- end}}
*RelatedTraces:* {{ if gt (len .Annotations.related_traces) 0 -}} View in <{{ .Annotations.related_traces }}|traces explorer> {{- end}}
*Details:*
{{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }}
{{ end }}
{{ end }}`,
title: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}
{{- if gt (len .CommonLabels) (len .GroupLabels) -}}
{{" "}}(
{{- with .CommonLabels.Remove .GroupLabels.Names }}
{{- range $index, $label := .SortedPairs -}}
{{ if $index }}, {{ end }}
{{- $label.Name }}="{{ $label.Value -}}"
{{- end }}
{{- end -}}
)
{{- end }}`,
};
// mirrors DefaultGoogleChatReceiverConfig in pkg/types/alertmanagertypes/googlechat.go,
// which the backend applies when title / text are left empty
export const GoogleChatInitialConfig: Partial<GoogleChatChannel> = {
title: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }}`,
text: `{{ range .Alerts -}}
**Alert:** {{ .Labels.alertname }}{{ if .Labels.severity }} ({{ .Labels.severity }}){{ end }}{{ if .Annotations.summary }}
**Summary:** {{ .Annotations.summary }}{{ end }}{{ if .Annotations.description }}
**Description:** {{ .Annotations.description }}{{ end }}
{{ end }}`,
};
import { EmailChannel, OpsgenieChannel, PagerChannel } from './config';
export const PagerInitialConfig: Partial<PagerChannel> = {
description: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}
@@ -493,26 +446,3 @@ export const EmailInitialConfig: Partial<EmailChannel> = {
</body>
</html>`,
};
// prefilled values of every channel type, keyed by type so the form can apply
// exactly one set of defaults and swap it when the type changes
export const ChannelInitialConfig: Record<
ChannelType,
Partial<
SlackChannel &
WebhookChannel &
PagerChannel &
MsTeamsChannel &
OpsgenieChannel &
EmailChannel &
GoogleChatChannel
>
> = {
[ChannelType.Slack]: SlackInitialConfig,
[ChannelType.MsTeams]: SlackInitialConfig,
[ChannelType.GoogleChat]: GoogleChatInitialConfig,
[ChannelType.Pagerduty]: PagerInitialConfig,
[ChannelType.Opsgenie]: OpsgenieInitialConfig,
[ChannelType.Email]: EmailInitialConfig,
[ChannelType.Webhook]: {},
};

View File

@@ -14,24 +14,16 @@ import testPagerApi from 'api/channels/testPager';
import testSlackApi from 'api/channels/testSlack';
import testWebhookApi from 'api/channels/testWebhook';
import logEvent from 'api/common/logEvent';
import {
useCreateChannel,
useTestChannel,
} from 'api/generated/services/channels';
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { ErrorType } from 'api/generatedAPIInstance';
import ROUTES from 'constants/routes';
import FormAlertChannels from 'container/FormAlertChannels';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { toAPIError } from 'utils/errorUtils';
import {
ChannelType,
EmailChannel,
GoogleChatChannel,
MsTeamsChannel,
OpsgenieChannel,
PagerChannel,
@@ -39,12 +31,12 @@ import {
ValidatePagerChannel,
WebhookChannel,
} from './config';
import { ChannelInitialConfig } from './defaults';
import {
isChannelType,
isValidGoogleChatWebhookURL,
prepareGoogleChatRequest,
} from './utils';
EmailInitialConfig,
OpsgenieInitialConfig,
PagerInitialConfig,
} from './defaults';
import { isChannelType } from './utils';
import './CreateAlertChannels.styles.scss';
@@ -68,38 +60,69 @@ function CreateAlertChannels({
PagerChannel &
MsTeamsChannel &
OpsgenieChannel &
EmailChannel &
GoogleChatChannel
EmailChannel
>
>(() => ({
>({
send_resolved: true,
...ChannelInitialConfig[preType],
}));
text: `{{ range .Alerts -}}
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
*Summary:* {{ .Annotations.summary }}
*Description:* {{ .Annotations.description }}
*RelatedLogs:* {{ if gt (len .Annotations.related_logs) 0 -}} View in <{{ .Annotations.related_logs }}|logs explorer> {{- end}}
*RelatedTraces:* {{ if gt (len .Annotations.related_traces) 0 -}} View in <{{ .Annotations.related_traces }}|traces explorer> {{- end}}
*Details:*
{{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }}
{{ end }}
{{ end }}`,
title: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}
{{- if gt (len .CommonLabels) (len .GroupLabels) -}}
{{" "}}(
{{- with .CommonLabels.Remove .GroupLabels.Names }}
{{- range $index, $label := .SortedPairs -}}
{{ if $index }}, {{ end }}
{{- $label.Name }}="{{ $label.Value -}}"
{{- end }}
{{- end -}}
)
{{- end }}`,
});
const [savingState, setSavingState] = useState<boolean>(false);
const [testingState, setTestingState] = useState<boolean>(false);
const { notifications } = useNotifications();
const { mutateAsync: createChannel } = useCreateChannel();
const { mutateAsync: testChannel } = useTestChannel();
const [type, setType] = useState<ChannelType>(preType);
const onTypeChangeHandler = useCallback(
(value: string) => {
const nextType = value as ChannelType;
if (nextType === type) {
return;
const currentType = type;
setType(value as ChannelType);
if (value === ChannelType.Pagerduty && currentType !== value) {
// reset config to pager defaults
setSelectedConfig({
name: selectedConfig?.name,
send_resolved: selectedConfig.send_resolved,
...PagerInitialConfig,
});
}
setType(nextType);
if (value === ChannelType.Opsgenie && currentType !== value) {
setSelectedConfig((selectedConfig) => ({
...selectedConfig,
...OpsgenieInitialConfig,
}));
}
// the fields the types share (title, text, description) keep the value of
// the type that was selected before, so the new type's defaults have to be
// written to both the config and the form
const defaults = ChannelInitialConfig[nextType];
setSelectedConfig((selectedConfig) => ({ ...selectedConfig, ...defaults }));
formInstance.setFieldsValue(defaults);
// reset config to email defaults
if (value === ChannelType.Email && currentType !== value) {
setSelectedConfig((selectedConfig) => ({
...selectedConfig,
...EmailInitialConfig,
}));
}
},
[type, formInstance],
[type, selectedConfig],
);
const prepareSlackRequest = useCallback(
@@ -384,56 +407,6 @@ function CreateAlertChannels({
showErrorModal,
]);
const validateGoogleChatConfig = useCallback((): boolean => {
if (!selectedConfig.webhook_url) {
notifications.error({
message: 'Error',
description: t('webhook_url_required'),
});
return false;
}
if (!isValidGoogleChatWebhookURL(selectedConfig.webhook_url)) {
notifications.error({
message: 'Error',
description: t('google_chat_webhook_url_invalid'),
});
return false;
}
return true;
}, [selectedConfig.webhook_url, notifications, t]);
const onGoogleChatHandler = useCallback(async () => {
if (!validateGoogleChatConfig()) {
return { status: 'failed', statusMessage: t('channel_creation_failed') };
}
setSavingState(true);
try {
await createChannel({ data: prepareGoogleChatRequest(selectedConfig) });
notifications.success({
message: 'Success',
description: t('channel_creation_done'),
});
history.replace(ROUTES.ALL_CHANNELS);
return { status: 'success', statusMessage: t('channel_creation_done') };
} catch (error) {
showErrorModal(toAPIError(error as ErrorType<RenderErrorResponseDTO>));
return { status: 'failed', statusMessage: t('channel_creation_failed') };
} finally {
setSavingState(false);
}
}, [
validateGoogleChatConfig,
createChannel,
selectedConfig,
notifications,
t,
showErrorModal,
]);
const onSaveHandler = useCallback(
async (value: ChannelType) => {
if (!selectedConfig.name) {
@@ -451,7 +424,6 @@ function CreateAlertChannels({
[ChannelType.Opsgenie]: onOpsgenieHandler,
[ChannelType.MsTeams]: onMsTeamsHandler,
[ChannelType.Email]: onEmailHandler,
[ChannelType.GoogleChat]: onGoogleChatHandler,
};
if (isChannelType(value)) {
@@ -483,7 +455,6 @@ function CreateAlertChannels({
onOpsgenieHandler,
onMsTeamsHandler,
onEmailHandler,
onGoogleChatHandler,
notifications,
t,
],
@@ -521,13 +492,6 @@ function CreateAlertChannels({
request = prepareEmailRequest();
await testEmail(request);
break;
case ChannelType.GoogleChat:
if (!validateGoogleChatConfig()) {
setTestingState(false);
return;
}
await testChannel({ data: prepareGoogleChatRequest(selectedConfig) });
break;
default:
notifications.error({
message: 'Error',
@@ -549,11 +513,7 @@ function CreateAlertChannels({
status: 'Test success',
});
} catch (error) {
showErrorModal(
error instanceof APIError
? error
: toAPIError(error as ErrorType<RenderErrorResponseDTO>),
);
showErrorModal(error as APIError);
logEvent('Alert Channel: Test notification', {
type: channelType,
@@ -575,8 +535,6 @@ function CreateAlertChannels({
prepareSlackRequest,
prepareMsTeamsRequest,
prepareEmailRequest,
validateGoogleChatConfig,
testChannel,
notifications,
],
);
@@ -604,6 +562,9 @@ function CreateAlertChannels({
initialValue: {
type,
...selectedConfig,
...PagerInitialConfig,
...OpsgenieInitialConfig,
...EmailInitialConfig,
},
}}
/>

View File

@@ -1,39 +1,4 @@
import {
AlertmanagertypesPostableChannelDTO,
ConfigSecretURLDTO,
} from 'api/generated/services/sigNoz.schemas';
import { ChannelType, GoogleChatChannel } from './config';
import { ChannelType } from './config';
export const isChannelType = (type: string): type is ChannelType =>
Object.values(ChannelType).includes(type as ChannelType);
const GOOGLE_CHAT_WEBHOOK_HOST = 'chat.googleapis.com';
// the backend enforces the same two rules, this is only for a nicer error experience
export const isValidGoogleChatWebhookURL = (url: string): boolean => {
try {
const { protocol, hostname } = new URL(url);
return (
protocol === 'https:' && hostname.toLowerCase() === GOOGLE_CHAT_WEBHOOK_HOST
);
} catch {
return false;
}
};
// create, update and test all send the same body shape
export const prepareGoogleChatRequest = (
config: Partial<GoogleChatChannel>,
): AlertmanagertypesPostableChannelDTO => ({
name: config.name || '',
googlechat_configs: [
{
// the generated type models go's config.SecretURL as an object, the api takes a string
webhook_url: (config.webhook_url || '') as unknown as ConfigSecretURLDTO,
title: config.title || '',
text: config.text || '',
send_resolved: config.send_resolved || false,
},
],
});

View File

@@ -130,28 +130,6 @@ describe('Footer utils', () => {
};
expect(validateCreateAlertState(currentArgs)).toBeNull();
});
it('when threshold channels are null', () => {
const currentArgs: BuildCreateAlertRulePayloadArgs = {
...args,
basicAlertState: {
...args.basicAlertState,
name: 'test name',
},
thresholdState: {
...args.thresholdState,
thresholds: [
{
...args.thresholdState.thresholds[0],
channels: null as unknown as string[],
},
],
},
};
expect(validateCreateAlertState(currentArgs)).toBe(
'Please select at least one channel for each threshold or enable routing policies',
);
});
});
describe('getNotificationSettingsProps', () => {

View File

@@ -44,8 +44,7 @@ export function validateCreateAlertState(
if (!threshold.label) {
return 'Please enter a label for each threshold';
}
// this runs during render, so a throw here takes down the whole page
if (!notificationSettings.routingPolicies && !threshold.channels?.length) {
if (!notificationSettings.routingPolicies && !threshold.channels.length) {
return 'Please select at least one channel for each threshold or enable routing policies';
}
}

View File

@@ -316,34 +316,6 @@ describe('CreateAlertV2 utils', () => {
});
});
describe('getThresholdStateFromAlertDef null channels', () => {
it('falls back to an empty array so downstream consumers never see null', () => {
const def: PostableAlertRuleV2 = {
...defaultPostableAlertRuleV2,
condition: {
...defaultPostableAlertRuleV2.condition,
thresholds: {
kind: 'basic',
spec: [
{
name: 'critical',
target: 1,
targetUnit: UniversalYAxisUnit.MINUTES,
channels: null as unknown as string[],
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
op: AlertThresholdOperator.IS_ABOVE,
},
],
},
},
};
expect(
getThresholdStateFromAlertDef(def).thresholds[0].channels,
).toStrictEqual([]);
});
});
describe('normalizeOperator', () => {
it.each([
['1', AlertThresholdOperator.IS_ABOVE],

View File

@@ -258,9 +258,7 @@ export function getThresholdStateFromAlertDef(
recoveryThresholdValue: null,
unit: threshold.targetUnit,
color: getColorForThreshold(threshold.name),
// rules created outside the UI can come back with a null channels
// field; drop the guard once the API enforces the schema
channels: threshold.channels ?? [],
channels: threshold.channels,
})) || [],
selectedQuery: alertDef.condition.selectedQueryName || '',
operator:

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