Compare commits

..

1 Commits

Author SHA1 Message Date
srikanthccv
442d58bccc chore: temp commit 2026-07-28 20:55:13 +05:30
821 changed files with 32694 additions and 170051 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,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 -->
---

View File

@@ -1,92 +0,0 @@
name: cacheci
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
actions: write
# Cancelling mid-rotation is safe: the sequential delete-then-save order
# leaves at most one key missing at any moment.
concurrency:
group: cacheci
cancel-in-progress: true
jobs:
tests:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4
- name: restore
id: restore
uses: actions/cache/restore@v4
with:
path: ${{ runner.temp }}/cacheci
key: tests-primary
restore-keys: |
tests-secondary
- name: inject
if: steps.restore.outputs.cache-matched-key != ''
run: |
cat > "$RUNNER_TEMP/inject.Dockerfile" <<'EOF'
FROM busybox:1.37
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/pnpm/store \
--mount=type=bind,target=/restored \
tar -xf /restored/go-build.tar -C /root/.cache/go-build && \
tar -xf /restored/go-mod.tar -C /go/pkg/mod && \
tar -xf /restored/pnpm-store.tar -C /pnpm/store
EOF
docker build -f "$RUNNER_TEMP/inject.Dockerfile" "$RUNNER_TEMP/cacheci"
- name: build
run: |
docker build -f cmd/enterprise/Dockerfile.integration --build-arg TARGETARCH=amd64 --build-arg ZEUSURL=http://zeus:8080 .
docker build -f cmd/enterprise/Dockerfile.with-web.integration --build-arg TARGETARCH=amd64 --build-arg ZEUSURL=http://zeus:8080 .
# docker cp instead of --output type=local (the local exporter stalls on
# multi-GB outputs); tarballs instead of raw trees so the host never hits
# the permission and symlink semantics that broke docker cp.
- name: extract
run: |
rm -rf "$RUNNER_TEMP/cacheci"
mkdir -p "$RUNNER_TEMP/cacheci" "$RUNNER_TEMP/extract-context"
cat > "$RUNNER_TEMP/extract.Dockerfile" <<'EOF'
FROM busybox:1.37
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/pnpm/store \
mkdir -p /out && \
tar -cf /out/go-build.tar -C /root/.cache/go-build . && \
tar -cf /out/go-mod.tar -C /go/pkg/mod . && \
tar -cf /out/pnpm-store.tar -C /pnpm/store .
EOF
docker build -f "$RUNNER_TEMP/extract.Dockerfile" -t cacheci-extract "$RUNNER_TEMP/extract-context"
id=$(docker create cacheci-extract)
docker cp "$id":/out/. "$RUNNER_TEMP/cacheci/"
docker rm "$id"
# Fixed cache keys are immutable, so each key must be deleted before it
# can be saved again. Rotating primary and secondary one after the other
# keeps at least one key restorable for concurrent test runs.
- name: delete-primary
env:
GH_TOKEN: ${{ github.token }}
run: gh cache delete tests-primary --repo "$GITHUB_REPOSITORY" || true
- name: save-primary
uses: actions/cache/save@v4
with:
path: ${{ runner.temp }}/cacheci
key: tests-primary
- name: delete-secondary
env:
GH_TOKEN: ${{ github.token }}
run: gh cache delete tests-secondary --repo "$GITHUB_REPOSITORY" || true
- name: save-secondary
uses: actions/cache/save@v4
with:
path: ${{ runner.temp }}/cacheci
key: tests-secondary

View File

@@ -75,30 +75,6 @@ jobs:
docker rm pw
echo "PLAYWRIGHT_BROWSERS_PATH=$RUNNER_TEMP/ms-playwright" >> "$GITHUB_ENV"
cd tests/e2e && pnpm playwright install-deps ${{ matrix.project }}
# Restore-only: the cacheci workflow owns cache saves. Seeds the
# BuildKit cache mounts so the in-test image build is incremental.
- name: restore
id: restore
uses: actions/cache/restore@v4
with:
path: ${{ runner.temp }}/cacheci
key: tests-primary
restore-keys: |
tests-secondary
- name: inject
if: steps.restore.outputs.cache-matched-key != ''
run: |
cat > "$RUNNER_TEMP/inject.Dockerfile" <<'EOF'
FROM busybox:1.37
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/pnpm/store \
--mount=type=bind,target=/restored \
tar -xf /restored/go-build.tar -C /root/.cache/go-build && \
tar -xf /restored/go-mod.tar -C /go/pkg/mod && \
tar -xf /restored/pnpm-store.tar -C /pnpm/store
EOF
docker build -f "$RUNNER_TEMP/inject.Dockerfile" "$RUNNER_TEMP/cacheci"
- name: bring-up-stack
run: |
cd tests && \

View File

@@ -39,8 +39,6 @@ jobs:
matrix:
suite:
- alerts
- alertmanager
- alertmanagerrotation
- basepath
- callbackauthn
- cloudintegrations
@@ -55,9 +53,7 @@ jobs:
- queriermetrics
- querierscalar
- queriercommon
- querierai
- rawexportdata
- promqlconformance
- querierauthz
- role
- rootuser
@@ -113,30 +109,6 @@ jobs:
sudo mv chromedriver-linux64/chromedriver /usr/local/bin/chromedriver
chromedriver -version
google-chrome-stable --version
# Restore-only: the cacheci workflow owns cache saves. Seeds the
# BuildKit cache mounts so the in-test image build is incremental.
- name: restore
id: restore
uses: actions/cache/restore@v4
with:
path: ${{ runner.temp }}/cacheci
key: tests-primary
restore-keys: |
tests-secondary
- name: inject
if: steps.restore.outputs.cache-matched-key != ''
run: |
cat > "$RUNNER_TEMP/inject.Dockerfile" <<'EOF'
FROM busybox:1.37
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/pnpm/store \
--mount=type=bind,target=/restored \
tar -xf /restored/go-build.tar -C /root/.cache/go-build && \
tar -xf /restored/go-mod.tar -C /go/pkg/mod && \
tar -xf /restored/pnpm-store.tar -C /pnpm/store
EOF
docker build -f "$RUNNER_TEMP/inject.Dockerfile" "$RUNNER_TEMP/cacheci"
- name: run
run: |
cd tests && \

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

@@ -209,8 +209,8 @@ py-lint: ## Run ruff check across the shared tests project
@cd tests && uv run ruff check --fix .
.PHONY: py-test-setup
py-test-setup: ## Bring up the shared SigNoz backend used by integration and e2e tests, rebuilding signoz from the current sources
@cd tests && uv run pytest --basetemp=./tmp/ -vv --reuse --rebuild --capture=no integration/bootstrap/setup.py::test_setup
py-test-setup: ## Bring up the shared SigNoz backend used by integration and e2e tests
@cd tests && uv run pytest --basetemp=./tmp/ -vv --reuse --capture=no integration/bootstrap/setup.py::test_setup
.PHONY: py-test-teardown
py-test-teardown: ## Tear down the shared SigNoz backend

View File

@@ -93,13 +93,9 @@ func runGenerateAuthz(_ context.Context) error {
registry := coretypes.NewRegistry()
allowedResources := map[string]bool{
coretypes.NewResourceRef(coretypes.ResourceServiceAccount).String(): true,
coretypes.NewResourceRef(coretypes.ResourceRole).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourceFactorAPIKey).String(): true,
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceLogs).String(): true,
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceTraces).String(): true,
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceMetrics).String(): true,
coretypes.NewResourceRef(coretypes.ResourceTelemetryResourceMeterMetrics).String(): true,
coretypes.NewResourceRef(coretypes.ResourceServiceAccount).String(): true,
coretypes.NewResourceRef(coretypes.ResourceRole).String(): true,
coretypes.NewResourceRef(coretypes.ResourceMetaResourceFactorAPIKey).String(): true,
}
allowedTypes := map[string]bool{}

View File

@@ -4,13 +4,9 @@ ARG OS="linux"
ARG TARGETARCH
ARG ZEUSURL
# HOME comes from the build user, not the image config; declare it so the
# /root paths below trace to it.
ENV HOME=/root
# This path is important for stacktraces
WORKDIR $GOPATH/src/github.com/signoz/signoz
WORKDIR $HOME
WORKDIR /root
RUN set -eux; \
apt-get update; \
@@ -18,36 +14,23 @@ RUN set -eux; \
g++ \
gcc \
libc6-dev \
make \
pkg-config \
; \
rm -rf /var/lib/apt/lists/*
# Keep the literal cache-mount targets below in sync with these. The caches
# are shared with Dockerfile.with-web.integration (same target paths).
ENV GOCACHE=$HOME/.cache/go-build
ENV GOMODCACHE=$GOPATH/pkg/mod
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
RUN go mod download
COPY ./cmd/ ./cmd/
COPY ./ee/ ./ee/
COPY ./pkg/ ./pkg/
COPY ./templates /root/templates
# Invoked directly instead of via make so Makefile changes don't invalidate
# this layer; the Makefile's git-derived ldflags resolve to empty in here
# anyway (.git is dockerignored).
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
GOARCH=${TARGETARCH} GOOS=${OS} go build -C ./cmd/enterprise -race -tags timetzdata -o /root/signoz \
-ldflags "-s -w \
-X github.com/SigNoz/signoz/pkg/version.version=integration \
-X github.com/SigNoz/signoz/pkg/version.variant=enterprise \
-X github.com/SigNoz/signoz/ee/zeus.url=${ZEUSURL} \
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=${ZEUSURL}/api/v1"
COPY Makefile Makefile
RUN TARGET_DIR=/root ARCHS=${TARGETARCH} ZEUS_URL=${ZEUSURL} LICENSE_URL=${ZEUSURL}/api/v1 make go-build-enterprise-race
RUN mv /root/linux-${TARGETARCH}/signoz /root/signoz
RUN chmod 755 /root /root/signoz

View File

@@ -1,23 +1,10 @@
FROM node:22-bookworm AS build
WORKDIR /opt/
# HOME comes from the build user, not the image config.
ENV HOME=/root
# pnpm's store lives at $PNPM_HOME/store — a dedicated directory pnpm
# manages. Keep the literal cache-mount targets below in sync.
ENV PNPM_HOME=/pnpm
ENV NODE_OPTIONS=--max-old-space-size=8192
RUN CI=1 npm i -g pnpm@10
# pnpm fetch resolves from the lockfile alone and runs no lifecycle scripts;
# the repo's postinstall needs source files that are not copied yet.
COPY ./frontend/package.json ./frontend/pnpm-lock.yaml ./frontend/pnpm-workspace.yaml ./
RUN --mount=type=cache,target=/pnpm/store CI=1 pnpm fetch
COPY ./frontend/ ./
RUN --mount=type=cache,target=/pnpm/store CI=1 pnpm install --offline
ENV NODE_OPTIONS=--max-old-space-size=8192
RUN CI=1 npm i -g pnpm@10
RUN CI=1 pnpm install
RUN CI=1 pnpm build
FROM golang:1.25-bookworm
@@ -26,13 +13,9 @@ ARG OS="linux"
ARG TARGETARCH
ARG ZEUSURL
# HOME comes from the build user, not the image config; declare it so the
# /root paths below trace to it.
ENV HOME=/root
# This path is important for stacktraces
WORKDIR $GOPATH/src/github.com/signoz/signoz
WORKDIR $HOME
WORKDIR /root
RUN set -eux; \
apt-get update; \
@@ -40,36 +23,23 @@ RUN set -eux; \
g++ \
gcc \
libc6-dev \
make \
pkg-config \
; \
rm -rf /var/lib/apt/lists/*
# Keep the literal cache-mount targets below in sync with these. The caches
# are shared with Dockerfile.integration (same target paths).
ENV GOCACHE=$HOME/.cache/go-build
ENV GOMODCACHE=$GOPATH/pkg/mod
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
RUN go mod download
COPY ./cmd/ ./cmd/
COPY ./ee/ ./ee/
COPY ./pkg/ ./pkg/
COPY ./templates /root/templates
# Invoked directly instead of via make so Makefile changes don't invalidate
# this layer; the Makefile's git-derived ldflags resolve to empty in here
# anyway (.git is dockerignored).
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
GOARCH=${TARGETARCH} GOOS=${OS} go build -C ./cmd/enterprise -race -tags timetzdata -o /root/signoz \
-ldflags "-s -w \
-X github.com/SigNoz/signoz/pkg/version.version=integration \
-X github.com/SigNoz/signoz/pkg/version.variant=enterprise \
-X github.com/SigNoz/signoz/ee/zeus.url=${ZEUSURL} \
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=${ZEUSURL}/api/v1"
COPY Makefile Makefile
RUN TARGET_DIR=/root ARCHS=${TARGETARCH} ZEUS_URL=${ZEUSURL} LICENSE_URL=${ZEUSURL}/api/v1 make go-build-enterprise-race
RUN mv /root/linux-${TARGETARCH}/signoz /root/signoz
COPY --from=build /opt/build ./web/

View File

@@ -2,9 +2,9 @@ package main
import (
"github.com/SigNoz/signoz/pkg/metercollector"
"github.com/SigNoz/signoz/pkg/telemetryschema/logstelemetryschema"
"github.com/SigNoz/signoz/pkg/telemetryschema/metricstelemetryschema"
"github.com/SigNoz/signoz/pkg/telemetryschema/tracestelemetryschema"
"github.com/SigNoz/signoz/pkg/telemetrylogs"
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
"github.com/SigNoz/signoz/pkg/telemetrytraces"
"github.com/SigNoz/signoz/pkg/types/retentiontypes"
"github.com/SigNoz/signoz/pkg/types/zeustypes"
)
@@ -25,8 +25,8 @@ var meterConfigs = []metercollector.Config{
Name: zeustypes.MeterLogSize,
Unit: zeustypes.MeterUnitBytes,
Aggregation: zeustypes.MeterAggregationSum,
DBName: logstelemetryschema.DBName,
TableName: logstelemetryschema.LogsV2LocalTableName,
DBName: telemetrylogs.DBName,
TableName: telemetrylogs.LogsV2LocalTableName,
DefaultRetentionDays: retentiontypes.DefaultLogsRetentionDays,
},
},
@@ -36,8 +36,8 @@ var meterConfigs = []metercollector.Config{
Name: zeustypes.MeterSpanSize,
Unit: zeustypes.MeterUnitBytes,
Aggregation: zeustypes.MeterAggregationSum,
DBName: tracestelemetryschema.DBName,
TableName: tracestelemetryschema.SpanIndexV3LocalTableName,
DBName: telemetrytraces.DBName,
TableName: telemetrytraces.SpanIndexV3LocalTableName,
DefaultRetentionDays: retentiontypes.DefaultTracesRetentionDays,
},
},
@@ -47,8 +47,8 @@ var meterConfigs = []metercollector.Config{
Name: zeustypes.MeterDatapointCount,
Unit: zeustypes.MeterUnitCount,
Aggregation: zeustypes.MeterAggregationSum,
DBName: metricstelemetryschema.DBName,
TableName: metricstelemetryschema.SamplesV4LocalTableName,
DBName: telemetrymetrics.DBName,
TableName: telemetrymetrics.SamplesV4LocalTableName,
DefaultRetentionDays: retentiontypes.DefaultMetricsRetentionDays,
},
},

View File

@@ -1496,9 +1496,6 @@ components:
- redis
- cloudsql_postgres
- memorystore_redis
- computeengine
- gke
- cloudstorage
type: string
CloudintegrationtypesServiceMetadata:
properties:
@@ -6902,7 +6899,6 @@ components:
Querybuildertypesv5QueryEnvelope:
discriminator:
mapping:
builder_ai_query: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilderAI'
builder_formula: '#/components/schemas/Querybuildertypesv5QueryEnvelopeFormula'
builder_query: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilder'
builder_trace_operator: '#/components/schemas/Querybuildertypesv5QueryEnvelopeTraceOperator'
@@ -6911,7 +6907,6 @@ components:
propertyName: type
oneOf:
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilder'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeBuilderAI'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeFormula'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopeTraceOperator'
- $ref: '#/components/schemas/Querybuildertypesv5QueryEnvelopePromQL'
@@ -6926,15 +6921,6 @@ components:
required:
- type
type: object
Querybuildertypesv5QueryEnvelopeBuilderAI:
properties:
spec:
$ref: '#/components/schemas/Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregation'
type:
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
required:
- type
type: object
Querybuildertypesv5QueryEnvelopeClickHouseSQL:
properties:
spec:
@@ -7048,7 +7034,6 @@ components:
Querybuildertypesv5QueryType:
enum:
- builder_query
- builder_ai_query
- builder_formula
- builder_trace_operator
- clickhouse_sql
@@ -7442,8 +7427,6 @@ components:
- below
- equal
- not_equal
- above_or_equal
- below_or_equal
- outside_bounds
type: string
RuletypesCumulativeSchedule:
@@ -10247,9 +10230,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:update
- ADMIN
- tokenizer:
- dashboard:update
- ADMIN
summary: Delete public dashboard
tags:
- dashboard
@@ -10298,9 +10281,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:read
- ADMIN
- tokenizer:
- dashboard:read
- ADMIN
summary: Get public dashboard
tags:
- dashboard
@@ -10355,9 +10338,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:update
- ADMIN
- tokenizer:
- dashboard:update
- ADMIN
summary: Create public dashboard
tags:
- dashboard
@@ -10399,9 +10382,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:update
- ADMIN
- tokenizer:
- dashboard:update
- ADMIN
summary: Update public dashboard
tags:
- dashboard
@@ -14739,9 +14722,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:list
- VIEWER
- tokenizer:
- dashboard:list
- VIEWER
summary: List dashboard saved views
tags:
- dashboard
@@ -14796,9 +14779,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:list
- EDITOR
- tokenizer:
- dashboard:list
- EDITOR
summary: Create dashboard saved view
tags:
- dashboard
@@ -14849,9 +14832,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:list
- EDITOR
- tokenizer:
- dashboard:list
- EDITOR
summary: Delete dashboard saved view
tags:
- dashboard
@@ -14917,9 +14900,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:list
- EDITOR
- tokenizer:
- dashboard:list
- EDITOR
summary: Update dashboard saved view
tags:
- dashboard
@@ -14994,9 +14977,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:list
- VIEWER
- tokenizer:
- dashboard:list
- VIEWER
summary: List dashboards (v2)
tags:
- dashboard
@@ -15051,9 +15034,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:create
- EDITOR
- tokenizer:
- dashboard:create
- EDITOR
summary: Create dashboard (v2)
tags:
- dashboard
@@ -15104,9 +15087,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:delete
- EDITOR
- tokenizer:
- dashboard:delete
- EDITOR
summary: Delete dashboard (v2)
tags:
- dashboard
@@ -15167,9 +15150,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:read
- VIEWER
- tokenizer:
- dashboard:read
- VIEWER
summary: Get dashboard (v2)
tags:
- dashboard
@@ -15241,9 +15224,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:update
- EDITOR
- tokenizer:
- dashboard:update
- EDITOR
summary: Patch dashboard (v2)
tags:
- dashboard
@@ -15310,9 +15293,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:update
- EDITOR
- tokenizer:
- dashboard:update
- EDITOR
summary: Update dashboard (v2)
tags:
- dashboard
@@ -15378,11 +15361,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:read
- dashboard:create
- EDITOR
- tokenizer:
- dashboard:read
- dashboard:create
- EDITOR
summary: Clone dashboard (v2)
tags:
- dashboard
@@ -15433,9 +15414,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:update
- EDITOR
- tokenizer:
- dashboard:update
- EDITOR
summary: Unlock dashboard (v2)
tags:
- dashboard
@@ -15485,78 +15466,12 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:update
- EDITOR
- tokenizer:
- dashboard:update
- EDITOR
summary: Lock dashboard (v2)
tags:
- dashboard
/api/v2/dashboards/{id}/migrate:
post:
deprecated: false
description: 'This endpoint retries the v1→v2 (Perses) migration on a dashboard
still stored in the v1 schema and returns the v2-shape result. It is idempotent:
a dashboard already in the v2 schema is returned unchanged.'
operationId: MigrateDashboardV2
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesGettableDashboardV2'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- dashboard:update
- tokenizer:
- dashboard:update
summary: Migrate dashboard to v2
tags:
- dashboard
/api/v2/factor_password/forgot:
post:
deprecated: false
@@ -23697,9 +23612,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:list
- VIEWER
- tokenizer:
- dashboard:list
- VIEWER
summary: List dashboards for the current user (v2)
tags:
- dashboard
@@ -23744,9 +23659,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:list
- VIEWER
- tokenizer:
- dashboard:list
- VIEWER
summary: Unpin a dashboard for the current user (v2)
tags:
- dashboard
@@ -23803,9 +23718,9 @@ paths:
description: Internal Server Error
security:
- api_key:
- dashboard:list
- VIEWER
- tokenizer:
- dashboard:list
- VIEWER
summary: Pin a dashboard for the current user (v2)
tags:
- dashboard

View File

@@ -1,377 +0,0 @@
# 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.
---
## 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.
In v2, each query runs in one of two ways. The classifier decides per query:
- **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
(`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.
---
## 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.
## The engine path
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.
## 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()`
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.

View File

@@ -30,11 +30,11 @@ yarn install:browsers # one-time Playwright browser install
### Starting the Test Environment
To spin up the backend stack (SigNoz, ClickHouse, Postgres, ClickHouse Keeper, Zeus mock, gateway mock, seeder, migrator-with-web) and keep it running:
To spin up the backend stack (SigNoz, ClickHouse, Postgres, Zookeeper, Zeus mock, gateway mock, seeder, migrator-with-web) and keep it running:
```bash
cd tests
uv run pytest --basetemp=./tmp/ -vv --reuse --rebuild --with-web \
uv run pytest --basetemp=./tmp/ -vv --reuse --with-web \
e2e/bootstrap/setup.py::test_setup
```
@@ -45,13 +45,8 @@ This command will:
- Start the HTTP seeder container (`tests/seeder/` — exposing `/telemetry/{traces,logs,metrics}` POST + DELETE)
- Write backend coordinates to `tests/e2e/.env.local` (loaded by `playwright.config.ts` via dotenv)
- Keep containers running via the `--reuse` flag
- Rebuild the SigNoz container from the current sources via the `--rebuild` flag
The `--with-web` flag builds the frontend into the SigNoz container — required for E2E. The build takes ~4 mins on a cold start; later builds are incremental.
### Rebuilding After Source Changes
The `--with-web` image bakes the built frontend in, so neither backend nor frontend changes are picked up while `--reuse` keeps the container running. `--rebuild` fixes that for both: it kills the SigNoz container, rebuilds the image incrementally (go build cache + pnpm store — a frontend-only change rebuilds in about a minute), and starts a fresh one while databases, mocks, migrations, and the seeder stay reused. The setup command above passes it, so the iteration loop is: change code → re-run the setup command → re-run your specs. `--rebuild` requires `--reuse` and cannot be combined with `--teardown` or `--clean`.
The `--with-web` flag builds the frontend into the SigNoz container — required for E2E. The build takes ~4 mins on a cold start.
### Stopping the Test Environment
@@ -286,16 +281,13 @@ The full `playwright.config.ts` is the source of truth. Common things to tweak:
The same pytest flags integration tests expose work here, since E2E reuses the shared fixture graph:
- `--reuse` — keep containers warm between runs (required for all iteration).
- `--rebuild` — recreate the SigNoz container from the current sources (backend and, with `--with-web`, frontend) while the rest of the stack stays up. Requires `--reuse`.
- `--teardown` — tear everything down.
- `--clean` — prune the docker build caches, forcing the next image build to start cold.
- `--with-web` — build the frontend into the SigNoz container. **Required for E2E**; integration tests don't need it.
- `--sqlstore-provider`, `--postgres-version`, `--clickhouse-version`, etc. — see `docs/contributing/tests/integration.md`.
- `--sqlstore-provider`, `--postgres-version`, `--clickhouse-version`, etc. — see `docs/contributing/integration.md`.
## What should I remember?
- **Always use the `--reuse` flag** when setting up the E2E stack. `--with-web` adds a ~4 min frontend build on a cold start; later builds are incremental.
- **Changed backend or frontend code? Re-run the setup command** — it passes `--rebuild`, swapping the SigNoz container for one built from your current sources while the rest of the stack stays up.
- **Always use the `--reuse` flag** when setting up the E2E stack. `--with-web` adds a ~4 min frontend build; you only want to pay that once.
- **Don't teardown before setup.** `--reuse` correctly handles partially-set-up state, so chaining teardown → setup wastes time.
- **Prefer UI-driven flows.** Playwright captures BE requests in the trace; a parallel `fetch` probe is almost always redundant. Drop to `page.request.*` only when the UI can't reach what you need.
- **Use `page.waitForResponse` on UI clicks** to assert BE contracts — it still exercises the UI trigger path.

View File

@@ -37,34 +37,13 @@ make py-test-setup
Under the hood this runs, from `tests/`:
```bash
uv run pytest --basetemp=./tmp/ -vv --reuse --rebuild --capture=no integration/bootstrap/setup.py::test_setup
uv run pytest --basetemp=./tmp/ -vv --reuse integration/bootstrap/setup.py::test_setup
```
This command will:
- Start all required services (ClickHouse, PostgreSQL, ClickHouse Keeper, SigNoz, Zeus mock, gateway mock)
- Start all required services (ClickHouse, PostgreSQL, Zookeeper, SigNoz, Zeus mock, gateway mock)
- Register an admin user
- Keep containers running via the `--reuse` flag
- Rebuild the SigNoz container from the current sources via the `--rebuild` flag
### Rebuilding After Source Changes
`--reuse` keeps the running SigNoz container, which means backend source changes are not picked up. `--rebuild` fixes exactly that: it kills the existing SigNoz container, rebuilds the image (incremental — only changed packages recompile thanks to the build cache), and starts a fresh one, while everything else (databases, mocks, migrations) stays reused. `make py-test-setup` passes it by default, so the iteration loop is simply:
```bash
make py-test-setup # (re)build signoz from your current sources
uv run pytest --basetemp=./tmp/ -vv --reuse integration/tests/<suite>/
# ... edit backend code or tests ...
make py-test-setup # pick up the backend changes
uv run pytest --basetemp=./tmp/ -vv --reuse integration/tests/<suite>/
```
The same applies to the e2e stack. `--rebuild` requires `--reuse` and cannot be combined with `--teardown` or `--clean`.
Some suites define their own SigNoz variant in a suite-local `conftest.py` (`create_signoz(..., cache_key=...)` — e.g. `basepath`, `metricreduction`, `querier_json_body`). Those containers are not touched by `make py-test-setup`, which only rebuilds the default instance. For such suites, pass `--rebuild` on the suite run itself — it rebuilds every SigNoz variant the run instantiates:
```bash
uv run pytest --basetemp=./tmp/ -vv --reuse --rebuild integration/tests/<suite>/
```
### Stopping the Test Environment
@@ -77,21 +56,11 @@ make py-test-teardown
Which runs:
```bash
uv run pytest --basetemp=./tmp/ -vv --teardown --capture=no integration/bootstrap/setup.py::test_teardown
uv run pytest --basetemp=./tmp/ -vv --teardown integration/bootstrap/setup.py::test_teardown
```
This destroys the running integration test setup and cleans up resources.
### Cleaning the Image Build Cache
The `signoz:integration` image build keeps its Go build and module caches in BuildKit cache mounts, so rebuilds only recompile what changed. These caches survive `--teardown` (they belong to the Docker builder, not to any container). If a cache ever needs to be nuked — suspected corruption, disk pressure, or to force a genuinely cold build — pass the `--clean` flag:
```bash
uv run pytest --basetemp=./tmp/ -vv --teardown --clean integration/bootstrap/setup.py::test_teardown
```
`--clean` prunes the docker build artifacts backing the incremental image build at session start, so the next build starts from a clean slate. Images and regular layer cache stay intact, but note the pruning is host-wide — it clears build caches for other projects too, not just SigNoz's. The flag composes with any invocation — passing it on a normal `--reuse` run simply makes the next image build start cold (~34 minutes instead of seconds).
## Understanding the Integration Test Framework
Python and pytest form the foundation of the integration testing framework. Testcontainers are used to spin up disposable integration environments. WireMock is used to spin up **test doubles** of external services (Zeus cloud API, gateway, etc.).
@@ -130,7 +99,7 @@ tests/
│ ├── passwordauthn/
│ ├── querier/
│ └── ...
└── e2e/ # Playwright suite (see docs/contributing/tests/e2e.md)
└── e2e/ # Playwright suite (see docs/contributing/e2e.md)
```
Each test suite follows these principles:
@@ -255,9 +224,9 @@ Tests can be configured using pytest options:
- `--sqlstore-provider` — Choose the SQL store provider (default: `postgres`)
- `--sqlite-mode` — SQLite journal mode: `delete` or `wal` (default: `delete`). Only relevant when `--sqlstore-provider=sqlite`.
- `--postgres-version` — PostgreSQL version (default: `15`)
- `--clickhouse-version` — ClickHouse version, also used for ClickHouse Keeper (default: `25.12.5`)
- `--schema-migrator-version` — SigNoz schema migrator version (default: `v0.144.6`)
- `--with-web` — Build the frontend into the SigNoz image (required for e2e)
- `--clickhouse-version` — ClickHouse version (default: `25.5.6`)
- `--zookeeper-version` — Zookeeper version (default: `3.7.1`)
- `--schema-migrator-version` — SigNoz schema migrator version (default: `v0.144.2`)
Example:
@@ -270,7 +239,6 @@ uv run pytest --basetemp=./tmp/ -vv --reuse \
## What should I remember?
- **Always use the `--reuse` flag** when setting up the environment or running tests to keep containers warm. Without it every run rebuilds the stack (~4 mins).
- **Changed backend code? Re-run `make py-test-setup`** — it passes `--rebuild`, swapping the SigNoz container for one built from your current sources while the rest of the stack stays up.
- **Use the `--teardown` flag** only when cleaning up — mixing `--teardown` with `--reuse` is a contradiction.
- **Do not pre-emptively teardown before setup.** If the stack is partially up, `--reuse` picks up from wherever it is. `make py-test-teardown` then `make py-test-setup` wastes minutes.
- **Follow the naming convention** with two-digit numeric prefixes (`01_`, `02_`) for ordered test execution within a suite.
@@ -279,5 +247,5 @@ uv run pytest --basetemp=./tmp/ -vv --reuse \
- **Use descriptive test names** that clearly indicate what is being tested.
- **Leverage fixtures** for common setup. The shared fixture package is at `tests/fixtures/` — reuse before adding new ones.
- **Test both success and failure scenarios** (4xx / 5xx paths) to ensure robust functionality.
- **Run `make py-fmt` and `make py-lint` before committing** Python changes — ruff format + ruff check.
- **Run `make py-fmt` and `make py-lint` before committing** Python changes — black + isort + autoflake + pylint.
- **`--sqlite-mode=wal` does not work on macOS.** The integration test environment runs SigNoz inside a Linux container with the SQLite database file mounted from the macOS host. WAL mode requires shared memory between connections, and connections crossing the VM boundary (macOS host ↔ Linux container) cannot share the WAL index, resulting in `SQLITE_IOERR_SHORT_READ`. WAL mode is tested in CI on Linux only.

View File

@@ -16,7 +16,7 @@ import (
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/metercollector"
"github.com/SigNoz/signoz/pkg/modules/retention"
"github.com/SigNoz/signoz/pkg/telemetryschema/metertelemetryschema"
"github.com/SigNoz/signoz/pkg/telemetrymeter"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/types/retentiontypes"
@@ -153,7 +153,7 @@ func (provider *Provider) Collect(
func buildOriginQuery(meterName string) (string, []any) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select("toInt64(ifNull(min(unix_milli), 0))")
sb.From(metertelemetryschema.DBName + "." + metertelemetryschema.SamplesTableName)
sb.From(telemetrymeter.DBName + "." + telemetrymeter.SamplesTableName)
sb.Where(sb.Equal("metric_name", meterName))
return sb.BuildWithFlavor(sqlbuilder.ClickHouse)
}
@@ -171,7 +171,7 @@ func buildQuery(meterName string, segment *retentiontypes.RetentionPolicySegment
sb := sqlbuilder.NewSelectBuilder()
sb.Select(selects...)
sb.From(metertelemetryschema.DBName + "." + metertelemetryschema.SamplesTableName)
sb.From(telemetrymeter.DBName + "." + telemetrymeter.SamplesTableName)
sb.Where(
sb.Equal("metric_name", meterName),
sb.GTE("unix_milli", segment.StartMs),

View File

@@ -276,10 +276,6 @@ func (module *module) GetV2(ctx context.Context, orgID valuer.UUID, id valuer.UU
return module.pkgDashboardModule.GetV2(ctx, orgID, id)
}
func (module *module) MigrateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.MigrateV2(ctx, orgID, id)
}
func (module *module) UpdateV2(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, updatable dashboardtypes.UpdatableDashboardV2) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.UpdateV2(ctx, orgID, id, updatedBy, updatable)
}

View File

@@ -8,16 +8,16 @@ import (
sqlbuilder "github.com/huandu/go-sqlbuilder"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/telemetryschema/metricstelemetryschema"
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/ctxtypes"
"github.com/SigNoz/signoz/pkg/types/metricreductionruletypes"
)
var (
reductionRulesTable = metricstelemetryschema.DBName + "." + metricstelemetryschema.ReductionRulesTableName
metadataTable = metricstelemetryschema.DBName + "." + metricstelemetryschema.AttributesMetadataTableName
bufferSeriesTable = metricstelemetryschema.DBName + "." + metricstelemetryschema.TimeseriesV4BufferTableName
reductionRulesTable = telemetrymetrics.DBName + "." + telemetrymetrics.ReductionRulesTableName
metadataTable = telemetrymetrics.DBName + "." + telemetrymetrics.AttributesMetadataTableName
bufferSeriesTable = telemetrymetrics.DBName + "." + telemetrymetrics.TimeseriesV4BufferTableName
)
const timeSeriesBucketMilli = int64(time.Hour / time.Millisecond)
@@ -192,7 +192,7 @@ func (c *clickhouse) ingestedSeriesCount(ctx context.Context, metricNames []stri
sb := sqlbuilder.NewSelectBuilder()
sb.Select("metric_name", "uniq(fingerprint)")
sb.From(metricstelemetryschema.DBName + "." + metricstelemetryschema.SamplesV4BufferTableName)
sb.From(telemetrymetrics.DBName + "." + telemetrymetrics.SamplesV4BufferTableName)
conds := []string{
sb.In("metric_name", names...),
sb.GE("unix_milli", startMs),
@@ -229,8 +229,8 @@ func (c *clickhouse) ingestedSeriesCount(ctx context.Context, metricNames []stri
// reduced sample tables.
func (c *clickhouse) reducedSeriesCount(ctx context.Context, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (map[string]uint64, error) {
out := make(map[string]uint64, len(metricNames))
for _, table := range []string{metricstelemetryschema.SamplesV4ReducedLastTableName, metricstelemetryschema.SamplesV4ReducedSumTableName} {
counts, err := c.reducedSeriesCountForTable(ctx, metricstelemetryschema.DBName+"."+table, metricNames, effectiveFrom, startMs, endMs)
for _, table := range []string{telemetrymetrics.SamplesV4ReducedLastTableName, telemetrymetrics.SamplesV4ReducedSumTableName} {
counts, err := c.reducedSeriesCountForTable(ctx, telemetrymetrics.DBName+"."+table, metricNames, effectiveFrom, startMs, endMs)
if err != nil {
return nil, err
}
@@ -300,9 +300,9 @@ func (c *clickhouse) RankByVolume(ctx context.Context, metricNames []string, eff
direction = "DESC"
}
ingestedTable := metricstelemetryschema.DBName + "." + metricstelemetryschema.SamplesV4BufferTableName
reducedLast := metricstelemetryschema.DBName + "." + metricstelemetryschema.SamplesV4ReducedLastTableName
reducedSum := metricstelemetryschema.DBName + "." + metricstelemetryschema.SamplesV4ReducedSumTableName
ingestedTable := telemetrymetrics.DBName + "." + telemetrymetrics.SamplesV4BufferTableName
reducedLast := telemetrymetrics.DBName + "." + telemetrymetrics.SamplesV4ReducedLastTableName
reducedSum := telemetrymetrics.DBName + "." + telemetrymetrics.SamplesV4ReducedSumTableName
sb := sqlbuilder.NewSelectBuilder()
sb.Select("base.metric_name AS metric_name", "ifNull(i.cnt, 0) AS ingested", "ifNull(d.cnt, 0) AS reduced")
@@ -352,16 +352,16 @@ func (c *clickhouse) SampleVolumeByMetric(ctx context.Context, metricNames []str
}
ctx = c.withThreads(ctx)
ingested, err := c.countSamplesByMetric(ctx, metricstelemetryschema.DBName+"."+metricstelemetryschema.SamplesV4BufferTableName, "count()", metricNames, effectiveFrom, startMs, endMs)
ingested, err := c.countSamplesByMetric(ctx, telemetrymetrics.DBName+"."+telemetrymetrics.SamplesV4BufferTableName, "count()", metricNames, effectiveFrom, startMs, endMs)
if err != nil {
return nil, err
}
last, err := c.countSamplesByMetric(ctx, metricstelemetryschema.DBName+"."+metricstelemetryschema.SamplesV4ReducedLastTableName, "uniq(reduced_fingerprint, unix_milli)", metricNames, effectiveFrom, startMs, endMs)
last, err := c.countSamplesByMetric(ctx, telemetrymetrics.DBName+"."+telemetrymetrics.SamplesV4ReducedLastTableName, "uniq(reduced_fingerprint, unix_milli)", metricNames, effectiveFrom, startMs, endMs)
if err != nil {
return nil, err
}
sum, err := c.countSamplesByMetric(ctx, metricstelemetryschema.DBName+"."+metricstelemetryschema.SamplesV4ReducedSumTableName, "uniq(reduced_fingerprint, unix_milli)", metricNames, effectiveFrom, startMs, endMs)
sum, err := c.countSamplesByMetric(ctx, telemetrymetrics.DBName+"."+telemetrymetrics.SamplesV4ReducedSumTableName, "uniq(reduced_fingerprint, unix_milli)", metricNames, effectiveFrom, startMs, endMs)
if err != nil {
return nil, err
}
@@ -419,7 +419,7 @@ func (c *clickhouse) TotalVolume(ctx context.Context, startMs, endMs int64) (uin
sb := sqlbuilder.NewSelectBuilder()
sb.Select("uniq(fingerprint)", "count()")
sb.From(metricstelemetryschema.DBName + "." + metricstelemetryschema.SamplesV4BufferTableName)
sb.From(telemetrymetrics.DBName + "." + telemetrymetrics.SamplesV4BufferTableName)
sb.Where(sb.GE("unix_milli", startMs), sb.LT("unix_milli", endMs))
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
@@ -470,7 +470,7 @@ func (c *clickhouse) SampleTimeseries(ctx context.Context, ruledMetrics []string
func (c *clickhouse) totalSamplesByBucket(ctx context.Context, startMs, endMs int64) (map[int64]uint64, error) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select(sampleBucketExpr, "count()")
sb.From(metricstelemetryschema.DBName + "." + metricstelemetryschema.SamplesV4BufferTableName)
sb.From(telemetrymetrics.DBName + "." + telemetrymetrics.SamplesV4BufferTableName)
sb.Where(sb.GE("unix_milli", startMs), sb.LT("unix_milli", endMs))
sb.GroupBy("bucket")
@@ -485,7 +485,7 @@ func (c *clickhouse) ruledIngestedSamplesByBucket(ctx context.Context, metricNam
sb := sqlbuilder.NewSelectBuilder()
sb.Select(sampleBucketExpr, "count()")
sb.From(metricstelemetryschema.DBName + "." + metricstelemetryschema.SamplesV4BufferTableName)
sb.From(telemetrymetrics.DBName + "." + telemetrymetrics.SamplesV4BufferTableName)
conds := []string{sb.In("metric_name", names...), sb.GE("unix_milli", startMs), sb.LT("unix_milli", endMs)}
if len(effectiveFrom) > 0 {
conds = append(conds, strictEffectiveFrom(sb, metricNames, effectiveFrom))
@@ -499,7 +499,7 @@ func (c *clickhouse) ruledIngestedSamplesByBucket(ctx context.Context, metricNam
// reduced 60s rows are versioned by computed_at, so count distinct buckets.
func (c *clickhouse) ruledRetainedSamplesByBucket(ctx context.Context, metricNames []string, effectiveFrom map[string]int64, startMs, endMs int64) (map[int64]uint64, error) {
out := make(map[int64]uint64)
for _, table := range []string{metricstelemetryschema.SamplesV4ReducedLastTableName, metricstelemetryschema.SamplesV4ReducedSumTableName} {
for _, table := range []string{telemetrymetrics.SamplesV4ReducedLastTableName, telemetrymetrics.SamplesV4ReducedSumTableName} {
names := make([]any, len(metricNames))
for i, name := range metricNames {
names[i] = name
@@ -507,7 +507,7 @@ func (c *clickhouse) ruledRetainedSamplesByBucket(ctx context.Context, metricNam
sb := sqlbuilder.NewSelectBuilder()
sb.Select(sampleBucketExpr, "uniq(reduced_fingerprint, unix_milli)")
sb.From(metricstelemetryschema.DBName + "." + table)
sb.From(telemetrymetrics.DBName + "." + table)
conds := []string{sb.In("metric_name", names...), sb.GE("unix_milli", startMs), sb.LT("unix_milli", endMs)}
if len(effectiveFrom) > 0 {
conds = append(conds, strictEffectiveFrom(sb, metricNames, effectiveFrom))

View File

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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 139 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 49 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 86 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -1,136 +0,0 @@
# AuthZ Guide
How to structure a page so it works with the permission system.
We are migrating from the `ADMIN | EDITOR | VIEWER` roles to per-action checks. Instead of granting `VIEWER` and
exposing everything, a user can now be granted access to a single resource, eg: `Logs` only.
- [Prerequisites](#prerequisites)
- [Core rules](#core-rules)
- [Page patterns](#page-patterns)
- [What "blocked" means](#what-blocked-means)
- [Migration checklist](#migration-checklist)
- [Testing](#testing)
## Prerequisites
Check whether the resource your page represents is supported: see
[permissions.type.ts](../src/lib/authz/hooks/useAuthZ/permissions.type.ts) for the current resources and their allowed
verbs.
**If the resource is not listed there, skip authz for now**. The backend does not enforce it yet, so any frontend
check would be decorative. Revisit once the resource is generated into that file (it is auto-generated from the
backend).
## Core rules
These hold for every page. The per-pattern sections below only add to them.
1. **A page is always reachable, regardless of permission.** The intended action must be known before permission can
be checked, so the route renders first and individual pieces are gated.
2. **`update` requires both `read` and `update`.** A user who can write but cannot read the current value must not get
an edit affordance.
3. **`delete` is independent of `read`.** Delete controls stay visible for a user who holds `delete` but not `read`.
4. **Never gate per row.** If a user can `list`, render every row. Check `read` only when the row is opened (drawer or
detail route).
5. **Gate the narrowest thing that works**, a button over a section, a section over a page.
6. **A resource may be gated while a sub-resource is not.** A user without `read` on Service Accounts can still hold
`create` on API Keys, so blocking the outer container would hide work they are allowed to do.
7. **Verbs not covered here** (`attach`, `detach`, `assignee`) behave like `delete`: gate the control that triggers
them, not the surrounding content.
## Page patterns
### List page
![Visual rules for structuring a list page](./assets/list-example.svg)
Without `list`, but with any of `read` / `create` / `update`, only the table is blocked:
- Title, description, search filters and action buttons stay visible.
- Filters and any control that drives the table are non-interactive.
- The create button stays enabled if the user holds `create`.
### Edit page
![Visual rules for structuring an edit page](./assets/edit-example.svg)
Without `read`:
- Block the content with `withAuthZContent`, not the whole route.
- Keep delete visible (rule 3).
- Keep update blocked (rule 2).
### Drawer
![Visual rules for structuring a drawer](./assets/drawer-example.svg)
Without `read`:
- Block the drawer body, not the drawer itself.
- Prefer routing that carries the resource ID in the URL, so delete stays reachable without `read`.
- Keep update blocked (rule 2).
- Watch for sub-resources the user may still be allowed to act on (rule 6).
### Create
Without `create`:
| Entry point | Gate with |
| --------------------- | ------------------------------------------------------------- |
| Button | `AuthZButton` |
| Dedicated create page | `withAuthZPage` |
| Create drawer opened directly (deep link) | `withAuthZContent` on the body + `AuthZButton` on footer actions |
### Delete
Gate the control with `AuthZButton`, or `AuthZTooltip` for a non-button trigger such as an icon or menu item.
### Quick filters
![Visual rules for structuring a page with quick filters](./assets/quick-filters-example.svg)
## What "blocked" means
Blocking is always a visible denial, never a silent removal. Use the components in
[`lib/authz/components`](../src/lib/authz/components/README.md) rather than hand-rolling a check, they carry the
denial message and the loading state.
| Scope | Component | Denied state |
| --------------- | ----------------------------------------- | ------------------------------------------- |
| Button | `AuthZButton` | Disabled + tooltip |
| Any element | `AuthZTooltip` | Disabled child + tooltip |
| Section | `withAuthZContent` / `AuthZGuardContent` | Inline `PermissionDeniedCallout` |
| Page / route | `withAuthZPage` / `AuthZGuardPage` | `PermissionDeniedFullPage` |
| Custom fallback | `withAuthZ` / `AuthZGuard` | Whatever you pass as `fallback` |
Prefer the HOC (`withAuthZ*`); reach for the JSX guard (`AuthZGuard*`) when the gate depends on conditional rendering
and an HOC cannot express it. The components README has the full decision tree and how to build the `checks` array.
## Migration checklist
Use the existing components. Create a new one only if none fit.
- [ ] Can I apply a `withAuthZ*` variant directly, or must I extract the content into a component first?
- If extraction is needed, declare the new content component in the same file to keep the diff small, then move it
to its own file in a follow-up commit or PR.
- [ ] Is the layout structured to respect the [core rules](#core-rules)?
- If not, raise it in the frontend Slack channel before reshaping the page.
- [ ] Am I gating a shared component rather than a page or section?
- If so, it must stay functional with no permission. Example: the Query Builder works without field suggestions when
the user cannot read them.
## Testing
### Devtool
Press `Cmd + K` (`Ctrl + K` on Windows/Linux) to open the shortcuts, search for `AuthZ`, and pick the first
result. The devtool simulates granted and denied permissions across the UI.
Available only in local development, it is stripped from production builds.
### Unit tests
[`lib/authz/utils/README.md`](../src/lib/authz/utils/README.md) covers the `*.authz.test.tsx` naming convention, the
MSW handlers (`setupAuthzAdmin`, `setupAuthzDenyAll`, `setupAuthzDeny`, `setupAuthzAllow`,
`setupAuthzGrantByPrefix`), and how to test the loading state.

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

@@ -14,10 +14,7 @@ import { useAppContext } from 'providers/App/App';
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
import { OrgPreference } from 'types/api/preferences/preference';
import { USER_ROLES } from 'types/roles';
import {
routePermission,
routeWithInitialAuthZSupport,
} from 'utils/permission';
import { routePermission } from 'utils/permission';
import routes, {
LIST_LICENSES,
@@ -45,6 +42,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const isAdmin = user.role === USER_ROLES.ADMIN;
const isAIAssistantEnabled = useIsAIAssistantEnabled();
const isAIObservabilityEnabled = useIsAIObservabilityEnabled();
const mapRoutes = useMemo(
() =>
new Map(
@@ -228,16 +226,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
if (isPrivate) {
if (isLoggedInState) {
const route = routePermission[key];
const hasInitialAuthZSupport = Object.hasOwn(
routeWithInitialAuthZSupport,
key,
);
if (
route &&
route.find((e) => e === user.role) === undefined &&
hasInitialAuthZSupport === false
) {
if (route && route.find((e) => e === user.role) === undefined) {
return <Redirect to={ROUTES.UN_AUTHORIZED} />;
}
} else {

View File

@@ -17,7 +17,6 @@ import {
} from 'types/api/licensesV3/getActive';
import { OrgPreference } from 'types/api/preferences/preference';
import { ROLES, USER_ROLES } from 'types/roles';
import { routeWithInitialAuthZSupport } from 'utils/permission';
import PrivateRoute from '../Private';
@@ -179,7 +178,6 @@ function createMockAppContext(
isFetchingHosts: false,
isFetchingFeatureFlags: false,
isFetchingOrgPreferences: false,
isFetchingUserPreferences: false,
userFetchError: null,
activeLicenseFetchError: null,
hostsFetchError: null,
@@ -200,39 +198,24 @@ function createMockAppContext(
};
}
// Roles that no authz-aware route grants through the legacy routePermission table
const DENIED_ROLES: ROLES[] = [
USER_ROLES.ANONYMOUS as ROLES,
USER_ROLES.AUTHOR as ROLES,
];
const PERMITTED_ROLES: ROLES[] = [
USER_ROLES.ADMIN as ROLES,
USER_ROLES.EDITOR as ROLES,
USER_ROLES.VIEWER as ROLES,
];
interface AuthzRouteCase {
path: string;
deniedRoles: ROLES[];
}
interface RenderPrivateRouteOptions {
initialRoute?: string;
appContext?: Partial<IAppContext>;
isCloudUser?: boolean;
}
function buildPrivateRouteTree(
options: RenderPrivateRouteOptions,
): ReactElement {
const { initialRoute = ROUTES.HOME, appContext = {} } = options;
function renderPrivateRoute(options: RenderPrivateRouteOptions = {}): void {
const {
initialRoute = ROUTES.HOME,
appContext = {},
isCloudUser = true,
} = options;
const contextValue = createMockAppContext({
...appContext,
});
mockIsCloudUser = isCloudUser;
return (
const contextValue = createMockAppContext(appContext);
render(
<QueryClientProvider client={queryClient}>
<MemoryRouter initialEntries={[initialRoute]}>
<AppContext.Provider value={contextValue}>
@@ -246,15 +229,10 @@ function buildPrivateRouteTree(
</PrivateRoute>
</AppContext.Provider>
</MemoryRouter>
</QueryClientProvider>
</QueryClientProvider>,
);
}
function renderPrivateRoute(options: RenderPrivateRouteOptions = {}): void {
mockIsCloudUser = options.isCloudUser ?? true;
render(buildPrivateRouteTree(options));
}
// Generic assertion helpers for navigation behavior
// Using location-based assertions since Private.tsx now uses Redirect component
@@ -1454,25 +1432,6 @@ describe('PrivateRoute', () => {
assertStaysOnRoute(ROUTES.GET_STARTED_WITH_CLOUD);
});
it.each([...PERMITTED_ROLES, ...DENIED_ROLES])(
'should render the unauthorized page for a %s instead of bouncing off it',
(role) => {
// routePermission.UN_AUTHORIZED must grant every role: a role denied here
// is redirected to /un-authorized and then redirected away from it again,
// which loops until React bails out with a blank screen.
renderPrivateRoute({
initialRoute: ROUTES.UN_AUTHORIZED,
appContext: {
isLoggedIn: true,
user: createMockUser({ role }),
},
});
assertStaysOnRoute(ROUTES.UN_AUTHORIZED);
assertRendersChildren();
},
);
});
describe('Edge Cases', () => {
@@ -1518,174 +1477,6 @@ describe('PrivateRoute', () => {
});
});
describe('AuthZ Support (routeWithInitialAuthZSupport)', () => {
// Routes in routeWithInitialAuthZSupport always bypass legacy role check
const AUTHZ_ROUTE_CASES: Record<
keyof typeof routeWithInitialAuthZSupport,
AuthzRouteCase
> = {
// Everything under /settings resolves to the non-exact SETTINGS route
SETTINGS: { path: ROUTES.SETTINGS, deniedRoles: DENIED_ROLES },
MY_SETTINGS: { path: ROUTES.MY_SETTINGS, deniedRoles: DENIED_ROLES },
ROLES_SETTINGS: { path: ROUTES.ROLES_SETTINGS, deniedRoles: DENIED_ROLES },
ROLE_CREATE: { path: ROUTES.ROLE_CREATE, deniedRoles: DENIED_ROLES },
ROLE_DETAILS: {
path: ROUTES.ROLE_DETAILS.replace(':roleId', 'role-id-1'),
deniedRoles: DENIED_ROLES,
},
ROLE_EDIT: {
path: ROUTES.ROLE_EDIT.replace(':roleId', 'role-id-1'),
deniedRoles: DENIED_ROLES,
},
SERVICE_ACCOUNTS_SETTINGS: {
path: ROUTES.SERVICE_ACCOUNTS_SETTINGS,
deniedRoles: DENIED_ROLES,
},
TRACES_EXPLORER: { path: ROUTES.TRACES_EXPLORER, deniedRoles: DENIED_ROLES },
TRACE: { path: ROUTES.TRACE, deniedRoles: DENIED_ROLES },
TRACE_DETAIL: {
path: ROUTES.TRACE_DETAIL.replace(':id', 'trace-id-1'),
deniedRoles: DENIED_ROLES,
},
TRACE_DETAIL_OLD: {
path: ROUTES.TRACE_DETAIL_OLD.replace(':id', 'trace-id-1'),
deniedRoles: DENIED_ROLES,
},
// LOGS and LOGS_EXPLORER share a path - matchPath resolves it to whichever
// route definition comes last, and both keys are authz-aware either way.
LOGS: { path: ROUTES.LOGS, deniedRoles: DENIED_ROLES },
LOGS_EXPLORER: { path: ROUTES.LOGS_EXPLORER, deniedRoles: DENIED_ROLES },
LIVE_LOGS: { path: ROUTES.LIVE_LOGS, deniedRoles: DENIED_ROLES },
OLD_LOGS_EXPLORER: {
path: ROUTES.OLD_LOGS_EXPLORER,
deniedRoles: DENIED_ROLES,
},
METRICS_EXPLORER: {
path: ROUTES.METRICS_EXPLORER,
deniedRoles: DENIED_ROLES,
},
METRICS_EXPLORER_EXPLORER: {
path: ROUTES.METRICS_EXPLORER_EXPLORER,
deniedRoles: DENIED_ROLES,
},
METRICS_EXPLORER_VOLUME_CONTROL: {
path: ROUTES.METRICS_EXPLORER_VOLUME_CONTROL,
deniedRoles: DENIED_ROLES,
},
METER: { path: ROUTES.METER, deniedRoles: DENIED_ROLES },
METER_EXPLORER: { path: ROUTES.METER_EXPLORER, deniedRoles: DENIED_ROLES },
// SUPPORT already grants ANONYMOUS in routePermission, so only AUTHOR
// exercises the redirect branch here.
SUPPORT: {
path: ROUTES.SUPPORT,
deniedRoles: [USER_ROLES.AUTHOR as ROLES],
},
};
const authzRouteRolePairs: [string, string, ROLES][] = Object.entries(
AUTHZ_ROUTE_CASES,
).flatMap(([name, { path, deniedRoles }]) =>
deniedRoles.map((role): [string, string, ROLES] => [name, path, role]),
);
// Routes with no authz check - legacy role check still applies
const NON_AUTHZ_ROUTE_CASES: [string, string, ROLES][] = [
['ALERTS_NEW', ROUTES.ALERTS_NEW, USER_ROLES.VIEWER as ROLES],
['LIST_LICENSES', ROUTES.LIST_LICENSES, USER_ROLES.EDITOR as ROLES],
['ONBOARDING', ROUTES.ONBOARDING, USER_ROLES.VIEWER as ROLES],
['APPLICATION', ROUTES.APPLICATION, USER_ROLES.ANONYMOUS as ROLES],
[
'METRICS_EXPLORER_VIEWS',
ROUTES.METRICS_EXPLORER_VIEWS,
USER_ROLES.ANONYMOUS as ROLES,
],
];
it.each(authzRouteRolePairs)(
'should not redirect %s (%s) for role %s - authz routes bypass legacy role check',
(_name, path, role) => {
// Routes in routeWithInitialAuthZSupport always bypass the legacy role check.
// Authorization is handled by downstream components via fine-grained authz.
renderPrivateRoute({
initialRoute: path,
appContext: {
isLoggedIn: true,
user: createMockUser({ role }),
},
});
assertStaysOnRoute(path);
assertRendersChildren();
},
);
it.each([...PERMITTED_ROLES, ...DENIED_ROLES])(
'should not redirect a %s from an authz-aware route',
(role) => {
// All roles pass through - authorization handled downstream
renderPrivateRoute({
initialRoute: ROUTES.LOGS_EXPLORER,
appContext: {
isLoggedIn: true,
user: createMockUser({ role }),
},
});
assertStaysOnRoute(ROUTES.LOGS_EXPLORER);
assertRendersChildren();
},
);
it.each(NON_AUTHZ_ROUTE_CASES)(
'should redirect %s (%s) for role %s - non-authz routes use legacy role check',
async (_name, path, role) => {
// Routes NOT in routeWithInitialAuthZSupport still use legacy role check
renderPrivateRoute({
initialRoute: path,
appContext: {
isLoggedIn: true,
user: createMockUser({ role }),
},
});
await assertRedirectsTo(ROUTES.UN_AUTHORIZED);
},
);
it.each(authzRouteRolePairs)(
'should still redirect unauthenticated users away from %s (%s) with role %s',
async (_name, path, role) => {
// The authz bypass only relaxes the role check, never the login check.
renderPrivateRoute({
initialRoute: path,
appContext: {
isLoggedIn: false,
user: createMockUser({ role }),
},
});
await assertRedirectsTo(ROUTES.LOGIN);
},
);
it('should still redirect to workspace locked from an authz-aware route', async () => {
// Workspace guards run before the role check and must not be bypassed.
renderPrivateRoute({
initialRoute: ROUTES.LOGS_EXPLORER,
appContext: {
isLoggedIn: true,
isFetchingActiveLicense: false,
activeLicense: createMockLicense({ platform: LicensePlatform.CLOUD }),
trialInfo: createMockTrialInfo({ workSpaceBlock: true }),
user: createMockUser({ role: USER_ROLES.VIEWER as ROLES }),
},
isCloudUser: true,
});
await assertRedirectsTo(ROUTES.WORKSPACE_LOCKED);
});
});
describe('Old channel route redirects', () => {
it.each([
['/settings/channels', '/alerts', 'tab=Channels'],

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

View File

@@ -2,29 +2,6 @@ import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
import { AxiosError } from 'axios';
import APIError from 'types/api/error';
// The wire shape these handlers can actually rely on. The generated
// RenderErrorResponseDTO marks code/message/url/errors as required, but the
// server omits any of them even on valid errors (e.g. a 400 with just a
// message), so a present `error` object is all the guard can guarantee.
type ErrorEnvelope = {
error: {
code?: string;
message?: string;
url?: string;
errors?: { message?: string }[];
};
};
function isErrorEnvelope(data: unknown): data is ErrorEnvelope {
return (
typeof data === 'object' &&
data !== null &&
'error' in data &&
typeof (data as ErrorEnvelope).error === 'object' &&
(data as ErrorEnvelope).error !== null
);
}
// @deprecated Use convertToApiError instead
export function ErrorResponseHandlerForGeneratedAPIs(
error: AxiosError<RenderErrorResponseDTO>,
@@ -33,29 +10,15 @@ export function ErrorResponseHandlerForGeneratedAPIs(
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
if (response) {
// The body isn't guaranteed to be an error envelope — e.g. a gateway 5xx
// with an HTML/empty body during a deploy. Verify the shape before reading
// it; otherwise synthesize a consistent error from the status.
const data: unknown = response.data;
if (isErrorEnvelope(data)) {
const { code, message, url, errors } = data.error;
throw new APIError({
httpStatusCode: response.status || 500,
error: {
code: code ?? '',
message: message ?? '',
url: url ?? '',
errors: (errors ?? []).map((e) => ({ message: e.message ?? '' })),
},
});
}
throw new APIError({
httpStatusCode: response.status || 500,
error: {
code: 'UPSTREAM_UNAVAILABLE',
message: error.message || 'Something went wrong',
url: '',
errors: [],
code: response.data.error.code,
message: response.data.error.message,
url: response.data.error.url ?? '',
errors: (response.data.error.errors ?? []).map((e) => ({
message: e.message ?? '',
})),
},
});
}
@@ -99,7 +62,9 @@ export function convertToApiError(
return new APIError({
httpStatusCode: response?.status || error.status || 500,
error: {
code: errorData?.code || 'UPSTREAM_UNAVAILABLE',
code:
errorData?.code ||
String(response?.status || error.code || 'unknown_error'),
message:
errorData?.message ||
response?.statusText ||

View File

@@ -2,38 +2,19 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp } from 'types/api';
import APIError from 'types/api/error';
function isErrorV2Resp(data: unknown): data is ErrorV2Resp {
return (
typeof data === 'object' &&
data !== null &&
'error' in data &&
typeof (data as ErrorV2Resp).error?.code === 'string'
);
}
// reference - https://axios-http.com/docs/handling_errors
export function ErrorResponseHandlerV2(error: AxiosError<ErrorV2Resp>): never {
const { response, request } = error;
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
if (response) {
// response.data isn't guaranteed to be a V2 envelope (e.g. a gateway 5xx
// with an HTML/empty body during a deploy), so verify the shape first.
const data: unknown = response.data;
if (isErrorV2Resp(data)) {
const { code, message, url, errors } = data.error;
throw new APIError({
httpStatusCode: response.status || 500,
error: { code, message, url, errors },
});
}
throw new APIError({
httpStatusCode: response.status || 500,
error: {
code: 'UPSTREAM_UNAVAILABLE',
message: error.message || 'Something went wrong',
url: '',
errors: [],
code: response.data.error.code,
message: response.data.error.message,
url: response.data.error.url,
errors: response.data.error.errors,
},
});
}

View File

@@ -1,126 +0,0 @@
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp } from 'types/api';
import APIError from 'types/api/error';
function asAxiosError(partial: Partial<AxiosError>): AxiosError<ErrorV2Resp> {
return partial as AxiosError<ErrorV2Resp>;
}
// ErrorResponseHandlerV2 always throws — capture the APIError so assertions stay
// unconditional.
function runHandler(error: AxiosError<ErrorV2Resp>): APIError {
try {
ErrorResponseHandlerV2(error);
} catch (thrown) {
return thrown as APIError;
}
throw new Error('expected ErrorResponseHandlerV2 to throw');
}
type ExpectedError = {
httpStatusCode: number;
code: string;
message: string;
errors: { message: string }[];
};
// One row per response shape the handler must normalize. New shapes (with
// different bodies) can be added here without a new test block.
const cases: {
name: string;
error: AxiosError<ErrorV2Resp>;
expected: ExpectedError;
}[] = [
{
name: 'well-formed V2 error envelope',
error: asAxiosError({
message: 'Request failed with status code 400',
response: {
status: 400,
data: {
error: {
code: 'bad_request',
message: 'Invalid dashboard payload',
url: 'https://signoz.io/docs',
errors: [{ message: 'name is required' }, { message: 'name too long' }],
},
},
} as AxiosError['response'],
}),
expected: {
httpStatusCode: 400,
code: 'bad_request',
message: 'Invalid dashboard payload',
errors: [{ message: 'name is required' }, { message: 'name too long' }],
},
},
{
// Regression: during a deployment the gateway returns a 5xx with a
// non-envelope body. Reading response.data.error.code used to throw a
// TypeError from inside the handler itself. See engineering-pod#5760.
name: '5xx with a non-envelope HTML body',
error: asAxiosError({
message: 'Request failed with status code 503',
response: {
status: 503,
data: '<html><body>503 Service Temporarily Unavailable</body></html>',
} as AxiosError['response'],
}),
expected: {
httpStatusCode: 503,
code: 'UPSTREAM_UNAVAILABLE',
message: 'Request failed with status code 503',
errors: [],
},
},
{
name: '5xx with an empty body',
error: asAxiosError({
message: 'Request failed with status code 502',
response: {
status: 502,
data: undefined,
} as AxiosError['response'],
}),
expected: {
httpStatusCode: 502,
code: 'UPSTREAM_UNAVAILABLE',
message: 'Request failed with status code 502',
errors: [],
},
},
{
name: 'no response received (network error)',
error: asAxiosError({
message: 'Network Error',
code: 'ERR_NETWORK',
name: 'AxiosError',
request: {},
}),
expected: {
httpStatusCode: 500,
code: 'ERR_NETWORK',
message: 'Network Error',
errors: [],
},
},
];
describe('ErrorResponseHandlerV2', () => {
it.each(cases)(
'normalizes $name into a consistent APIError',
({ error, expected }) => {
const apiError = runHandler(error);
expect(apiError).toBeInstanceOf(APIError);
expect(apiError.getHttpStatusCode()).toBe(expected.httpStatusCode);
expect(apiError.getErrorCode()).toBe(expected.code);
expect(apiError.getErrorMessage()).toBe(expected.message);
// The sub-error messages feed several parts of the UI, so assert them.
expect(apiError.getErrorDetails().error.errors).toStrictEqual(
expected.errors,
);
},
);
});

View File

@@ -0,0 +1,28 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AlertRuleStatsPayload } from 'types/api/alerts/def';
import { RuleStatsProps } from 'types/api/alerts/ruleStats';
const ruleStats = async (
props: RuleStatsProps,
): Promise<SuccessResponse<AlertRuleStatsPayload> | ErrorResponse> => {
try {
const response = await axios.post(`/rules/${props.id}/history/stats`, {
start: props.start,
end: props.end,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default ruleStats;

View File

@@ -0,0 +1,33 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AlertRuleTimelineGraphResponsePayload } from 'types/api/alerts/def';
import { GetTimelineGraphRequestProps } from 'types/api/alerts/timelineGraph';
const timelineGraph = async (
props: GetTimelineGraphRequestProps,
): Promise<
SuccessResponse<AlertRuleTimelineGraphResponsePayload> | ErrorResponse
> => {
try {
const response = await axios.post(
`/rules/${props.id}/history/overall_status`,
{
start: props.start,
end: props.end,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default timelineGraph;

View File

@@ -0,0 +1,36 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AlertRuleTimelineTableResponsePayload } from 'types/api/alerts/def';
import { GetTimelineTableRequestProps } from 'types/api/alerts/timelineTable';
const timelineTable = async (
props: GetTimelineTableRequestProps,
): Promise<
SuccessResponse<AlertRuleTimelineTableResponsePayload> | ErrorResponse
> => {
try {
const response = await axios.post(`/rules/${props.id}/history/timeline`, {
start: props.start,
end: props.end,
offset: props.offset,
limit: props.limit,
order: props.order,
state: props.state,
// TODO(shaheer): implement filters
filters: props.filters,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default timelineTable;

View File

@@ -0,0 +1,33 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { AlertRuleTopContributorsPayload } from 'types/api/alerts/def';
import { TopContributorsProps } from 'types/api/alerts/topContributors';
const topContributors = async (
props: TopContributorsProps,
): Promise<
SuccessResponse<AlertRuleTopContributorsPayload> | ErrorResponse
> => {
try {
const response = await axios.post(
`/rules/${props.id}/history/top_contributors`,
{
start: props.start,
end: props.end,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default topContributors;

View File

@@ -52,8 +52,6 @@ import type {
ListDashboardsV2200,
ListDashboardsV2Params,
LockDashboardV2PathParameters,
MigrateDashboardV2200,
MigrateDashboardV2PathParameters,
PatchDashboardV2200,
PatchDashboardV2PathParameters,
PinDashboardV2PathParameters,
@@ -1806,85 +1804,6 @@ export const useLockDashboardV2 = <
> => {
return useMutation(getLockDashboardV2MutationOptions(options));
};
/**
* This endpoint retries the v1→v2 (Perses) migration on a dashboard still stored in the v1 schema and returns the v2-shape result. It is idempotent: a dashboard already in the v2 schema is returned unchanged.
* @summary Migrate dashboard to v2
*/
export const migrateDashboardV2 = (
{ id }: MigrateDashboardV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<MigrateDashboardV2200>({
url: `/api/v2/dashboards/${id}/migrate`,
method: 'POST',
signal,
});
};
export const getMigrateDashboardV2MutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof migrateDashboardV2>>,
TError,
{ pathParams: MigrateDashboardV2PathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof migrateDashboardV2>>,
TError,
{ pathParams: MigrateDashboardV2PathParameters },
TContext
> => {
const mutationKey = ['migrateDashboardV2'];
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 migrateDashboardV2>>,
{ pathParams: MigrateDashboardV2PathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return migrateDashboardV2(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type MigrateDashboardV2MutationResult = NonNullable<
Awaited<ReturnType<typeof migrateDashboardV2>>
>;
export type MigrateDashboardV2MutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Migrate dashboard to v2
*/
export const useMigrateDashboardV2 = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof migrateDashboardV2>>,
TError,
{ pathParams: MigrateDashboardV2PathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof migrateDashboardV2>>,
TError,
{ pathParams: MigrateDashboardV2PathParameters },
TContext
> => {
return useMutation(getMigrateDashboardV2MutationOptions(options));
};
/**
* This endpoint returns the sanitized v2-shape dashboard data for public access. Each panel query is reduced to a safe field subset, so filters and raw query strings are not exposed.
* @summary Get public dashboard data (v2)

View File

@@ -2815,9 +2815,6 @@ export enum CloudintegrationtypesServiceIDDTO {
redis = 'redis',
cloudsql_postgres = 'cloudsql_postgres',
memorystore_redis = 'memorystore_redis',
computeengine = 'computeengine',
gke = 'gke',
cloudstorage = 'cloudstorage',
}
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
/**
@@ -4301,18 +4298,6 @@ export interface Querybuildertypesv5QueryEnvelopeBuilderDTO {
type: Querybuildertypesv5QueryEnvelopeBuilderDTOType;
}
export enum Querybuildertypesv5QueryEnvelopeBuilderAIDTOType {
builder_ai_query = 'builder_ai_query',
}
export interface Querybuildertypesv5QueryEnvelopeBuilderAIDTO {
spec?: Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5TraceAggregationDTO;
/**
* @type string
* @enum builder_ai_query
*/
type: Querybuildertypesv5QueryEnvelopeBuilderAIDTOType;
}
export interface Querybuildertypesv5QueryBuilderFormulaDTO {
/**
* @type boolean
@@ -4496,7 +4481,6 @@ export interface Querybuildertypesv5QueryEnvelopeClickHouseSQLDTO {
export type Querybuildertypesv5QueryEnvelopeDTO =
| Querybuildertypesv5QueryEnvelopeBuilderDTO
| Querybuildertypesv5QueryEnvelopeBuilderAIDTO
| Querybuildertypesv5QueryEnvelopeFormulaDTO
| Querybuildertypesv5QueryEnvelopeTraceOperatorDTO
| Querybuildertypesv5QueryEnvelopePromQLDTO
@@ -8300,7 +8284,6 @@ export interface Querybuildertypesv5QueryRangeResponseDTO {
export enum Querybuildertypesv5QueryTypeDTO {
builder_query = 'builder_query',
builder_ai_query = 'builder_ai_query',
builder_formula = 'builder_formula',
builder_trace_operator = 'builder_trace_operator',
clickhouse_sql = 'clickhouse_sql',
@@ -8493,8 +8476,6 @@ export enum RuletypesCompareOperatorDTO {
below = 'below',
equal = 'equal',
not_equal = 'not_equal',
above_or_equal = 'above_or_equal',
below_or_equal = 'below_or_equal',
outside_bounds = 'outside_bounds',
}
export interface RuletypesBasicRuleThresholdDTO {
@@ -11178,17 +11159,6 @@ export type UnlockDashboardV2PathParameters = {
export type LockDashboardV2PathParameters = {
id: string;
};
export type MigrateDashboardV2PathParameters = {
id: string;
};
export type MigrateDashboardV2200 = {
data: DashboardtypesGettableDashboardV2DTO;
/**
* @type string
*/
status: string;
};
export type GetFeatures200 = {
/**
* @type array

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 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24"><defs><style>.cls-1{fill:#aecbfa;}.cls-2{fill:#669df6;}.cls-3{fill:#4285f4;}.cls-4{fill:#fff;}</style></defs><title>Icon_24px_CloudStorage_Color</title><g data-name="Product Icons"><rect class="cls-1" x="2" y="4" width="20" height="7"/><rect class="cls-2" x="20" y="4" width="2" height="7"/><polygon class="cls-3" points="22 4 20 4 20 11 22 4"/><rect class="cls-2" x="2" y="4" width="2" height="7"/><rect class="cls-4" x="6" y="7" width="6" height="1"/><rect class="cls-4" x="15" y="6" width="3" height="3" rx="1.5"/><rect class="cls-1" x="2" y="13" width="20" height="7"/><rect class="cls-2" x="20" y="13" width="2" height="7"/><polygon class="cls-3" points="22 13 20 13 20 20 22 13"/><rect class="cls-2" x="2" y="13" width="2" height="7"/><rect class="cls-4" x="6" y="16" width="6" height="1"/><rect class="cls-4" x="15" y="15" width="3" height="3" rx="1.5"/></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1200" height="800" viewBox="-19.2 -28.483 166.401 170.898"><g transform="translate(0 -7.034)"><linearGradient id="a" x1="64" x2="64" y1="7.034" y2="120.789" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#4387fd"/><stop offset="1" stop-color="#4683ea"/></linearGradient><path fill="url(#a)" d="M27.79 115.217 1.54 69.749a11.5 11.5 0 0 1 0-11.499l26.25-45.467a11.5 11.5 0 0 1 9.96-5.75h52.5a11.5 11.5 0 0 1 9.959 5.75l26.25 45.467a11.5 11.5 0 0 1 0 11.5l-26.25 45.466a11.5 11.5 0 0 1-9.959 5.75h-52.5a11.5 11.5 0 0 1-9.96-5.75z"/></g><g transform="translate(0 -7.034)"><defs><path id="b" d="M27.791 115.217 1.541 69.749a11.5 11.5 0 0 1 0-11.499l26.25-45.467a11.5 11.5 0 0 1 9.959-5.75h52.5a11.5 11.5 0 0 1 9.96 5.75l26.25 45.467a11.5 11.5 0 0 1 0 11.5l-26.25 45.466a11.5 11.5 0 0 1-9.96 5.75h-52.5a11.5 11.5 0 0 1-9.959-5.75z"/></defs><clipPath id="c"><use xlink:href="#b" width="100%" height="100%" overflow="visible"/></clipPath><path d="m49.313 53.875-7.01 6.99 5.957 5.958-5.898 10.476 44.635 44.636 10.816.002L118.936 84 85.489 50.55z" clip-path="url(#c)" opacity=".07"/></g><path fill="#fff" d="M84.7 43.236H43.264c-.667 0-1.212.546-1.212 1.214v8.566c0 .666.546 1.212 1.212 1.212H84.7c.667 0 1.213-.546 1.213-1.212v-8.568c0-.666-.545-1.213-1.212-1.213m-6.416 7.976a2.484 2.484 0 0 1-2.477-2.48 2.475 2.475 0 0 1 2.477-2.477c1.37 0 2.48 1.103 2.48 2.477a2.48 2.48 0 0 1-2.48 2.48m6.415 8.491-41.436.002c-.667 0-1.212.546-1.212 1.214v8.565c0 .666.546 1.213 1.212 1.213H84.7c.667 0 1.213-.547 1.213-1.213v-8.567c0-.666-.545-1.214-1.212-1.214m-6.416 7.976a2.483 2.483 0 0 1-2.477-2.48 2.475 2.475 0 0 1 2.477-2.477 2.48 2.48 0 1 1 0 4.956"/></svg>

Before

Width:  |  Height:  |  Size: 958 B

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24"><defs><style>.cls-1{fill:#4285f4;}.cls-1,.cls-2,.cls-4{fill-rule:evenodd;}.cls-2{fill:#669df6;}.cls-3,.cls-4{fill:#aecbfa;}</style></defs><title>Icon_24px_K8Engine_Color</title><g data-name="Product Icons"><g ><polygon class="cls-1" points="14.68 13.06 19.23 15.69 19.23 16.68 14.29 13.83 14.68 13.06"/><polygon class="cls-2" points="9.98 13.65 4.77 16.66 4.45 15.86 9.53 12.92 9.98 13.65"/><rect class="cls-3" x="11.55" y="3.29" width="0.86" height="5.78"/><path class="cls-4" d="M3.25,7V17L12,22l8.74-5V7L12,2Zm15.63,8.89L12,19.78,5.12,15.89V8.11L12,4.22l6.87,3.89v7.78Z"/><polygon class="cls-4" points="11.98 11.5 15.96 9.21 11.98 6.91 8.01 9.21 11.98 11.5"/><polygon class="cls-2" points="11.52 12.3 7.66 10.01 7.66 14.6 11.52 16.89 11.52 12.3"/><polygon class="cls-1" points="12.48 12.3 12.48 16.89 16.34 14.6 16.34 10.01 12.48 12.3"/></g></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M19.73 6.56a1.73 1.73 0 0 0-1.68 1.68 1.83 1.83 0 0 0 .89 1.48v6.9l-5.16 3.06.8 1.28 5.55-3.25a.84.84 0 0 0 .4-.69v-7.3a1.64 1.64 0 0 0 .89-1.48 1.61 1.61 0 0 0-1.69-1.68" style="fill:#aecbfa"/><path d="m18 5.48-5.61-3.16a1.18 1.18 0 0 0-.79 0L5.25 6a1.72 1.72 0 0 0-2.68 1.35A1.73 1.73 0 0 0 4.26 9a1.73 1.73 0 0 0 1.68-1.65L12 3.9l5.15 3ZM11.2 18.5a1.57 1.57 0 0 0-.89.29l-5.16-3V9.92H3.56v6.31a.84.84 0 0 0 .4.69L9.52 20v.09a1.69 1.69 0 0 0 3.37 0 1.65 1.65 0 0 0-1.69-1.59" data-name="Path" style="fill:#aecbfa"/><path d="M16.96 8.63 12.1 5.78 7.13 8.63l4.97 2.77z" data-name="Path" style="fill:#669df6"/><path d="M12.1 12.38 6.84 9.32v2.47l5.26 2.96zM12.1 15.73l-5.26-3.05v2.07l5.26 3.06z" style="fill:#669df6"/><path d="M12.09 12.38v2.37l5.26-3.06V9.33Zm4.32-.94a.38.38 0 1 1 0-.76.38.38 0 0 1 0 .76M12.09 15.73v2.07l5.26-3.05v-2.07Zm4.32-1.07a.38.38 0 1 1 0-.76.38.38 0 0 1 0 .76" style="fill:#4285f4"/></svg>

Before

Width:  |  Height:  |  Size: 941 B

After

Width:  |  Height:  |  Size: 988 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

@@ -3,7 +3,6 @@ import { useQueryClient } from 'react-query';
import { X } from '@signozhq/icons';
import { Button } from '@signozhq/ui/button';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import { AuthZGuardContent } from 'lib/authz/components/AuthZGuard/AuthZGuardContent';
import { SACreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import { DialogFooter, DialogWrapper } from '@signozhq/ui/dialog';
import { Input } from '@signozhq/ui/input';
@@ -21,7 +20,6 @@ import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import './CreateServiceAccountModal.styles.scss';
import { Skeleton } from 'antd';
interface FormValues {
name: string;
@@ -97,39 +95,33 @@ function CreateServiceAccountModal(): JSX.Element {
testId="create-service-account-modal"
>
<div className="create-sa-modal__content">
<AuthZGuardContent
checks={[SACreatePermission]}
fallbackOnLoading={<Skeleton active paragraph={{ rows: 1 }} />}
<form
id="create-sa-form"
className="create-sa-form"
onSubmit={handleSubmit(handleCreate)}
>
<form
id="create-sa-form"
className="create-sa-form"
onSubmit={handleSubmit(handleCreate)}
>
<div className="create-sa-form__item">
<label htmlFor="sa-name">Name</label>
<Controller
name="name"
control={control}
rules={{ required: 'Name is required' }}
render={({ field }): JSX.Element => (
<Input
id="sa-name"
placeholder="Enter a name"
className="create-sa-form__input"
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
data-testid="create-sa-name-input"
/>
)}
/>
{errors.name && (
<p className="create-sa-form__error">{errors.name.message}</p>
<div className="create-sa-form__item">
<label htmlFor="sa-name">Name</label>
<Controller
name="name"
control={control}
rules={{ required: 'Name is required' }}
render={({ field }): JSX.Element => (
<Input
id="sa-name"
placeholder="Enter a name"
className="create-sa-form__input"
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
/>
)}
</div>
</form>
</AuthZGuardContent>
/>
{errors.name && (
<p className="create-sa-form__error">{errors.name.message}</p>
)}
</div>
</form>
</div>
<DialogFooter className="create-sa-modal__footer">
@@ -138,7 +130,6 @@ function CreateServiceAccountModal(): JSX.Element {
variant="solid"
color="secondary"
onClick={handleClose}
data-testid="create-sa-cancel-btn"
>
<X size={12} />
Cancel
@@ -146,14 +137,12 @@ function CreateServiceAccountModal(): JSX.Element {
<AuthZButton
checks={[SACreatePermission]}
withPortal={false}
type="submit"
form="create-sa-form"
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
data-testid="create-sa-submit-btn"
>
Create Service Account
</AuthZButton>

View File

@@ -1,14 +1,19 @@
import { toast } from '@signozhq/ui/sonner';
import {
setupAuthzAdmin,
setupAuthzDenyAll,
} from 'lib/authz/utils/authz-test-utils';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import CreateServiceAccountModal from '../CreateServiceAccountModal';
jest.mock('lib/authz/components/AuthZTooltip/AuthZTooltip', () => ({
__esModule: true,
default: ({
children,
}: {
children: React.ReactElement;
}): React.ReactElement => children,
}));
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: { success: jest.fn(), error: jest.fn() },
@@ -40,7 +45,6 @@ describe('CreateServiceAccountModal', () => {
beforeEach(() => {
jest.clearAllMocks();
server.use(
setupAuthzAdmin(),
rest.post(SERVICE_ACCOUNTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(201), ctx.json({ status: 'success', data: {} })),
),
@@ -51,41 +55,23 @@ describe('CreateServiceAccountModal', () => {
server.resetHandlers();
});
it('submit button is disabled while the form is empty', async () => {
it('submit button is disabled when form is empty', () => {
renderModal();
// The form only renders once the create check resolves, and the name field
// registers its `required` rule on mount, so the empty-form invalid state
// settles a tick later.
await screen.findByTestId('create-sa-name-input');
await waitFor(() =>
expect(screen.getByTestId('create-sa-submit-btn')).toBeDisabled(),
);
});
it('submit button becomes disabled after clearing the name field', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
const nameInput = await screen.findByTestId('create-sa-name-input');
const submitBtn = await screen.findByTestId('create-sa-submit-btn');
await user.type(nameInput, 'test');
await waitFor(() => expect(submitBtn).not.toBeDisabled());
await user.clear(nameInput);
await waitFor(() => expect(submitBtn).toBeDisabled());
expect(
screen.getByRole('button', { name: /Create Service Account/i }),
).toBeDisabled();
});
it('successful submit shows toast.success and closes modal', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
const nameInput = await screen.findByTestId('create-sa-name-input');
await user.type(nameInput, 'Deploy Bot');
await user.type(screen.getByPlaceholderText('Enter a name'), 'Deploy Bot');
const submitBtn = screen.getByTestId('create-sa-submit-btn');
const submitBtn = screen.getByRole('button', {
name: /Create Service Account/i,
});
await waitFor(() => expect(submitBtn).not.toBeDisabled());
await user.click(submitBtn);
@@ -116,10 +102,11 @@ describe('CreateServiceAccountModal', () => {
renderModal();
const nameInput = await screen.findByTestId('create-sa-name-input');
await user.type(nameInput, 'Dupe Bot');
await user.type(screen.getByPlaceholderText('Enter a name'), 'Dupe Bot');
const submitBtn = screen.getByTestId('create-sa-submit-btn');
const submitBtn = screen.getByRole('button', {
name: /Create Service Account/i,
});
await waitFor(() => expect(submitBtn).not.toBeDisabled());
await user.click(submitBtn);
@@ -144,45 +131,8 @@ describe('CreateServiceAccountModal', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
const cancelBtn = await screen.findByTestId('create-sa-cancel-btn');
await user.click(cancelBtn);
await waitFor(() => {
expect(
screen.queryByTestId('create-service-account-modal'),
).not.toBeInTheDocument();
});
});
it('shows inline permission denial and hides the form when create permission is denied', async () => {
server.use(setupAuthzDenyAll());
renderModal();
await expect(
screen.findByText(/is not authorized to perform/i),
).resolves.toBeInTheDocument();
expect(screen.queryByTestId('create-sa-name-input')).not.toBeInTheDocument();
expect(
screen.getByTestId('create-service-account-modal'),
).toBeInTheDocument();
});
it('keeps the footer usable when create permission is denied', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(setupAuthzDenyAll());
renderModal();
await expect(
screen.findByText(/is not authorized to perform/i),
).resolves.toBeInTheDocument();
// The footer lives outside the guard: submit is gated, Cancel still works.
expect(screen.getByTestId('create-sa-submit-btn')).toBeDisabled();
await user.click(screen.getByTestId('create-sa-cancel-btn'));
await screen.findByTestId('create-service-account-modal');
await user.click(screen.getByRole('button', { name: /Cancel/i }));
await waitFor(() => {
expect(
@@ -195,7 +145,7 @@ describe('CreateServiceAccountModal', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
const nameInput = await screen.findByTestId('create-sa-name-input');
const nameInput = screen.getByPlaceholderText('Enter a name');
await user.type(nameInput, 'Bot');
await user.clear(nameInput);

View File

@@ -8,6 +8,7 @@ import { buildCompositeKey } from 'container/OptionsMenu/utils';
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
import {
FieldContext,
FieldDataType,
SignalType,
TelemetryFieldKey,
} from 'types/api/v5/queryRange';
@@ -54,7 +55,7 @@ function OtherFields({
key: buildCompositeKey(attr.name, attr.fieldContext as string),
signal: attr.signal as SignalType,
fieldContext: attr.fieldContext as FieldContext,
fieldDataType: attr.fieldDataType,
fieldDataType: attr.fieldDataType as FieldDataType,
}),
);
const addedIds = new Set(

View File

@@ -6,7 +6,6 @@ import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { VIEWS } from './constants';
import { MouseEventHandler } from 'react';
export type LogDetailProps = {
log: ILog | null;
@@ -17,8 +16,6 @@ export type LogDetailProps = {
logs?: ILog[];
onNavigateLog?: (log: ILog) => void;
onScrollToLog?: (logId: string) => void;
handleOpenInExplorer?: MouseEventHandler;
getContainer?: DrawerProps['getContainer'];
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
Pick<DrawerProps, 'onClose'>;

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useCopyToClipboard } from 'react-use';
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
import { useCopyToClipboard, useLocation } from 'react-use';
import { Color, Spacing } from '@signozhq/design-tokens';
import { Button } from '@signozhq/ui/button';
import { Drawer, Tooltip } from 'antd';
@@ -13,6 +14,9 @@ import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySear
import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import { QueryParams } from 'constants/query';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
import InfraMetrics from 'container/LogDetailedView/InfraMetrics/InfraMetrics';
import Overview from 'container/LogDetailedView/Overview';
@@ -27,6 +31,8 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import createQueryParams from 'lib/createQueryParams';
import { cloneDeep } from 'lodash-es';
import {
ArrowDown,
@@ -44,9 +50,12 @@ import {
} from '@signozhq/icons';
import { JsonView } from 'periscope/components/JsonView';
import { useAppContext } from 'providers/App/App';
import { AppState } from 'store/reducers';
import { ILogBody } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed } from 'utils/app';
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
@@ -66,8 +75,6 @@ function LogDetailInner({
logs,
onNavigateLog,
onScrollToLog,
handleOpenInExplorer,
getContainer,
}: LogDetailInnerProps): JSX.Element {
const initialContextQuery = useInitialQuery(log);
const [contextQuery, setContextQuery] = useState<Query | undefined>(
@@ -84,7 +91,7 @@ function LogDetailInner({
const [filters, setFilters] = useState<TagFilter | null>(null);
const [isEdit, setIsEdit] = useState<boolean>(false);
const { stagedQuery } = useQueryBuilder();
const { stagedQuery, updateAllQueriesOperators } = useQueryBuilder();
// Handle clicks outside to close drawer, except on explicitly ignored regions
useEffect(() => {
@@ -179,6 +186,11 @@ function LogDetailInner({
});
const isDarkMode = useIsDarkMode();
const location = useLocation();
const { safeNavigate } = useSafeNavigate();
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { notifications } = useNotifications();
@@ -234,6 +246,25 @@ function LogDetailInner({
});
};
// Go to logs explorer page with the log data
const handleOpenInExplorer = (e?: React.MouseEvent): void => {
const queryParams = {
[QueryParams.activeLogId]: `"${log?.id}"`,
[QueryParams.startTime]: minTime?.toString() || '',
[QueryParams.endTime]: maxTime?.toString() || '',
[QueryParams.compositeQuery]: JSON.stringify(
updateAllQueriesOperators(
initialQueriesMap[DataSource.LOGS],
PANEL_TYPES.LIST,
DataSource.LOGS,
),
),
};
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`, {
newTab: !!e && isModifierKeyPressed(e),
});
};
const handleQueryExpressionChange = useCallback(
(value: string, queryIndex: number) => {
// update the query at the given index
@@ -302,6 +333,13 @@ function LogDetailInner({
}
};
// Only show when opened from infra monitoring page
const showOpenInExplorerBtn = useMemo(
() => location.pathname?.includes('/infrastructure-monitoring'),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const logType = log?.attributes_string?.log_level || LogType.INFO;
const currentLogIndex = logs ? logs.findIndex((l) => l.id === log.id) : -1;
const isPrevDisabled =
@@ -336,7 +374,6 @@ function LogDetailInner({
width="60%"
mask={false}
maskClosable={false}
getContainer={getContainer}
title={
<div className="log-detail-drawer__title" data-log-detail-ignore="true">
<div className="log-detail-drawer__title-left">
@@ -374,7 +411,7 @@ function LogDetailInner({
/>
</Tooltip>
</div>
{handleOpenInExplorer && (
{showOpenInExplorerBtn && (
<div>
<Button
variant="outlined"
@@ -401,11 +438,7 @@ function LogDetailInner({
destroyOnClose
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
>
<div
className="log-detail-drawer__content"
data-log-detail-ignore="true"
data-testid="log-detail-drawer"
>
<div className="log-detail-drawer__content" data-log-detail-ignore="true">
<div className="log-detail-drawer__log">
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
<Tooltip

View File

@@ -25,12 +25,12 @@ import CodeMirror, {
} from '@uiw/react-codemirror';
import { Button, Popover, Tooltip } from 'antd';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { QUERY_BUILDER_KEY_TYPES } from 'constants/antlrQueryConstants';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { tracesAggregateOperatorOptions } from 'constants/queryBuilderOperators';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Info, TriangleAlert } from '@signozhq/icons';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { FieldDataType } from 'types/api/v5/queryRange';
import { TracesAggregatorOperator } from 'types/common/queryBuilder';
import { useQueryBuilderV2Context } from '../../QueryBuilderV2Context';
@@ -323,16 +323,16 @@ function QueryAggregationSelect({
TracesAggregatorOperator.RATE,
];
const fieldDataType: FieldDataType | undefined =
const fieldDataType =
functionContextForFetch &&
operatorsWithoutDataType.includes(functionContextForFetch)
? undefined
: 'number';
: QUERY_BUILDER_KEY_TYPES.NUMBER;
return getKeySuggestions({
signal: queryData.dataSource,
searchText: '',
fieldDataType,
fieldDataType: fieldDataType as QUERY_BUILDER_KEY_TYPES,
});
},
{

View File

@@ -49,11 +49,7 @@ import { unquote } from 'utils/stringUtils';
import { getRecentQueries } from 'lib/recentQueries/getRecentQueries';
import type { SignalType } from 'types/api/v5/queryRange';
import {
queryExamples,
SUGGESTION_FETCH_DEBOUNCE_MS,
SUGGESTIONS_SECTION,
} from './constants';
import { queryExamples, SUGGESTIONS_SECTION } from './constants';
import {
combineInitialAndUserExpression,
dedupeOptionsByLabel,
@@ -102,15 +98,6 @@ interface QuerySearchProps {
showFilterSuggestionsWithoutMetric?: boolean;
/** When set, the editor shows only the user expression; API/filter uses `initial AND (user)`. */
initialExpression?: string;
/** When set, replaces the generic value-suggestion API with a custom fetcher. */
valueSuggestionsOverride?: (
key: string,
searchText: string,
) => Promise<{
stringValues: string[];
numberValues: number[];
complete: boolean;
}>;
}
function QuerySearch({
@@ -124,7 +111,6 @@ function QuerySearch({
showFilterSuggestionsWithoutMetric,
initialExpression,
metricNamespace,
valueSuggestionsOverride,
}: QuerySearchProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
@@ -367,7 +353,7 @@ function QuerySearch({
);
const debouncedFetchKeySuggestions = useMemo(
() => debounce(fetchKeySuggestions, SUGGESTION_FETCH_DEBOUNCE_MS),
() => debounce(fetchKeySuggestions, 300),
[fetchKeySuggestions],
);
@@ -494,24 +480,13 @@ function QuerySearch({
const sanitizedSearchText = searchText ? searchText?.trim() : '';
try {
const values = valueSuggestionsOverride
? await valueSuggestionsOverride(key, sanitizedSearchText)
: await getValueSuggestions({
key,
searchText: sanitizedSearchText,
signal: dataSource,
signalSource: signalSource as 'meter' | '',
metricName: debouncedMetricName ?? undefined,
}).then((response) => {
const responseData = response.data as any;
const data = responseData.data || {};
const values = data.values || {};
return {
stringValues: values.stringValues || [],
numberValues: values.numberValues || [],
complete: data.complete ?? false,
};
});
const response = await getValueSuggestions({
key,
searchText: sanitizedSearchText,
signal: dataSource,
signalSource: signalSource as 'meter' | '',
metricName: debouncedMetricName ?? undefined,
});
// Skip updates if component unmounted or key changed
if (
@@ -523,6 +498,8 @@ function QuerySearch({
}
// Process the response data
const responseData = response.data as any;
const values = responseData.data?.values || {};
const stringValues = values.stringValues || [];
const numberValues = values.numberValues || [];
@@ -603,12 +580,11 @@ function QuerySearch({
debouncedMetricName,
signalSource,
toggleSuggestions,
valueSuggestionsOverride,
],
);
const debouncedFetchValueSuggestions = useMemo(
() => debounce(fetchValueSuggestions, SUGGESTION_FETCH_DEBOUNCE_MS),
() => debounce(fetchValueSuggestions, 300),
[fetchValueSuggestions],
);

View File

@@ -1,8 +1,6 @@
export const RECENTS_SECTION = { name: 'Recent searches', rank: 1 } as const;
export const SUGGESTIONS_SECTION = { name: 'Suggestions', rank: 2 } as const;
export const SUGGESTION_FETCH_DEBOUNCE_MS = 300;
// TODO: move to using TelemetrytypesFieldContextDTO when we migrate getKeySuggestions API (Part of https://github.com/SigNoz/engineering-pod/issues/5289)
export const FIELD_CONTEXTS = [
'attribute',

View File

@@ -23,13 +23,6 @@ jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
}),
}));
// Shrink the suggestion-fetch debounce (300ms in prod) so these integration
// tests aren't paced by it; coalescing semantics stay intact.
jest.mock('../QuerySearch/constants', () => ({
...jest.requireActual('../QuerySearch/constants'),
SUGGESTION_FETCH_DEBOUNCE_MS: 30,
}));
jest.mock('hooks/queryBuilder/useQueryBuilder', () => {
const handleRunQuery = jest.fn();
return {

View File

@@ -92,76 +92,52 @@
color: var(--l3-foreground);
}
.only-btn,
.only-btn {
cursor: not-allowed;
color: var(--l3-foreground);
}
.toggle-btn {
cursor: not-allowed;
color: var(--l3-foreground);
}
}
// Stack "Only"/"All" and "Toggle" in a single cell so revealing
// one swaps in place instead of shifting the row layout.
.value-actions {
display: grid;
align-items: center;
justify-items: end;
> * {
grid-area: 1 / 1;
}
.only-btn {
display: none;
}
.only-btn,
.toggle-btn {
display: none;
}
.toggle-btn:hover {
background-color: unset;
}
.only-btn:hover {
background-color: unset;
}
}
.checkbox-value-section:hover {
.toggle-btn {
display: none;
}
.only-btn {
display: flex;
align-items: center;
justify-content: center;
height: 21px;
opacity: 0;
transform: translateX(4px);
// `display` is animated as a discrete property so the button
// stays mounted through its fade-out on exit.
transition:
opacity 0.16s ease,
transform 0.16s ease,
display 0.16s allow-discrete;
&:hover {
background-color: unset;
}
}
}
// Hovering the label/value area reveals the "Only"/"All" action.
.checkbox-value-section:hover .only-btn {
display: flex;
opacity: 1;
transform: translateX(0);
// Entry animation start state (fires on the display switch).
@starting-style {
opacity: 0;
transform: translateX(4px);
}
}
// Hovering the checkbox reveals the "Toggle" action.
.check-box:hover ~ .checkbox-value-section .toggle-btn {
display: flex;
opacity: 1;
transform: translateX(0);
@starting-style {
opacity: 0;
transform: translateX(4px);
}
}
}
@media (prefers-reduced-motion: reduce) {
.only-btn,
.value:hover {
.toggle-btn {
transition: none;
display: flex;
align-items: center;
justify-content: center;
height: 21px;
}
}
}

View File

@@ -5,7 +5,6 @@ import { Skeleton } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import {
IQuickFiltersConfig,
QuickFilterChangeEventData,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
@@ -29,12 +28,11 @@ interface ICheckboxProps {
filter: IQuickFiltersConfig;
source: QuickFiltersSource;
onFilterChange?: (query: Query) => void;
onQuickFilterChange?: (data: QuickFilterChangeEventData) => void;
}
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
const { source, filter, onFilterChange, onQuickFilterChange } = props;
const { source, filter, onFilterChange } = props;
const [searchText, setSearchText] = useState<string>('');
const activeQueryIndex = useActiveQueryIndex(source);
@@ -63,7 +61,6 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
attributeValues,
activeQueryIndex,
onFilterChange,
onQuickFilterChange,
});
const setSearchTextDebounced = useDebouncedFn((...args) => {

View File

@@ -53,14 +53,12 @@ function CheckboxValueRow({
</Typography.Text>
</TooltipSimple>
)}
<div className="value-actions">
<Button type="text" className="only-btn">
{onlyButtonLabel}
</Button>
<Button type="text" className="toggle-btn">
Toggle
</Button>
</div>
<Button type="text" className="only-btn">
{onlyButtonLabel}
</Button>
<Button type="text" className="toggle-btn">
Toggle
</Button>
</div>
</div>
);

View File

@@ -1,6 +1,5 @@
import {
IQuickFiltersConfig,
QuickFilterChangeEventData,
QuickFiltersSource,
} from 'components/QuickFilters/types';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -20,7 +19,6 @@ interface UseCheckboxFilterActionsProps {
attributeValues: string[];
activeQueryIndex: number;
onFilterChange?: ((query: Query) => void) | null;
onQuickFilterChange?: (data: QuickFilterChangeEventData) => void;
}
interface UseCheckboxFilterActionsReturn {
@@ -44,7 +42,6 @@ function useCheckboxFilterActions({
attributeValues,
activeQueryIndex,
onFilterChange,
onQuickFilterChange,
}: UseCheckboxFilterActionsProps): UseCheckboxFilterActionsReturn {
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
@@ -63,34 +60,20 @@ function useCheckboxFilterActions({
previousState?: CheckedState,
sectionType?: SectionType,
): void => {
const updatedQuery = applyCheckboxToggle({
currentQuery,
activeQueryIndex,
filter,
source,
attributeValues,
value,
checked,
isOnlyOrAllClicked,
previousState,
sectionType,
});
dispatch(updatedQuery);
if (onQuickFilterChange) {
const queryData = updatedQuery.builder.queryData[activeQueryIndex];
const expression = queryData?.filter?.expression || '';
const filterItemKeys = (queryData?.filters?.items || [])
.map((item) => item.key?.key)
.filter((key): key is string => !!key);
onQuickFilterChange({
filterKey: filter.attributeKey.key,
expression,
filterItemKeys,
});
}
dispatch(
applyCheckboxToggle({
currentQuery,
activeQueryIndex,
filter,
source,
attributeValues,
value,
checked,
isOnlyOrAllClicked,
previousState,
sectionType,
}),
);
};
const onClear = (): void => {

View File

@@ -5,7 +5,6 @@ import { Typography } from '@signozhq/ui/typography';
import { LoaderCircle } from '@signozhq/icons';
import {
IQuickFiltersConfig,
QuickFilterChangeEventData,
QuickFilterCheckboxUseFieldApis,
QuickFiltersSource,
} from 'components/QuickFilters/types';
@@ -34,15 +33,13 @@ interface CheckboxFilterV2Props {
filter: IQuickFiltersConfig;
source: QuickFiltersSource;
onFilterChange?: (query: Query) => void;
onQuickFilterChange?: (data: QuickFilterChangeEventData) => void;
useFieldApis: QuickFilterCheckboxUseFieldApis;
}
export default function CheckboxFilterV2(
props: CheckboxFilterV2Props,
): JSX.Element {
const { source, filter, onFilterChange, onQuickFilterChange, useFieldApis } =
props;
const { source, filter, onFilterChange, useFieldApis } = props;
const [searchText, setSearchText] = useState<string>('');
const [userToggleState, setUserToggleState] = useState<boolean | null>(null);
@@ -106,7 +103,6 @@ export default function CheckboxFilterV2(
attributeValues,
activeQueryIndex,
onFilterChange,
onQuickFilterChange,
});
const setSearchTextDebounced = useDebouncedFn((...args) => {

View File

@@ -49,7 +49,6 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
handleFilterVisibilityChange,
source,
onFilterChange,
onQuickFilterChange,
signal,
showFilterCollapse = true,
showQueryName = true,
@@ -306,7 +305,6 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
source={source}
filter={filter}
onFilterChange={onFilterChange}
onQuickFilterChange={onQuickFilterChange}
useFieldApis={useFieldApis}
/>
) : (
@@ -315,7 +313,6 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
source={source}
filter={filter}
onFilterChange={onFilterChange}
onQuickFilterChange={onQuickFilterChange}
/>
);
case FiltersType.DURATION:
@@ -336,7 +333,6 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
source={source}
filter={filter}
onFilterChange={onFilterChange}
onQuickFilterChange={onQuickFilterChange}
useFieldApis={useFieldApis}
/>
) : (
@@ -345,7 +341,6 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
source={source}
filter={filter}
onFilterChange={onFilterChange}
onQuickFilterChange={onQuickFilterChange}
/>
);
}

View File

@@ -42,18 +42,11 @@ export interface IQuickFiltersConfig {
defaultOpen: boolean;
}
export interface QuickFilterChangeEventData {
filterKey: string;
expression: string;
filterItemKeys: string[];
}
export interface IQuickFiltersProps {
config: IQuickFiltersConfig[];
handleFilterVisibilityChange: () => void;
source: QuickFiltersSource;
onFilterChange?: (query: Query) => void;
onQuickFilterChange?: (data: QuickFilterChangeEventData) => void;
signal?: SignalType;
className?: string;
showFilterCollapse?: boolean;

View File

@@ -5,7 +5,6 @@ import { Input } from '@signozhq/ui/input';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { DatePicker } from 'antd';
import AuthZButton from 'lib/authz/components/AuthZButton/AuthZButton';
import { AuthZGuardContent } from 'lib/authz/components/AuthZGuard/AuthZGuardContent';
import {
APIKeyCreatePermission,
buildSAAttachPermission,
@@ -37,104 +36,91 @@ function KeyFormPhase({
onClose,
accountId,
}: KeyFormPhaseProps): JSX.Element {
const checks = accountId
? [APIKeyCreatePermission, buildSAAttachPermission(accountId)]
: [];
return (
<>
<form id={FORM_ID} className="add-key-modal__form" onSubmit={onSubmit}>
<AuthZGuardContent checks={checks}>
<>
<div className="add-key-modal__field">
<label className="add-key-modal__label" htmlFor="key-name">
Name <span style={{ color: 'var(--destructive)' }}>*</span>
</label>
<Input
id="key-name"
placeholder="Enter key name e.g.: Service Owner"
className="add-key-modal__input"
testId="add-key-name-input"
{...register('keyName', {
required: true,
validate: (v) => !!v.trim(),
})}
/>
</div>
<div className="add-key-modal__field">
<label className="add-key-modal__label" htmlFor="key-name">
Name <span style={{ color: 'var(--destructive)' }}>*</span>
</label>
<Input
id="key-name"
placeholder="Enter key name e.g.: Service Owner"
className="add-key-modal__input"
{...register('keyName', {
required: true,
validate: (v) => !!v.trim(),
})}
/>
</div>
<div className="add-key-modal__field">
<span className="add-key-modal__label">Expiration</span>
<div className="add-key-modal__field">
<span className="add-key-modal__label">Expiration</span>
<Controller
name="expiryMode"
control={control}
render={({ field }): JSX.Element => (
<ToggleGroupSimple
type="single"
value={field.value}
onChange={(val: string): void => {
if (val) {
field.onChange(val);
}
}}
size="sm"
className="add-key-modal__expiry-toggle"
items={[
{ value: ExpiryMode.NONE, label: 'No Expiration' },
{ value: ExpiryMode.DATE, label: 'Set Expiration Date' },
]}
/>
)}
/>
</div>
{expiryMode === ExpiryMode.DATE && (
<div className="add-key-modal__field">
<label className="add-key-modal__label" htmlFor="expiry-date">
Expiration Date
</label>
<div className="add-key-modal__datepicker">
<Controller
name="expiryMode"
name="expiryDate"
control={control}
render={({ field }): JSX.Element => (
<ToggleGroupSimple
type="single"
<DatePicker
id="expiry-date"
value={field.value}
onChange={(val: string): void => {
if (val) {
field.onChange(val);
}
}}
size="sm"
className="add-key-modal__expiry-toggle"
items={[
{ value: ExpiryMode.NONE, label: 'No Expiration' },
{ value: ExpiryMode.DATE, label: 'Set Expiration Date' },
]}
onChange={field.onChange}
popupClassName="add-key-modal-datepicker-popup"
getPopupContainer={popupContainer}
disabledDate={disabledDate}
/>
)}
/>
</div>
{expiryMode === ExpiryMode.DATE && (
<div className="add-key-modal__field">
<label className="add-key-modal__label" htmlFor="expiry-date">
Expiration Date
</label>
<div className="add-key-modal__datepicker">
<Controller
name="expiryDate"
control={control}
render={({ field }): JSX.Element => (
<DatePicker
id="expiry-date"
value={field.value}
onChange={field.onChange}
popupClassName="add-key-modal-datepicker-popup"
getPopupContainer={popupContainer}
disabledDate={disabledDate}
/>
)}
/>
</div>
</div>
)}
</>
</AuthZGuardContent>
</div>
)}
</form>
<div className="add-key-modal__footer">
<div className="add-key-modal__footer-right">
<Button
variant="solid"
color="secondary"
onClick={onClose}
testId="add-key-cancel-btn"
>
<Button variant="solid" color="secondary" onClick={onClose}>
Cancel
</Button>
<AuthZButton
checks={checks}
checks={[
APIKeyCreatePermission,
buildSAAttachPermission(accountId ?? ''),
]}
authZEnabled={!!accountId}
withPortal={false}
type="submit"
form={FORM_ID}
variant="solid"
color="primary"
loading={isSubmitting}
disabled={!isValid}
testId="add-key-submit-btn"
>
Create Key
</AuthZButton>

View File

@@ -92,7 +92,6 @@ function DeleteAccountModal(): JSX.Element {
loading={isDeleting}
onClick={handleConfirm}
data-testid="confirm-delete-btn"
withPortal={false}
>
<Trash2 size={12} />
Delete

View File

@@ -60,7 +60,6 @@ function EditKeyForm({
<AuthZTooltip
checks={[buildAPIKeyUpdatePermission(keyItem?.id ?? '')]}
enabled={!!keyItem?.id}
withPortal={false}
>
<div className="edit-key-modal__key-display">
<span className="edit-key-modal__id-text">{keyItem?.name || '—'}</span>
@@ -169,7 +168,6 @@ function EditKeyForm({
variant="link"
color="destructive"
onClick={onRevokeClick}
withPortal={false}
>
<Trash2 size={12} />
Revoke Key
@@ -188,7 +186,6 @@ function EditKeyForm({
color="primary"
loading={isSaving}
disabled={!isDirty}
withPortal={false}
>
Save Changes
</AuthZButton>

View File

@@ -114,14 +114,13 @@ function buildColumns({
render: (_, record): JSX.Element => {
const tooltipTitle = isDisabled ? 'Service account disabled' : 'Revoke Key';
return (
<Tooltip title={tooltipTitle} placement="bottom">
<Tooltip title={tooltipTitle}>
<AuthZButton
checks={[
buildAPIKeyDeletePermission(record.id),
buildSADetachPermission(accountId),
]}
authZEnabled={!isDisabled && !!accountId}
withPortal={false}
variant="ghost"
size="sm"
color="destructive"
@@ -215,7 +214,6 @@ function KeysTab({
<AuthZButton
checks={[APIKeyCreatePermission, buildSAAttachPermission(accountId)]}
authZEnabled={!isDisabled && !!accountId}
withPortal={false}
variant="link"
color="primary"
onClick={async (): Promise<void> => {

View File

@@ -90,20 +90,14 @@ function OverviewTab({
Name
</label>
{isDisabled ? (
<AuthZTooltip
checks={[buildSAUpdatePermission(account.id)]}
withPortal={false}
>
<AuthZTooltip checks={[buildSAUpdatePermission(account.id)]}>
<div className="sa-drawer__input-wrapper sa-drawer__input-wrapper--disabled">
<span className="sa-drawer__input-text">{localName || '—'}</span>
<LockKeyhole size={14} className="sa-drawer__lock-icon" />
</div>
</AuthZTooltip>
) : (
<AuthZTooltip
checks={[buildSAUpdatePermission(account.id)]}
withPortal={false}
>
<AuthZTooltip checks={[buildSAUpdatePermission(account.id)]}>
<Input
id="sa-name"
value={localName}

View File

@@ -55,7 +55,6 @@ export function RevokeKeyFooter({
color="destructive"
loading={isRevoking}
onClick={onConfirm}
withPortal={false}
>
<Trash2 size={12} />
Revoke Key

View File

@@ -38,7 +38,6 @@ import {
APIKeyCreatePermission,
buildSAAttachPermission,
buildSADeletePermission,
buildSAReadPermission,
buildSAUpdatePermission,
} from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import {
@@ -377,7 +376,6 @@ function ServiceAccountDrawer({
<AuthZButton
checks={[buildSADeletePermission(selectedAccountId ?? '')]}
authZEnabled={!!selectedAccountId}
withPortal={false}
variant="link"
color="destructive"
onClick={(): void => {
@@ -393,12 +391,8 @@ function ServiceAccountDrawer({
Cancel
</Button>
<AuthZButton
checks={[
buildSAReadPermission(selectedAccountId ?? ''),
buildSAUpdatePermission(selectedAccountId ?? ''),
]}
checks={[buildSAUpdatePermission(selectedAccountId ?? '')]}
authZEnabled={!!selectedAccountId}
withPortal={false}
variant="solid"
color="primary"
loading={isSaving}
@@ -471,7 +465,6 @@ function ServiceAccountDrawer({
buildSAAttachPermission(selectedAccountId ?? ''),
]}
authZEnabled={!isDeleted && !!selectedAccountId}
withPortal={false}
variant="outlined"
size="sm"
color="secondary"

View File

@@ -1,10 +1,5 @@
import { toast } from '@signozhq/ui/sonner';
import { buildSAAttachPermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
import {
setupAuthzAdmin,
setupAuthzDeny,
setupAuthzDenyAll,
} from 'lib/authz/utils/authz-test-utils';
import { setupAuthzAdmin } from 'lib/authz/utils/authz-test-utils';
import { rest, server } from 'mocks-server/server';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
@@ -71,29 +66,28 @@ describe('AddKeyModal', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
// The form only renders once the checks resolve, so waiting for it also
// guarantees the button is no longer in its authz-loading state.
const nameInput = await screen.findByTestId('add-key-name-input');
const createBtn = await screen.findByTestId('add-key-submit-btn');
expect(screen.getByRole('button', { name: /Create Key/i })).toBeDisabled();
expect(createBtn).toBeDisabled();
await user.type(screen.getByPlaceholderText(/Enter key name/i), 'My Key');
await user.type(nameInput, 'My Key');
await waitFor(() => expect(createBtn).not.toBeDisabled());
await user.clear(nameInput);
await waitFor(() => expect(createBtn).toBeDisabled());
await waitFor(() =>
expect(
screen.getByRole('button', { name: /Create Key/i }),
).not.toBeDisabled(),
);
});
it('successful creation transitions to phase 2 with key displayed and security callout', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
const nameInput = await screen.findByTestId('add-key-name-input');
const submitBtn = await screen.findByTestId('add-key-submit-btn');
await user.type(nameInput, 'Deploy Key');
await waitFor(() => expect(submitBtn).not.toBeDisabled());
await user.click(submitBtn);
await user.type(screen.getByPlaceholderText(/Enter key name/i), 'Deploy Key');
await waitFor(() =>
expect(
screen.getByRole('button', { name: /Create Key/i }),
).not.toBeDisabled(),
);
await user.click(screen.getByRole('button', { name: /Create Key/i }));
await screen.findByText('snz_abc123xyz456secret');
expect(screen.getByText(/Store the key securely/i)).toBeInTheDocument();
@@ -105,11 +99,13 @@ describe('AddKeyModal', () => {
renderModal();
const nameInput = await screen.findByTestId('add-key-name-input');
const submitBtn = await screen.findByTestId('add-key-submit-btn');
await user.type(nameInput, 'Deploy Key');
await waitFor(() => expect(submitBtn).not.toBeDisabled());
await user.click(submitBtn);
await user.type(screen.getByPlaceholderText(/Enter key name/i), 'Deploy Key');
await waitFor(() =>
expect(
screen.getByRole('button', { name: /Create Key/i }),
).not.toBeDisabled(),
);
await user.click(screen.getByRole('button', { name: /Create Key/i }));
await screen.findByText('snz_abc123xyz456secret');
@@ -127,57 +123,12 @@ describe('AddKeyModal', () => {
});
});
it('shows inline permission denial and hides the form when key create is denied', async () => {
server.use(setupAuthzDenyAll());
renderModal();
await expect(
screen.findByText(/is not authorized to perform/i),
).resolves.toBeInTheDocument();
expect(screen.queryByTestId('add-key-name-input')).not.toBeInTheDocument();
expect(screen.getByTestId('add-key-modal')).toBeInTheDocument();
});
it('keeps the footer usable when key create is denied', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(setupAuthzDenyAll());
renderModal();
await expect(
screen.findByText(/is not authorized to perform/i),
).resolves.toBeInTheDocument();
// The footer lives outside the guard: submit is gated, Cancel still works.
expect(screen.getByTestId('add-key-submit-btn')).toBeDisabled();
await user.click(screen.getByTestId('add-key-cancel-btn'));
await waitFor(() => {
expect(screen.queryByTestId('add-key-modal')).not.toBeInTheDocument();
});
});
it('shows inline permission denial when attach on the service account is denied', async () => {
server.use(setupAuthzDeny(buildSAAttachPermission('sa-1')));
renderModal();
await expect(
screen.findByText(/is not authorized to perform/i),
).resolves.toBeInTheDocument();
expect(screen.queryByTestId('add-key-name-input')).not.toBeInTheDocument();
});
it('Cancel button closes the modal', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderModal();
const cancelBtn = await screen.findByTestId('add-key-cancel-btn');
await user.click(cancelBtn);
await screen.findByTestId('add-key-modal');
await user.click(screen.getByRole('button', { name: /Cancel/i }));
await waitFor(() => {
expect(screen.queryByTestId('add-key-modal')).not.toBeInTheDocument();

View File

@@ -88,7 +88,6 @@ function TanStackCustomTableRow<TData, TItemKey = string>({
{...props}
className={rowClassName}
style={rowStyle}
data-testid={context?.getRowTestId?.(rowData)}
onMouseEnter={handleMouseEnter}
onMouseLeave={clearHovered}
>
@@ -155,12 +154,6 @@ function areTableRowPropsEqual<TData, TItemKey = string>(
return false;
}
const prevTestId = prev.context?.getRowTestId?.(prevData) ?? '';
const nextTestId = next.context?.getRowTestId?.(nextData) ?? '';
if (prevTestId !== nextTestId) {
return false;
}
const prevStyle = prev.context?.getRowStyle?.(prevData);
const nextStyle = next.context?.getRowStyle?.(nextData);
if (prevStyle !== nextStyle) {

View File

@@ -1,12 +1,8 @@
import { type ReactNode, useLayoutEffect, useMemo } from 'react';
import { useLayoutEffect, type ReactNode } from 'react';
import { chromePerformanceTanstackTableEndHover } from './perfDevtools';
import { useIsRowHovered } from './TanStackTableStateContext';
import {
TooltipContentProps,
TooltipSimple,
TooltipSimpleProps,
} from '@signozhq/ui/tooltip';
import { TooltipSimple, TooltipSimpleProps } from '@signozhq/ui/tooltip';
export type HoverTooltipProps = Omit<TooltipSimpleProps, 'open'> & {
rowId: string;
@@ -26,28 +22,9 @@ export function TanStackHoverTooltip({
}
}, [isHovered, rowId]);
const tooltipContentProps = useMemo(
() =>
({
onPointerDownOutside: (e): void => {
e.preventDefault();
e.stopPropagation();
},
}) satisfies TooltipContentProps,
[],
);
if (!isHovered) {
return <>{children}</>;
}
return (
<TooltipSimple
delayDuration={700}
tooltipContentProps={tooltipContentProps}
{...tooltipProps}
>
{children}
</TooltipSimple>
);
return <TooltipSimple {...tooltipProps}>{children}</TooltipSimple>;
}

View File

@@ -33,6 +33,7 @@ function TanStackRowCellsInner<TData, TItemKey = string>({
// Stable references via destructuring, keep them as is
const onRowClick = context?.onRowClick;
const onRowClickNewTab = context?.onRowClickNewTab;
const onRowDeactivate = context?.onRowDeactivate;
const isRowActive = context?.isRowActive;
const getRowKeyData = context?.getRowKeyData;
const rowIndex = row.index;
@@ -51,9 +52,21 @@ function TanStackRowCellsInner<TData, TItemKey = string>({
}
const isActive = isRowActive?.(rowData) ?? false;
onRowClick?.(rowData, itemKey, { isActive });
if (isActive && onRowDeactivate) {
onRowDeactivate();
} else {
onRowClick?.(rowData, itemKey);
}
},
[isRowActive, onRowClick, onRowClickNewTab, rowData, getRowKeyData, rowIndex],
[
isRowActive,
onRowDeactivate,
onRowClick,
onRowClickNewTab,
rowData,
getRowKeyData,
rowIndex,
],
);
if (itemKind === 'expansion') {
@@ -107,6 +120,7 @@ function areRowCellsPropsEqual<TData>(
prev.columnVisibilityKey === next.columnVisibilityKey &&
prev.context?.onRowClick === next.context?.onRowClick &&
prev.context?.onRowClickNewTab === next.context?.onRowClickNewTab &&
prev.context?.onRowDeactivate === next.context?.onRowDeactivate &&
prev.context?.isRowActive === next.context?.isRowActive &&
prev.context?.getRowKeyData === next.context?.getRowKeyData &&
prev.context?.renderRowActions === next.context?.renderRowActions &&

View File

@@ -51,7 +51,6 @@ import { useColumnHandlers } from './useColumnHandlers';
import { useColumnState } from './useColumnState';
import { useEffectiveData } from './useEffectiveData';
import { useFlatItems } from './useFlatItems';
import { useResetScroll } from './useResetScroll';
import { useRowKeyData } from './useRowKeyData';
import { useTableParams } from './useTableParams';
import { buildPageSizeItems, buildTanstackColumnDef } from './utils';
@@ -93,11 +92,11 @@ function TanStackTableInner<TData, TItemKey = string>(
getGroupKey,
getRowStyle,
getRowClassName,
getRowTestId,
isRowActive,
renderRowActions,
onRowClick,
onRowClickNewTab,
onRowDeactivate,
onSort,
activeRowIndex,
renderExpandedRow,
@@ -111,7 +110,6 @@ function TanStackTableInner<TData, TItemKey = string>(
suffixPaginationContent,
enableAlternatingRowColors,
disableVirtualScroll,
resetScrollKey,
}: TanStackTableProps<TData, TItemKey>,
forwardedRef: React.ForwardedRef<TanStackTableHandle>,
): JSX.Element {
@@ -326,8 +324,6 @@ function TanStackTableInner<TData, TItemKey = string>(
});
}, [flatIndexForActiveRow]);
useResetScroll(virtuosoRef, resetScrollKey);
const { sensors, columnIds, handleDragEnd } = useColumnDnd({
columns: effectiveColumns,
onColumnOrderChange: handleColumnOrderChange,
@@ -382,11 +378,11 @@ function TanStackTableInner<TData, TItemKey = string>(
() => ({
getRowStyle,
getRowClassName,
getRowTestId,
isRowActive,
renderRowActions,
onRowClick,
onRowClickNewTab,
onRowDeactivate,
renderExpandedRow,
getRowKeyData,
colCount: visibleColumnsCount,
@@ -400,11 +396,11 @@ function TanStackTableInner<TData, TItemKey = string>(
[
getRowStyle,
getRowClassName,
getRowTestId,
isRowActive,
renderRowActions,
onRowClick,
onRowClickNewTab,
onRowDeactivate,
renderExpandedRow,
getRowKeyData,
visibleColumnsCount,

View File

@@ -85,9 +85,7 @@ describe('TanStackRowCells', () => {
</table>,
);
await user.click(screen.getAllByRole('cell')[0]);
expect(onRowClick).toHaveBeenCalledWith({ id: 'r1' }, 'r1', {
isActive: false,
});
expect(onRowClick).toHaveBeenCalledWith({ id: 'r1' }, 'r1');
});
it('fires onRowClick with empty itemKey when getRowKeyData is not provided', async () => {
@@ -119,19 +117,17 @@ describe('TanStackRowCells', () => {
</table>,
);
await user.click(screen.getAllByRole('cell')[0]);
expect(onRowClick).toHaveBeenCalledWith({ id: 'r1' }, '', {
isActive: false,
});
expect(onRowClick).toHaveBeenCalledWith({ id: 'r1' }, '');
});
it('calls onRowClick with isActive: true when the row is active', async () => {
// The table no longer owns open/close — it reports the active state and the
// consumer routes the click. An active row must still fire onRowClick.
it('calls onRowDeactivate instead of onRowClick when row is active', async () => {
const user = userEvent.setup();
const onRowClick = jest.fn();
const onRowDeactivate = jest.fn();
const ctx: TableRowContext<Row> = {
colCount: 1,
onRowClick,
onRowDeactivate,
isRowActive: () => true,
getRowKeyData: () => ({ finalKey: 'r1', itemKey: 'r1' }),
hasSingleColumn: false,
@@ -156,44 +152,8 @@ describe('TanStackRowCells', () => {
</table>,
);
await user.click(screen.getAllByRole('cell')[0]);
expect(onRowClick).toHaveBeenCalledWith({ id: 'r1' }, 'r1', {
isActive: true,
});
});
it('calls onRowClick with isActive: false when the row is not active', async () => {
const user = userEvent.setup();
const onRowClick = jest.fn();
const ctx: TableRowContext<Row> = {
colCount: 1,
onRowClick,
isRowActive: () => false,
getRowKeyData: () => ({ finalKey: 'r1', itemKey: 'r1' }),
hasSingleColumn: false,
columnOrderKey: '',
columnVisibilityKey: '',
};
const row = buildMockRow([{ id: 'body' }]);
render(
<table>
<tbody>
<tr>
<TanStackRowCells<Row>
row={row as never}
context={ctx}
itemKind="row"
hasSingleColumn={false}
columnOrderKey=""
columnVisibilityKey=""
/>
</tr>
</tbody>
</table>,
);
await user.click(screen.getAllByRole('cell')[0]);
expect(onRowClick).toHaveBeenCalledWith({ id: 'r1' }, 'r1', {
isActive: false,
});
expect(onRowDeactivate).toHaveBeenCalled();
expect(onRowClick).not.toHaveBeenCalled();
});
it('does not render renderRowActions before hover', () => {

View File

@@ -611,7 +611,6 @@ describe('TanStackTableView Integration', () => {
expect(onRowClick).toHaveBeenCalledWith(
expect.objectContaining({ id: '1', name: 'Item 1' }),
'1',
{ isActive: false },
);
});
@@ -640,7 +639,6 @@ describe('TanStackTableView Integration', () => {
expect(onRowClick).toHaveBeenCalledWith(
expect.objectContaining({ id: '1', name: 'Item 1' }),
{ id: '1', name: 'Item 1' },
{ isActive: false },
);
});
@@ -661,15 +659,15 @@ describe('TanStackTableView Integration', () => {
expect(row).toHaveClass('tableRowActive');
});
it('calls onRowClick with isActive: true when clicking the active row', async () => {
// The consumer owns open/close routing — the table just reports the
// active state via the click context.
it('calls onRowDeactivate when clicking active row', async () => {
const user = userEvent.setup();
const onRowClick = jest.fn();
const onRowDeactivate = jest.fn();
renderTanStackTable({
props: {
onRowClick,
onRowDeactivate,
isRowActive: (row) => row.id === '1',
},
});
@@ -680,11 +678,8 @@ describe('TanStackTableView Integration', () => {
await user.click(screen.getByText('Item 1'));
expect(onRowClick).toHaveBeenCalledWith(
expect.objectContaining({ id: '1' }),
expect.anything(),
{ isActive: true },
);
expect(onRowDeactivate).toHaveBeenCalled();
expect(onRowClick).not.toHaveBeenCalled();
});
it('opens in new tab on ctrl+click', async () => {

View File

@@ -217,55 +217,6 @@ describe('useColumnState', () => {
});
});
describe('storageKey changes', () => {
it('does not auto-show hidden columns when switching between different storageKeys', () => {
// Regression test: when switching Pods→Nodes→Pods, hidden columns were
// incorrectly shown because prevColumnIdsRef wasn't reset on storageKey change.
// The autoShowNewlyAddedColumns effect would see pod columns as "new" (not in
// node columns) and unhide them.
const KEY_PODS = 'k8s-pods';
const KEY_NODES = 'k8s-nodes';
const podColumns = [
col('podName'),
col('podStatus'),
col('cpu_request', { defaultVisibility: false }),
col('memory_request', { defaultVisibility: false }),
];
const nodeColumns = [
col('nodeName'),
col('nodeStatus'),
col('cpu'),
col('memory'),
];
// Initialize both tables
act(() => {
useColumnStore.getState().initializeFromDefaults(KEY_PODS, podColumns);
useColumnStore.getState().initializeFromDefaults(KEY_NODES, nodeColumns);
});
// Start with pods - cpu_request and memory_request should be hidden
const { result, rerender } = renderHook(
({ storageKey, columns }) => useColumnState({ storageKey, columns }),
{ initialProps: { storageKey: KEY_PODS, columns: podColumns } },
);
expect(result.current.hiddenColumnIds).toContain('cpu_request');
expect(result.current.hiddenColumnIds).toContain('memory_request');
// Switch to nodes
rerender({ storageKey: KEY_NODES, columns: nodeColumns });
// Switch back to pods - hidden columns should STILL be hidden
rerender({ storageKey: KEY_PODS, columns: podColumns });
expect(result.current.hiddenColumnIds).toContain('cpu_request');
expect(result.current.hiddenColumnIds).toContain('memory_request');
});
});
describe('actions', () => {
it('hideColumn hides a column', () => {
const columns = [col('a'), col('b')];

View File

@@ -1,103 +0,0 @@
import { RefObject } from 'react';
import { renderHook } from '@testing-library/react';
import type { TableVirtuosoHandle } from 'react-virtuoso';
import { useResetScroll } from '../useResetScroll';
function createMockRef(scrollTo: jest.Mock): RefObject<TableVirtuosoHandle> {
return {
current: {
scrollToIndex: jest.fn(),
scrollIntoView: jest.fn(),
scrollTo,
scrollBy: jest.fn(),
},
};
}
describe('useResetScroll', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('does not call scrollTo on initial mount', () => {
const scrollTo = jest.fn();
const ref = createMockRef(scrollTo);
renderHook(() => useResetScroll(ref, 'initial-key'));
expect(scrollTo).not.toHaveBeenCalled();
});
it('calls scrollTo when key changes', () => {
const scrollTo = jest.fn();
const ref = createMockRef(scrollTo);
const { rerender } = renderHook(
({ resetKey }) => useResetScroll(ref, resetKey),
{ initialProps: { resetKey: 'key-1' } },
);
expect(scrollTo).not.toHaveBeenCalled();
rerender({ resetKey: 'key-2' });
expect(scrollTo).toHaveBeenCalledWith({ left: 0 });
});
it('calls scrollTo once per key change', () => {
const scrollTo = jest.fn();
const ref = createMockRef(scrollTo);
const { rerender } = renderHook(
({ resetKey }) => useResetScroll(ref, resetKey),
{ initialProps: { resetKey: 'key-1' } },
);
rerender({ resetKey: 'key-2' });
rerender({ resetKey: 'key-3' });
rerender({ resetKey: 'key-4' });
expect(scrollTo).toHaveBeenCalledTimes(3);
});
it('does not call scrollTo when key unchanged', () => {
const scrollTo = jest.fn();
const ref = createMockRef(scrollTo);
const { rerender } = renderHook(
({ resetKey }) => useResetScroll(ref, resetKey),
{ initialProps: { resetKey: 'same-key' } },
);
rerender({ resetKey: 'same-key' });
rerender({ resetKey: 'same-key' });
expect(scrollTo).not.toHaveBeenCalled();
});
it('handles null ref.current gracefully', () => {
const ref: RefObject<TableVirtuosoHandle | null> = { current: null };
const { rerender } = renderHook(
({ resetKey }) => useResetScroll(ref, resetKey),
{ initialProps: { resetKey: 'key-1' } },
);
expect(() => rerender({ resetKey: 'key-2' })).not.toThrow();
});
it('calls scrollTo when changing from undefined to defined', () => {
const scrollTo = jest.fn();
const ref = createMockRef(scrollTo);
const { rerender } = renderHook(
({ resetKey }) => useResetScroll(ref, resetKey),
{ initialProps: { resetKey: undefined as string | undefined } },
);
rerender({ resetKey: 'new-key' });
expect(scrollTo).toHaveBeenCalledWith({ left: 0 });
});
});

View File

@@ -116,11 +116,9 @@ export * from './useTableParams';
* getItemKey={(row) => row.id}
* isRowActive={(row) => row.id === selectedId}
* activeRowIndex={selectedIndex}
* // The table reports the click + the row's active state; the consumer owns open/close.
* onRowClick={(row, itemKey, { isActive }) =>
* setSelectedId(isActive ? undefined : itemKey)
* }
* onRowClick={(row, itemKey) => setSelectedId(itemKey)}
* onRowClickNewTab={(row, itemKey) => openInNewTab(itemKey)}
* onRowDeactivate={() => setSelectedId(undefined)}
* getRowClassName={(row) => (row.severity === 'error' ? 'row-error' : '')}
* getRowStyle={(row) => (row.dimmed ? { opacity: 0.5 } : {})}
* renderRowActions={(row) => <Button size="small">Open</Button>}
@@ -215,16 +213,6 @@ export * from './useTableParams';
* />
* ```
*
* @example Reset scroll on context change — use `resetScrollKey` to scroll back to start
* when the data context changes (e.g., switching between categories or tabs).
* ```tsx
* <TanStackTable
* data={data}
* columns={columns}
* resetScrollKey={selectedCategory}
* />
* ```
*
* @example useTableParams — manages pagination state with URL sync and persistence
*
* The `useTableParams` hook handles page, limit, orderBy, and expanded state. It can sync

View File

@@ -81,19 +81,15 @@ export type FlatItem<TData> =
| { kind: 'row'; row: TanStackRowType<TData> }
| { kind: 'expansion'; row: TanStackRowType<TData> };
export type RowClickContext = {
isActive: boolean;
};
export type TableRowContext<TData, TItemKey = string> = {
getRowStyle?: (row: TData) => CSSProperties;
getRowClassName?: (row: TData) => string;
getRowTestId?: (row: TData) => string;
isRowActive?: (row: TData) => boolean;
renderRowActions?: (row: TData) => ReactNode;
onRowClick?: (row: TData, itemKey: TItemKey, context: RowClickContext) => void;
onRowClick?: (row: TData, itemKey: TItemKey) => void;
/** Called when ctrl+click or cmd+click on a row */
onRowClickNewTab?: (row: TData, itemKey: TItemKey) => void;
onRowDeactivate?: () => void;
renderExpandedRow?: (
row: TData,
rowKey: string,
@@ -182,12 +178,12 @@ export type TanStackTableProps<TData, TItemKey = string> = {
getGroupKey?: (row: TData) => Record<string, string>;
getRowStyle?: (row: TData) => CSSProperties;
getRowClassName?: (row: TData) => string;
getRowTestId?: (row: TData) => string;
isRowActive?: (row: TData) => boolean;
renderRowActions?: (row: TData) => ReactNode;
onRowClick?: (row: TData, itemKey: TItemKey, context: RowClickContext) => void;
onRowClick?: (row: TData, itemKey: TItemKey) => void;
/** Called when ctrl+click or cmd+click on a row */
onRowClickNewTab?: (row: TData, itemKey: TItemKey) => void;
onRowDeactivate?: () => void;
activeRowIndex?: number;
renderExpandedRow?: (
row: TData,
@@ -213,8 +209,6 @@ export type TanStackTableProps<TData, TItemKey = string> = {
enableAlternatingRowColors?: boolean;
/** Disable virtual scrolling and render all rows at once. Cannot be used with onEndReached. */
disableVirtualScroll?: boolean;
/** When this value changes, the table's scroll resets to the start. Useful for resetting scroll when the data context changes (e.g., switching categories). */
resetScrollKey?: string;
};
export type TanStackTableHandle = TableVirtuosoHandle & {

View File

@@ -67,13 +67,6 @@ export function useColumnState<TData>({
const columnSizing = useStoreSizing(storageKey ?? '');
const prevColumnIdsRef = useRef<Set<string> | null>(null);
const prevStorageKeyRef = useRef<string | undefined>(storageKey);
// Reset prevColumnIdsRef when storageKey changes to avoid cross-table interference
if (prevStorageKeyRef.current !== storageKey) {
prevColumnIdsRef.current = null;
prevStorageKeyRef.current = storageKey;
}
useEffect(
function autoShowNewlyAddedColumns(): void {

View File

@@ -1,18 +0,0 @@
import { RefObject, useEffect, useRef } from 'react';
import type { TableVirtuosoHandle } from 'react-virtuoso';
export function useResetScroll(
scrollRef: RefObject<TableVirtuosoHandle | null>,
resetKey: string | undefined,
): void {
const prevKeyRef = useRef(resetKey);
useEffect(() => {
if (prevKeyRef.current !== resetKey) {
prevKeyRef.current = resetKey;
scrollRef.current?.scrollTo({
left: 0,
});
}
}, [resetKey, scrollRef]);
}

View File

@@ -1,11 +1,6 @@
.warning-popover-overlay {
--antd-arrow-background-color: var(--l2-background);
}
.warning-content {
display: flex;
flex-direction: column;
background-color: var(--l2-background);
// === SECTION: Summary (Top)
&__summary-section {
@@ -15,25 +10,16 @@
&__summary {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--spacing-8);
padding: 16px;
}
&__summary-left {
display: flex;
align-items: flex-start;
align-items: baseline;
gap: 8px;
}
&__icon-wrapper {
display: flex;
align-items: center;
flex-shrink: 0;
height: var(--spacing-12);
}
&__summary-text {
display: flex;
flex-direction: column;

View File

@@ -2,7 +2,6 @@ import { ReactNode, useMemo } from 'react';
import { Color } from '@signozhq/design-tokens';
import { Button, Popover, PopoverProps } from 'antd';
import ErrorIcon from 'assets/Error';
import cx from 'classnames';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { BookOpenText, ChevronsDown, TriangleAlert } from '@signozhq/icons';
import KeyValueLabel from 'periscope/components/KeyValueLabel';
@@ -31,18 +30,16 @@ export function WarningContent({ warning }: WarningContentProps): JSX.Element {
{/* Summary Header */}
<section className="warning-content__summary-section">
<header className="warning-content__summary">
{(warningCode || warningMessage) && (
<div className="warning-content__summary-left">
<div className="warning-content__icon-wrapper">
<ErrorIcon />
</div>
<div className="warning-content__summary-text">
<h2 className="warning-content__warning-code">{warningCode}</h2>
<p className="warning-content__warning-message">{warningMessage}</p>
</div>
<div className="warning-content__summary-left">
<div className="warning-content__icon-wrapper">
<ErrorIcon />
</div>
)}
<div className="warning-content__summary-text">
<h2 className="warning-content__warning-code">{warningCode}</h2>
<p className="warning-content__warning-message">{warningMessage}</p>
</div>
</div>
{warningUrl && (
<div className="warning-content__summary-right">
@@ -157,10 +154,6 @@ function WarningPopover({
overlayInnerStyle={{ padding: 0 }}
autoAdjustOverflow
{...popoverProps}
overlayClassName={cx(
'warning-popover-overlay',
popoverProps.overlayClassName,
)}
>
{children || (
<TriangleAlert

View File

@@ -9,6 +9,8 @@ export enum FeatureKeys {
ANOMALY_DETECTION = 'anomaly_detection',
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
USE_JSON_BODY = 'use_json_body',
USE_FINE_GRAINED_AUTHZ = 'use_fine_grained_authz',
USE_INFRA_MONITORING_V2 = 'use_infra_monitoring_v2',
ENABLE_AI_OBSERVABILITY = 'enable_ai_observability',
ENABLE_METRICS_REDUCTION = 'enable_metrics_reduction',
}

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