mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-01 18:50:35 +01:00
Compare commits
15 Commits
ci/cacheci
...
feat/alert
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a167ddeac | ||
|
|
97557d0145 | ||
|
|
bf247a000a | ||
|
|
720ccfde5e | ||
|
|
5c95bf63da | ||
|
|
2f257fa04f | ||
|
|
c08eb2f70a | ||
|
|
a185a504f8 | ||
|
|
7e62d1edf7 | ||
|
|
221063c91d | ||
|
|
99f1c8333e | ||
|
|
dd502444d0 | ||
|
|
f5e1c25b23 | ||
|
|
eea7b3eba5 | ||
|
|
cbbd96ab50 |
92
.github/workflows/cacheci.yml
vendored
92
.github/workflows/cacheci.yml
vendored
@@ -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
|
||||
4
Makefile
4
Makefile
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,24 @@ 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
|
||||
RUN mv /root/linux-${TARGETARCH}/signoz /root/signoz
|
||||
|
||||
COPY --from=build /opt/build ./web/
|
||||
|
||||
|
||||
@@ -7430,8 +7430,6 @@ components:
|
||||
- below
|
||||
- equal
|
||||
- not_equal
|
||||
- above_or_equal
|
||||
- below_or_equal
|
||||
- outside_bounds
|
||||
type: string
|
||||
RuletypesCumulativeSchedule:
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
# PromQL Serving — clickhouseprometheusv2
|
||||
|
||||
This document is the subsystem context for `pkg/prometheus/clickhouseprometheusv2`,
|
||||
the second-generation ClickHouse-backed Prometheus provider. It explains why the
|
||||
package exists, the correctness constraints that shaped it, and how each fetch
|
||||
reduction is proven not to change results. Any change to the provider must keep
|
||||
these invariants; if a change would violate one, it must be flagged and
|
||||
discussed.
|
||||
|
||||
---
|
||||
|
||||
## Why a second provider
|
||||
|
||||
The v1 provider (`pkg/prometheus/clickhouseprometheus`) serves the promql engine
|
||||
through the remote-read protobuf adapter: every raw sample of a query's union
|
||||
window is fetched, serialized, and handed to the engine. The cost is a function
|
||||
of ingested data, not of the question asked — which is how a dashboard of PromQL
|
||||
panels can take an instance down.
|
||||
|
||||
In v2 the stock promql engine evaluates over a native `storage.Querier`: no
|
||||
translation layer, per-selector fetch windows, and fetch reductions that are
|
||||
provably invisible to the engine.
|
||||
|
||||
**The core constraint: every reduction either preserves engine semantics exactly
|
||||
or is not performed.** A PromQL result that differs from upstream Prometheus is
|
||||
a lost user. The conformance suite
|
||||
(`tests/integration/tests/promqlconformance/`) replays Prometheus' own test
|
||||
corpus against both providers and is the arbiter.
|
||||
|
||||
---
|
||||
|
||||
## Series lookup
|
||||
|
||||
Matchers resolve to series once per selector (`selectSeries`) against the series
|
||||
tables, which hold one row per (fingerprint, bucket) at 1h/6h/1d/1w
|
||||
granularities. Table selection and window rounding delegate to the shared
|
||||
metrics schema package (`pkg/telemetryschema/metricstelemetryschema`); the
|
||||
window start rounds down to the bucket boundary so a window beginning mid-bucket
|
||||
still matches the bucket's row.
|
||||
|
||||
How matchers become SQL is documented at `applySeriesConditions`. The rules that
|
||||
carry semantics:
|
||||
|
||||
- `__name__` matchers (all four types) translate to the `metric_name` column.
|
||||
- Every other matcher becomes a `JSONExtractString` condition on the labels
|
||||
column. An equality matcher against `""` matches series *without* the label,
|
||||
mirroring PromQL, because `JSONExtractString` returns `""` for missing keys.
|
||||
- Regexes are anchored (`^(?:...)$`) before they reach `match()`: PromQL
|
||||
matchers match the whole value, ClickHouse `match()` searches for a
|
||||
substring.
|
||||
- The series-lookup upper bound is inclusive (`unix_milli <= end`) because the
|
||||
exporter floors registration rows to the bucket start: a series first
|
||||
registered in the bucket beginning exactly at `end` would otherwise be
|
||||
invisible while its samples are in range.
|
||||
|
||||
Empty-valued labels come off at this boundary: an empty value means "label
|
||||
absent" in Prometheus, but stored attribute JSON can carry them.
|
||||
|
||||
---
|
||||
|
||||
## Sample fetch
|
||||
|
||||
Samples are fetched per selector using the engine's per-selector hints, not the
|
||||
query-wide union window — `foo / foo offset 1d` reads two narrow windows
|
||||
instead of the widest one twice.
|
||||
|
||||
**Last-sample-per-step reduction.** Instant selectors of subquery-free queries
|
||||
fetch only the last sample per step bucket. The engine resolves an instant
|
||||
selector at each grid timestamp `t` to the latest sample in the left-open
|
||||
lookback window `(t − lookback, t]`. Buckets anchor at the selector's first
|
||||
evaluation timestamp — recovered from the hints as
|
||||
`hints.Start + lookback − 1ms`, the inverse of how the engine derives
|
||||
`hints.Start` — so bucket boundaries coincide with evaluation timestamps, and a
|
||||
non-final sample of a bucket can never be the latest sample in
|
||||
`(t − lookback, t]` for any grid `t`. Real timestamps are preserved, so the
|
||||
engine's own lookback and staleness handling stay exact.
|
||||
|
||||
Range selectors always fetch raw — every sample feeds the range function. The
|
||||
subquery-free proof travels in the context as `prometheus.QueryTraits`, because
|
||||
subquery selectors evaluate at the subquery's step while the hints carry the
|
||||
top-level step; call sites that do not attach traits get the conservative raw
|
||||
fetch.
|
||||
|
||||
**Row assembly** maps stale flags to the engine's `StaleNaN` and merges series
|
||||
with identical label sets (`sortAndMerge`) — the engine assumes storages never
|
||||
emit duplicates. Duplicate timestamps pass through as stored: uniqueness is
|
||||
ingest's job, and v1 feeds them to the engine as-is over the same data.
|
||||
|
||||
**The fingerprint filter is a shard-local semi-join.** The samples query
|
||||
restricts to the matched series by re-running the series predicates as an
|
||||
`IN (SELECT fingerprint FROM <local series table> ...)` subquery, not a GLOBAL
|
||||
broadcast of the matched set. ClickHouse materializes the subquery's set per
|
||||
shard before the scan, so it still engages the fingerprint primary-key column.
|
||||
Because the subquery re-executes the predicates after the lookup ran, it can
|
||||
match series registered in between; sample rows whose fingerprint the lookup
|
||||
never saw are skipped — the lookup is the read snapshot.
|
||||
|
||||
---
|
||||
|
||||
## Sharding
|
||||
|
||||
`samples_v4` and `time_series_v4` (and all their rollups) shard on the same key
|
||||
— `cityHash64(env, temporality, metric_name, fingerprint)` — so a series'
|
||||
samples and catalog rows live on the same shard. The semi-join above exploits
|
||||
that: each shard filters by its own series rows, which are exactly the series
|
||||
of that shard's samples.
|
||||
|
||||
The temporality filter on every samples statement
|
||||
(`temporality IN ['Cumulative', 'Unspecified']`) is a semantic no-op — the
|
||||
matched fingerprints already come from those temporalities — that engages the
|
||||
leading samples primary-key column.
|
||||
|
||||
Delta-temporality series stay invisible to PromQL exactly as they are in v1:
|
||||
the rollout gate is parity with v1, and a Delta stream fed to `rate()`
|
||||
as-if-cumulative would be wrong, not just new.
|
||||
|
||||
---
|
||||
|
||||
## Observability
|
||||
|
||||
Every statement carries a `log_comment` with
|
||||
`code.namespace=clickhouse-prometheus-v2` and `code.function.name` naming the
|
||||
call site, so this provider's work is attributable in `system.query_log`.
|
||||
@@ -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
|
||||
|
||||
@@ -232,11 +227,14 @@ cd tests/e2e
|
||||
# Single feature dir
|
||||
npx playwright test tests/alerts/ --project=chromium
|
||||
|
||||
# Single sub-area
|
||||
npx playwright test tests/alerts/history/ --project=chromium
|
||||
|
||||
# Single file
|
||||
npx playwright test tests/alerts/alerts.spec.ts --project=chromium
|
||||
npx playwright test tests/alerts/page.spec.ts --project=chromium
|
||||
|
||||
# Single test by title grep
|
||||
npx playwright test --project=chromium -g "TC-01"
|
||||
npx playwright test --project=chromium -g "AL-01"
|
||||
```
|
||||
|
||||
### Iterative modes
|
||||
@@ -270,7 +268,14 @@ yarn test:staging
|
||||
| `SIGNOZ_E2E_PASSWORD` | Admin password. Bootstrap writes the integration-test default. |
|
||||
| `SIGNOZ_E2E_SEEDER_URL` | Seeder HTTP base URL — hit by specs that need per-test telemetry. |
|
||||
|
||||
Loading order in `playwright.config.ts`: `.env` first (user-provided, staging), then `.env.local` with `override: true` (bootstrap-generated, local mode). Anything already set in `process.env` at yarn-test time wins because dotenv doesn't touch vars that are already present.
|
||||
Precedence in `playwright.config.ts`, lowest to highest: `.env` (user-provided, staging) → `.env.local` (bootstrap-generated, local mode) → whatever is already in `process.env`. The config parses both files itself and only fills in keys the environment does not already define, so exporting a variable always wins:
|
||||
|
||||
```bash
|
||||
# runs against a locally served frontend, not whatever .env.local points at
|
||||
SIGNOZ_E2E_BASE_URL=http://127.0.0.1:3301 pnpm test tests/alerts
|
||||
```
|
||||
|
||||
This is deliberately not `dotenv.config({ override: true })`. That flag makes the *file* beat `process.env`, which silently discarded exported values — including the `SIGNOZ_E2E_BASE_URL` in `pnpm test:staging`, whenever a `.env.local` happened to exist.
|
||||
|
||||
### Playwright options
|
||||
|
||||
@@ -286,16 +291,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.
|
||||
|
||||
@@ -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 (~3–4 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.
|
||||
|
||||
@@ -8479,8 +8479,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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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'>;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -92,7 +92,6 @@ function DeleteAccountModal(): JSX.Element {
|
||||
loading={isDeleting}
|
||||
onClick={handleConfirm}
|
||||
data-testid="confirm-delete-btn"
|
||||
withPortal={false}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Delete
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -55,7 +55,6 @@ export function RevokeKeyFooter({
|
||||
color="destructive"
|
||||
loading={isRevoking}
|
||||
onClick={onConfirm}
|
||||
withPortal={false}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Revoke Key
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
|
||||
@@ -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')];
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -215,16 +215,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
|
||||
|
||||
@@ -213,8 +213,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 & {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -1,9 +1,3 @@
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import type { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { getNavigationReferrer } from 'lib/navigation';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
import { isCustomTimeRange } from 'store/globalTime';
|
||||
|
||||
export enum Events {
|
||||
UPDATE_GRAPH_VISIBILITY_STATE = 'UPDATE_GRAPH_VISIBILITY_STATE',
|
||||
UPDATE_GRAPH_MANAGER_TABLE = 'UPDATE_GRAPH_MANAGER_TABLE',
|
||||
@@ -45,155 +39,3 @@ export enum InfraMonitoringEvents {
|
||||
StatefulSet = 'statefulSet',
|
||||
Volumes = 'volumes',
|
||||
}
|
||||
|
||||
export function logInfraFilterCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
source: 'quick_filter' | 'search' | 'host_status_toggle',
|
||||
expression: string,
|
||||
extraKeys?: string[],
|
||||
): void {
|
||||
const expressionKeys = extractQueryPairs(expression?.trim() || '').map(
|
||||
(pair) => pair.key,
|
||||
);
|
||||
|
||||
if (extraKeys) {
|
||||
extraKeys.forEach((key) => expressionKeys.push(key));
|
||||
}
|
||||
|
||||
if (expressionKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
void logEvent('infra_filter_customized', {
|
||||
entity_type: entityType,
|
||||
source,
|
||||
expression_keys: [...new Set(expressionKeys)],
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraMonitoringListViewedEvent(
|
||||
entity: InfraMonitoringEntity,
|
||||
): void {
|
||||
const referrer = getNavigationReferrer();
|
||||
|
||||
void logEvent('infra_list_viewed', {
|
||||
entity,
|
||||
referrer,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraTimeRangeCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
rangeLabel: string,
|
||||
): void {
|
||||
void logEvent('infra_time_range_customized', {
|
||||
entity_type: entityType,
|
||||
range_label: isCustomTimeRange(rangeLabel) ? 'custom' : rangeLabel,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraColumnCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
columnsList: string[],
|
||||
fontSize: string,
|
||||
maxLinesPerRow: number,
|
||||
source: 'list' | 'expanded',
|
||||
): void {
|
||||
void logEvent('infra_column_customized', {
|
||||
entity_type: entityType,
|
||||
columns_list: columnsList,
|
||||
font_size: fontSize,
|
||||
max_lines_per_row: maxLinesPerRow,
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraColumnSortedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
columnKey: string,
|
||||
direction: 'asc' | 'desc',
|
||||
source: 'list' | 'expanded',
|
||||
): void {
|
||||
void logEvent('infra_column_sorted', {
|
||||
entity_type: entityType,
|
||||
column_key: columnKey,
|
||||
direction,
|
||||
source,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraDrawerTimeRangeCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
rangeLabel: string,
|
||||
): void {
|
||||
void logEvent('infra_drawer_time_range_customized', {
|
||||
entity_type: entityType,
|
||||
range_label: isCustomTimeRange(rangeLabel) ? 'custom' : rangeLabel,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraDrawerFilterCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
tab: 'metrics' | 'logs' | 'traces' | 'events' | 'pod_metrics',
|
||||
expression: string,
|
||||
filterSource: 'search' | 'logs',
|
||||
): void {
|
||||
const expressionKeys = extractQueryPairs(expression?.trim() || '').map(
|
||||
(pair) => pair.key,
|
||||
);
|
||||
|
||||
if (expressionKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
void logEvent('infra_drawer_filter_customized', {
|
||||
entity_type: entityType,
|
||||
tab,
|
||||
expression_keys: [...new Set(expressionKeys)],
|
||||
filter_source: filterSource,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraGroupByCustomizedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
groupByKeysList: string[],
|
||||
): void {
|
||||
void logEvent('infra_group_by_customized', {
|
||||
entity_type: entityType,
|
||||
group_by_keys_list: groupByKeysList,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraDrawerTabViewedEvent(
|
||||
entityType: InfraMonitoringEntity,
|
||||
tab: string,
|
||||
isDefaultTab: boolean,
|
||||
): void {
|
||||
void logEvent('infra_drawer_tab_viewed', {
|
||||
entity_type: entityType,
|
||||
tab,
|
||||
is_default_tab: isDefaultTab,
|
||||
});
|
||||
}
|
||||
|
||||
export function logInfraExplorerNavigatedEvent(params: {
|
||||
entityType: InfraMonitoringEntity;
|
||||
destination:
|
||||
| 'metrics_explorer'
|
||||
| 'logs_explorer'
|
||||
| 'traces_explorer'
|
||||
| 'k8s_list';
|
||||
source: 'chart_compass_icon' | 'tab_cta_button' | 'stats_card';
|
||||
tab: string;
|
||||
sourceKey: string | null;
|
||||
drawerDurationMsAtNavigation: number | null;
|
||||
}): void {
|
||||
void logEvent('infra_explorer_navigated', {
|
||||
entity_type: params.entityType,
|
||||
destination: params.destination,
|
||||
source: params.source,
|
||||
tab: params.tab,
|
||||
source_key: params.sourceKey,
|
||||
drawer_duration_ms_at_navigation: params.drawerDurationMsAtNavigation,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,3 @@ export const DASHBOARD_CACHE_TIME = 30_000;
|
||||
export const DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED = 0;
|
||||
|
||||
export const FIELD_API_CACHE_TIME = 60_000;
|
||||
|
||||
// intentionally lower since I only want to prevent refresh in background
|
||||
// after click in the row
|
||||
export const INFRA_MONITORING_DETAILS_CACHE_TIME = 1_000;
|
||||
|
||||
@@ -29,6 +29,7 @@ function PopoverContent({
|
||||
<Link
|
||||
to={`${ROUTES.LOGS_EXPLORER}?${relatedLogsLink}`}
|
||||
className="contributor-row-popover-buttons__button"
|
||||
data-testid="alert-popover-view-logs"
|
||||
>
|
||||
<div className="icon">
|
||||
<LogsIcon />
|
||||
@@ -40,6 +41,7 @@ function PopoverContent({
|
||||
<Link
|
||||
to={`${ROUTES.TRACES_EXPLORER}?${relatedTracesLink}`}
|
||||
className="contributor-row-popover-buttons__button"
|
||||
data-testid="alert-popover-view-traces"
|
||||
>
|
||||
<div className="icon">
|
||||
<DraftingCompass
|
||||
|
||||
@@ -26,7 +26,10 @@ function ChangePercentage({
|
||||
}: ChangePercentageProps): JSX.Element {
|
||||
if (direction > 0) {
|
||||
return (
|
||||
<div className="change-percentage change-percentage--success">
|
||||
<div
|
||||
className="change-percentage change-percentage--success"
|
||||
data-testid="stats-card-change"
|
||||
>
|
||||
<div className="change-percentage__icon">
|
||||
<ArrowDownLeft size={14} color={Color.BG_FOREST_500} />
|
||||
</div>
|
||||
@@ -38,7 +41,10 @@ function ChangePercentage({
|
||||
}
|
||||
if (direction < 0) {
|
||||
return (
|
||||
<div className="change-percentage change-percentage--error">
|
||||
<div
|
||||
className="change-percentage change-percentage--error"
|
||||
data-testid="stats-card-change"
|
||||
>
|
||||
<div className="change-percentage__icon">
|
||||
<ArrowUpRight size={14} color={Color.BG_CHERRY_500} />
|
||||
</div>
|
||||
@@ -50,7 +56,10 @@ function ChangePercentage({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="change-percentage change-percentage--no-previous-data">
|
||||
<div
|
||||
className="change-percentage change-percentage--no-previous-data"
|
||||
data-testid="stats-card-change"
|
||||
>
|
||||
<div className="change-percentage__label">no previous data</div>
|
||||
</div>
|
||||
);
|
||||
@@ -103,7 +112,12 @@ function StatsCard({
|
||||
const formattedEndTimeForTooltip = convertTimestampToLocaleDateString(endTime);
|
||||
|
||||
return (
|
||||
<div className={`stats-card ${isEmpty ? 'stats-card--empty' : ''}`}>
|
||||
<div
|
||||
className={`stats-card ${isEmpty ? 'stats-card--empty' : ''}`}
|
||||
data-testid="stats-card"
|
||||
data-stats-title={title}
|
||||
data-empty={isEmpty ? 'true' : 'false'}
|
||||
>
|
||||
<div className="stats-card__title-wrapper">
|
||||
<div className="title">{title}</div>
|
||||
<div className="duration-indicator">
|
||||
@@ -123,7 +137,7 @@ function StatsCard({
|
||||
</div>
|
||||
|
||||
<div className="stats-card__stats">
|
||||
<div className="count-label">
|
||||
<div className="count-label" data-testid="stats-card-value">
|
||||
{isEmpty ? emptyMessage : displayValue || totalCurrentCount}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -81,7 +81,11 @@ function StatsGraph({ timeSeries, changeDirection }: Props): JSX.Element {
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
|
||||
<div
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
ref={graphRef}
|
||||
data-testid="stats-card-sparkline"
|
||||
>
|
||||
<Uplot data={[xData, yData]} options={options} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -48,11 +48,16 @@ function TopContributorsCard({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="top-contributors-card">
|
||||
<div className="top-contributors-card" data-testid="top-contributors-card">
|
||||
<div className="top-contributors-card__header">
|
||||
<div className="title">top contributors</div>
|
||||
{topContributorsData.length > 3 && (
|
||||
<Button type="text" className="view-all" onClick={toggleViewAllDrawer}>
|
||||
<Button
|
||||
type="text"
|
||||
className="view-all"
|
||||
onClick={toggleViewAllDrawer}
|
||||
data-testid="top-contributors-view-all"
|
||||
>
|
||||
<div className="label">View all</div>
|
||||
<div className="icon">
|
||||
<ArrowRight
|
||||
|
||||
@@ -68,7 +68,10 @@ function TopContributorsRows({
|
||||
relatedTracesLink={record.relatedTracesLink}
|
||||
relatedLogsLink={record.relatedLogsLink}
|
||||
>
|
||||
<div className="total-contribution">
|
||||
<div
|
||||
className="total-contribution"
|
||||
data-testid="top-contributors-row-count"
|
||||
>
|
||||
{count}/{totalCurrentTriggers}
|
||||
</div>
|
||||
</ConditionalAlertPopover>
|
||||
@@ -78,7 +81,10 @@ function TopContributorsRows({
|
||||
|
||||
const handleRowClick = (
|
||||
record: AlertRuleTopContributors,
|
||||
): HTMLAttributes<AlertRuleTimelineTableResponse> => ({
|
||||
): HTMLAttributes<AlertRuleTimelineTableResponse> & {
|
||||
'data-testid': string;
|
||||
} => ({
|
||||
'data-testid': 'top-contributors-row',
|
||||
onClick: (): void => {
|
||||
logEvent('Alert history: Top contributors row: Clicked', {
|
||||
labels: record.labels,
|
||||
|
||||
@@ -31,7 +31,10 @@ function ViewAllDrawer({
|
||||
}}
|
||||
title="Viewing All Contributors"
|
||||
>
|
||||
<div className="top-contributors-card--view-all">
|
||||
<div
|
||||
className="top-contributors-card--view-all"
|
||||
data-testid="top-contributors-drawer"
|
||||
>
|
||||
<div className="top-contributors-card__content">
|
||||
<TopContributorsRows
|
||||
topContributors={topContributorsData}
|
||||
|
||||
@@ -32,8 +32,8 @@ function GraphWrapper({
|
||||
}, [data?.data]);
|
||||
|
||||
return (
|
||||
<div className="timeline-graph">
|
||||
<div className="timeline-graph__title">
|
||||
<div className="timeline-graph" data-testid="timeline-graph">
|
||||
<div className="timeline-graph__title" data-testid="timeline-graph-title">
|
||||
{totalCurrentTriggers} triggers in {relativeTime}
|
||||
</div>
|
||||
<div className="timeline-graph__chart">
|
||||
|
||||
@@ -118,7 +118,10 @@ function TimelineTableContent(): JSX.Element {
|
||||
|
||||
const handleRowClick = (
|
||||
record: AlertRuleTimelineTableResponse,
|
||||
): HTMLAttributes<AlertRuleTimelineTableResponse> => ({
|
||||
): HTMLAttributes<AlertRuleTimelineTableResponse> & {
|
||||
'data-testid': string;
|
||||
} => ({
|
||||
'data-testid': 'timeline-row',
|
||||
onClick: (): void => {
|
||||
void logEvent('Alert history: Timeline table row: Clicked', {
|
||||
ruleId: record.ruleID,
|
||||
@@ -128,12 +131,15 @@ function TimelineTableContent(): JSX.Element {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="timeline-table">
|
||||
<div className="timeline-table" data-testid="timeline-table">
|
||||
{/* If we don't wait to have the keys, the QuerySearch will not render them at first usage */}
|
||||
{!isLoadingKeys && hardcodedAttributeKeys ? (
|
||||
<div className="timeline-table__filter">
|
||||
<div className="timeline-table__filter-row">
|
||||
<div className="timeline-table__filter-search">
|
||||
<div
|
||||
className="timeline-table__filter-search"
|
||||
data-testid="timeline-filter-search"
|
||||
>
|
||||
<QuerySearch
|
||||
onChange={querySearchOnChange}
|
||||
queryData={queryData}
|
||||
@@ -155,6 +161,7 @@ function TimelineTableContent(): JSX.Element {
|
||||
<Skeleton.Input
|
||||
className="timeline-table__filter--loading-skeleton"
|
||||
active
|
||||
data-testid="timeline-filter-skeleton"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -172,14 +179,17 @@ function TimelineTableContent(): JSX.Element {
|
||||
locale={{
|
||||
emptyText:
|
||||
isError && apiError ? (
|
||||
<div className="timeline-table__error">
|
||||
<div className="timeline-table__error" data-testid="timeline-error">
|
||||
<ErrorContent error={apiError} />
|
||||
</div>
|
||||
) : undefined,
|
||||
}}
|
||||
footer={(): JSX.Element => (
|
||||
<div className="timeline-table__pagination">
|
||||
<div className="timeline-table__pagination-info">
|
||||
<div
|
||||
className="timeline-table__pagination-info"
|
||||
data-testid="timeline-footer-range"
|
||||
>
|
||||
{paginationConfig.showTotal?.(totalItems, [
|
||||
totalItems === 0
|
||||
? 0
|
||||
|
||||
@@ -21,7 +21,7 @@ export const timelineTableColumns = ({
|
||||
sorter: true,
|
||||
width: 140,
|
||||
render: (value): JSX.Element => (
|
||||
<div className="alert-rule-state">
|
||||
<div className="alert-rule-state" data-testid="timeline-row-state">
|
||||
<AlertState state={value} showLabel />
|
||||
</div>
|
||||
),
|
||||
@@ -30,7 +30,7 @@ export const timelineTableColumns = ({
|
||||
title: 'LABELS',
|
||||
dataIndex: 'labels',
|
||||
render: (labels): JSX.Element => (
|
||||
<div className="alert-rule-labels">
|
||||
<div className="alert-rule-labels" data-testid="timeline-row-labels">
|
||||
<AlertLabels labels={labels} />
|
||||
</div>
|
||||
),
|
||||
@@ -40,7 +40,10 @@ export const timelineTableColumns = ({
|
||||
dataIndex: 'unixMilli',
|
||||
width: 200,
|
||||
render: (value): JSX.Element => (
|
||||
<div className="alert-rule__created-at">
|
||||
<div
|
||||
className="alert-rule__created-at"
|
||||
data-testid="timeline-row-created-at"
|
||||
>
|
||||
{formatTimezoneAdjustedTimestamp(value, DATE_TIME_FORMATS.DASH_DATETIME)}
|
||||
</div>
|
||||
),
|
||||
@@ -53,7 +56,7 @@ export const timelineTableColumns = ({
|
||||
if (!record.relatedTracesLink && !record.relatedLogsLink) {
|
||||
return (
|
||||
<Tooltip title="No links available for this item">
|
||||
<Button type="text" ghost disabled>
|
||||
<Button type="text" ghost disabled data-testid="timeline-row-actions">
|
||||
<Ellipsis className="dropdown-icon" size="md" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
@@ -65,7 +68,7 @@ export const timelineTableColumns = ({
|
||||
relatedTracesLink={record.relatedTracesLink ?? ''}
|
||||
relatedLogsLink={record.relatedLogsLink ?? ''}
|
||||
>
|
||||
<Button type="text" ghost>
|
||||
<Button type="text" ghost data-testid="timeline-row-actions">
|
||||
<Ellipsis className="dropdown-icon" size="md" />
|
||||
</Button>
|
||||
</ConditionalAlertPopover>
|
||||
|
||||
@@ -23,6 +23,7 @@ function TimelineTabs(): JSX.Element {
|
||||
{
|
||||
value: TimelineTab.OVERALL_STATUS,
|
||||
label: 'Overall Status',
|
||||
testId: 'timeline-tab-overall-status',
|
||||
},
|
||||
{
|
||||
value: TimelineTab.TOP_5_CONTRIBUTORS,
|
||||
@@ -33,6 +34,7 @@ function TimelineTabs(): JSX.Element {
|
||||
</div>
|
||||
),
|
||||
disabled: true,
|
||||
testId: 'timeline-tab-top-contributors',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -57,14 +59,17 @@ function TimelineFilters(): JSX.Element {
|
||||
{
|
||||
value: TimelineFilter.ALL,
|
||||
label: 'All',
|
||||
testId: 'timeline-filter-all',
|
||||
},
|
||||
{
|
||||
value: TimelineFilter.FIRED,
|
||||
label: 'Fired',
|
||||
testId: 'timeline-filter-fired',
|
||||
},
|
||||
{
|
||||
value: TimelineFilter.RESOLVED,
|
||||
label: 'Resolved',
|
||||
testId: 'timeline-filter-resolved',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -66,10 +66,6 @@ function ThresholdItem({
|
||||
return '=';
|
||||
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
|
||||
return '!=';
|
||||
case AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO:
|
||||
return '>=';
|
||||
case AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO:
|
||||
return '<=';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -83,10 +83,6 @@ const getOperatorWord = (op: AlertThresholdOperator): string => {
|
||||
return 'equal';
|
||||
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
|
||||
return 'not equal';
|
||||
case AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO:
|
||||
return 'equal or exceed';
|
||||
case AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO:
|
||||
return 'equal or fall below';
|
||||
default:
|
||||
return 'exceed';
|
||||
}
|
||||
@@ -102,10 +98,6 @@ const getThresholdValue = (op: AlertThresholdOperator): number => {
|
||||
return 100;
|
||||
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
|
||||
return 0;
|
||||
case AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO:
|
||||
return 80;
|
||||
case AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO:
|
||||
return 50;
|
||||
default:
|
||||
return 80;
|
||||
}
|
||||
@@ -124,8 +116,6 @@ const getDataPoints = (
|
||||
[AlertThresholdOperator.IS_EQUAL_TO]: [95, 100, 105, 90, 100],
|
||||
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [5, 0, 10, 15, 0],
|
||||
[AlertThresholdOperator.IS_ABOVE]: [75, 85, 90, 78, 95],
|
||||
[AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO]: [75, 80, 90, 78, 95],
|
||||
[AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO]: [60, 50, 40, 55, 35],
|
||||
[AlertThresholdOperator.ABOVE_BELOW]: [75, 85, 90, 78, 95],
|
||||
},
|
||||
[AlertThresholdMatchType.ALL_THE_TIME]: {
|
||||
@@ -133,8 +123,6 @@ const getDataPoints = (
|
||||
[AlertThresholdOperator.IS_EQUAL_TO]: [100, 100, 100, 100, 100],
|
||||
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [5, 10, 15, 8, 12],
|
||||
[AlertThresholdOperator.IS_ABOVE]: [85, 87, 90, 88, 95],
|
||||
[AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO]: [80, 87, 90, 88, 95],
|
||||
[AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO]: [50, 40, 35, 42, 38],
|
||||
[AlertThresholdOperator.ABOVE_BELOW]: [85, 87, 90, 88, 95],
|
||||
},
|
||||
[AlertThresholdMatchType.ON_AVERAGE]: {
|
||||
@@ -142,8 +130,6 @@ const getDataPoints = (
|
||||
[AlertThresholdOperator.IS_EQUAL_TO]: [95, 105, 100, 95, 105],
|
||||
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [5, 10, 15, 8, 12],
|
||||
[AlertThresholdOperator.IS_ABOVE]: [75, 85, 90, 78, 95],
|
||||
[AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO]: [70, 85, 90, 75, 80],
|
||||
[AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO]: [60, 40, 55, 45, 50],
|
||||
[AlertThresholdOperator.ABOVE_BELOW]: [75, 85, 90, 78, 95],
|
||||
},
|
||||
[AlertThresholdMatchType.IN_TOTAL]: {
|
||||
@@ -151,8 +137,6 @@ const getDataPoints = (
|
||||
[AlertThresholdOperator.IS_EQUAL_TO]: [20, 20, 20, 20, 20],
|
||||
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [10, 15, 25, 5, 30],
|
||||
[AlertThresholdOperator.IS_ABOVE]: [10, 15, 25, 5, 30],
|
||||
[AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO]: [10, 15, 25, 5, 25],
|
||||
[AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO]: [8, 5, 10, 12, 15],
|
||||
[AlertThresholdOperator.ABOVE_BELOW]: [10, 15, 25, 5, 30],
|
||||
},
|
||||
[AlertThresholdMatchType.LAST]: {
|
||||
@@ -160,8 +144,6 @@ const getDataPoints = (
|
||||
[AlertThresholdOperator.IS_EQUAL_TO]: [75, 85, 90, 78, 100],
|
||||
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [75, 85, 90, 78, 25],
|
||||
[AlertThresholdOperator.IS_ABOVE]: [75, 85, 90, 78, 95],
|
||||
[AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO]: [75, 85, 90, 78, 80],
|
||||
[AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO]: [75, 85, 90, 78, 50],
|
||||
[AlertThresholdOperator.ABOVE_BELOW]: [75, 85, 90, 78, 95],
|
||||
},
|
||||
};
|
||||
@@ -175,8 +157,6 @@ const getTooltipOperatorSymbol = (op: AlertThresholdOperator): string => {
|
||||
[AlertThresholdOperator.IS_BELOW]: '<',
|
||||
[AlertThresholdOperator.IS_EQUAL_TO]: '=',
|
||||
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: '!=',
|
||||
[AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO]: '>=',
|
||||
[AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO]: '<=',
|
||||
[AlertThresholdOperator.ABOVE_BELOW]: '>',
|
||||
};
|
||||
return symbolMap[op] || '>';
|
||||
@@ -272,10 +252,6 @@ export const getMatchTypeTooltip = (
|
||||
return p === thresholdValue;
|
||||
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
|
||||
return p !== thresholdValue;
|
||||
case AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO:
|
||||
return p >= thresholdValue;
|
||||
case AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO:
|
||||
return p <= thresholdValue;
|
||||
default:
|
||||
return p > thresholdValue;
|
||||
}
|
||||
@@ -318,8 +294,7 @@ export const getMatchTypeTooltip = (
|
||||
matchType={matchType}
|
||||
>
|
||||
Alert triggers (all points {operatorWord} {thresholdValue})<br />
|
||||
If any point didn't {operatorWord} {thresholdValue}, no alert would
|
||||
fire
|
||||
If any point was {thresholdValue}, no alert would fire
|
||||
</TooltipExample>
|
||||
<TooltipLink />
|
||||
</TooltipContent>
|
||||
|
||||
@@ -34,6 +34,7 @@ function AdvancedOptions(): JSX.Element {
|
||||
})
|
||||
}
|
||||
value={advancedOptions.sendNotificationIfDataIsMissing.toleranceLimit}
|
||||
testId="send-notification-if-data-is-missing-input"
|
||||
/>
|
||||
<Typography.Text>Minutes</Typography.Text>
|
||||
</div>
|
||||
@@ -66,6 +67,7 @@ function AdvancedOptions(): JSX.Element {
|
||||
})
|
||||
}
|
||||
value={advancedOptions.enforceMinimumDatapoints.minimumDatapoints}
|
||||
testId="enforce-minimum-datapoints-input"
|
||||
/>
|
||||
<Typography.Text>Datapoints</Typography.Text>
|
||||
</div>
|
||||
|
||||
@@ -66,6 +66,7 @@ function EvaluationWindowPopover({
|
||||
tabIndex={0}
|
||||
data-value={option.value}
|
||||
data-section-id={sectionId}
|
||||
data-testid={`${sectionId}-option-${option.value}`}
|
||||
onClick={(): void => onChange(option.value)}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
|
||||
@@ -186,6 +186,7 @@ function Footer(): JSX.Element {
|
||||
color="primary"
|
||||
onClick={handleSaveAlert}
|
||||
disabled={disableButtons || Boolean(alertValidationMessage)}
|
||||
testId="save-alert-rule-button"
|
||||
>
|
||||
{isCreatingAlertRule || isUpdatingAlertRule ? (
|
||||
<Loader data-testid="save-alert-rule-loader-icon" size={14} />
|
||||
@@ -218,6 +219,7 @@ function Footer(): JSX.Element {
|
||||
color="secondary"
|
||||
onClick={handleTestNotification}
|
||||
disabled={disableButtons || Boolean(alertValidationMessage)}
|
||||
testId="test-notification-button"
|
||||
>
|
||||
{isTestingAlertRule ? (
|
||||
<Loader data-testid="test-notification-loader-icon" size={14} />
|
||||
@@ -249,6 +251,7 @@ function Footer(): JSX.Element {
|
||||
color="secondary"
|
||||
onClick={handleDiscard}
|
||||
disabled={disableButtons}
|
||||
testId="discard-alert-rule-button"
|
||||
>
|
||||
<X size={14} /> Discard
|
||||
</Button>
|
||||
|
||||
@@ -532,7 +532,7 @@ describe('Footer utils', () => {
|
||||
['symbol', '>', 'at_least_once'],
|
||||
['literal', 'above', 'at_least_once'],
|
||||
['short', 'eq', 'avg'],
|
||||
['inclusive', 'above_or_equal', 'at_least_once'],
|
||||
['UI-unexposed', 'above_or_equal', 'at_least_once'],
|
||||
])(
|
||||
'round-trips %s op/matchType unchanged through the submit payload (%s / %s)',
|
||||
(_desc, op, matchType) => {
|
||||
|
||||
@@ -332,20 +332,25 @@ describe('CreateAlertV2 utils', () => {
|
||||
['not_equal', AlertThresholdOperator.IS_NOT_EQUAL_TO],
|
||||
['not_eq', AlertThresholdOperator.IS_NOT_EQUAL_TO],
|
||||
['!=', AlertThresholdOperator.IS_NOT_EQUAL_TO],
|
||||
['5', AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO],
|
||||
['above_or_equal', AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO],
|
||||
['above_or_eq', AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO],
|
||||
['>=', AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO],
|
||||
['6', AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO],
|
||||
['below_or_equal', AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO],
|
||||
['below_or_eq', AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO],
|
||||
['<=', AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO],
|
||||
['7', AlertThresholdOperator.ABOVE_BELOW],
|
||||
['outside_bounds', AlertThresholdOperator.ABOVE_BELOW],
|
||||
])('maps backend alias %s to canonical enum', (alias, expected) => {
|
||||
expect(normalizeOperator(alias)).toBe(expected);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['5', 'above_or_equal'],
|
||||
['above_or_equal', 'above_or_equal'],
|
||||
['above_or_eq', 'above_or_equal'],
|
||||
['>=', 'above_or_equal'],
|
||||
['6', 'below_or_equal'],
|
||||
['below_or_equal', 'below_or_equal'],
|
||||
['below_or_eq', 'below_or_equal'],
|
||||
['<=', 'below_or_equal'],
|
||||
])('returns undefined for UI-unexposed alias %s (%s family)', (alias) => {
|
||||
expect(normalizeOperator(alias)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for unknown values', () => {
|
||||
expect(normalizeOperator('gibberish')).toBeUndefined();
|
||||
expect(normalizeOperator(undefined)).toBeUndefined();
|
||||
@@ -408,8 +413,8 @@ describe('CreateAlertV2 utils', () => {
|
||||
['symbol', '>', 'at_least_once'],
|
||||
['short form', 'eq', 'avg'],
|
||||
['mixed numeric and literal', '7', 'last'],
|
||||
['inclusive literal operator', 'above_or_equal', 'at_least_once'],
|
||||
['inclusive numeric operator', '5', 'at_least_once'],
|
||||
['UI-unexposed operator', 'above_or_equal', 'at_least_once'],
|
||||
['UI-unexposed numeric operator', '5', 'at_least_once'],
|
||||
])('preserves %s op/matchType verbatim (%s / %s)', (_desc, op, matchType) => {
|
||||
const state = getThresholdStateFromAlertDef(buildDef(op, matchType));
|
||||
expect(state.operator).toBe(op);
|
||||
|
||||
@@ -2,8 +2,9 @@ import { AlertThresholdMatchType, AlertThresholdOperator } from './types';
|
||||
|
||||
// Mirrors the backend's CompareOperator.Normalize() in
|
||||
// pkg/types/ruletypes/compare.go. Maps any accepted alias to the enum value
|
||||
// the dropdown understands. Returns undefined for unknown values so callers
|
||||
// can keep the raw value on screen instead of silently rewriting it.
|
||||
// the dropdown understands. Returns undefined for aliases the UI does not
|
||||
// expose (e.g. above_or_equal, below_or_equal) so callers can keep the raw
|
||||
// value on screen instead of silently rewriting it.
|
||||
export function normalizeOperator(
|
||||
raw: string | undefined,
|
||||
): AlertThresholdOperator | undefined {
|
||||
@@ -26,16 +27,6 @@ export function normalizeOperator(
|
||||
case 'not_eq':
|
||||
case '!=':
|
||||
return AlertThresholdOperator.IS_NOT_EQUAL_TO;
|
||||
case '5':
|
||||
case 'above_or_equal':
|
||||
case 'above_or_eq':
|
||||
case '>=':
|
||||
return AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO;
|
||||
case '6':
|
||||
case 'below_or_equal':
|
||||
case 'below_or_eq':
|
||||
case '<=':
|
||||
return AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO;
|
||||
case '7':
|
||||
case 'outside_bounds':
|
||||
return AlertThresholdOperator.ABOVE_BELOW;
|
||||
|
||||
@@ -125,14 +125,6 @@ export const THRESHOLD_OPERATOR_OPTIONS = [
|
||||
{ value: AlertThresholdOperator.IS_BELOW, label: 'BELOW' },
|
||||
{ value: AlertThresholdOperator.IS_EQUAL_TO, label: 'EQUAL TO' },
|
||||
{ value: AlertThresholdOperator.IS_NOT_EQUAL_TO, label: 'NOT EQUAL TO' },
|
||||
{
|
||||
value: AlertThresholdOperator.IS_ABOVE_OR_EQUAL_TO,
|
||||
label: 'ABOVE OR EQUAL TO',
|
||||
},
|
||||
{
|
||||
value: AlertThresholdOperator.IS_BELOW_OR_EQUAL_TO,
|
||||
label: 'BELOW OR EQUAL TO',
|
||||
},
|
||||
];
|
||||
|
||||
export const ANOMALY_THRESHOLD_OPERATOR_OPTIONS = [
|
||||
|
||||
@@ -99,8 +99,6 @@ export enum AlertThresholdOperator {
|
||||
IS_BELOW = 'below',
|
||||
IS_EQUAL_TO = 'equal',
|
||||
IS_NOT_EQUAL_TO = 'not_equal',
|
||||
IS_ABOVE_OR_EQUAL_TO = 'above_or_equal',
|
||||
IS_BELOW_OR_EQUAL_TO = 'below_or_equal',
|
||||
ABOVE_BELOW = 'outside_bounds',
|
||||
}
|
||||
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { sortByMeanDesc } from '../sortByMeanDesc';
|
||||
|
||||
interface Item {
|
||||
name: string;
|
||||
values: (number | null)[];
|
||||
}
|
||||
|
||||
const item = (name: string, values: (number | null)[]): Item => ({
|
||||
name,
|
||||
values,
|
||||
});
|
||||
|
||||
const sorted = (items: Item[]): string[] =>
|
||||
sortByMeanDesc(items, {
|
||||
getValues: (entry) => entry.values,
|
||||
getKey: (entry) => entry.name,
|
||||
}).map((entry) => entry.name);
|
||||
|
||||
describe('sortByMeanDesc', () => {
|
||||
it('orders items by descending mean', () => {
|
||||
expect(
|
||||
sorted([item('a', [1, 1]), item('b', [10, 20]), item('c', [5, 5])]),
|
||||
).toStrictEqual(['b', 'c', 'a']);
|
||||
});
|
||||
|
||||
it('sinks items with no finite values to the bottom', () => {
|
||||
expect(
|
||||
sorted([item('a', []), item('b', [NaN, Infinity]), item('c', [1])]),
|
||||
).toStrictEqual(['c', 'a', 'b']);
|
||||
});
|
||||
|
||||
it('ignores non-finite and null values when averaging', () => {
|
||||
// Mean over the finite values only is 10, so NaN must not poison it to last place.
|
||||
expect(
|
||||
sorted([item('a', [2, 2]), item('b', [10, NaN]), item('c', [4, null])]),
|
||||
).toStrictEqual(['b', 'c', 'a']);
|
||||
});
|
||||
|
||||
it('produces the same order whichever order the items arrive in', () => {
|
||||
const a = item('a', [5]);
|
||||
const b = item('b', [5]);
|
||||
const c = item('c', [9]);
|
||||
|
||||
expect(sorted([a, b, c])).toStrictEqual(sorted([c, b, a]));
|
||||
expect(sorted([b, a, c])).toStrictEqual(['c', 'a', 'b']);
|
||||
});
|
||||
|
||||
it('tiebreaks equal means on the key', () => {
|
||||
expect(sorted([item('b', [1]), item('a', [1])])).toStrictEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('tiebreaks on the key for items that have no finite values either', () => {
|
||||
expect(sorted([item('b', []), item('a', [NaN])])).toStrictEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('does not mutate the input array', () => {
|
||||
const items = [item('a', [1]), item('b', [9])];
|
||||
|
||||
expect(sorted(items)).toStrictEqual(['b', 'a']);
|
||||
expect(items.map((entry) => entry.name)).toStrictEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('returns an empty array for no items', () => {
|
||||
expect(sorted([])).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
type MeanInput = number | null | undefined;
|
||||
|
||||
function finiteMean(values: Iterable<MeanInput>): number | null {
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
for (const value of values) {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
sum += value;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
return count > 0 ? sum / count : null;
|
||||
}
|
||||
|
||||
export interface SortByMeanDescOptions<T> {
|
||||
/** Numeric values of an item; non-finite entries are ignored. */
|
||||
getValues: (item: T) => Iterable<MeanInput>;
|
||||
/** Stable identity of an item, used to break ties on equal means. */
|
||||
getKey: (item: T) => string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Orders series by descending mean, on a copy (V1 parity: `utils/getSortedSeriesData`). The wire
|
||||
* order can't stand in for it: the backend returns series in Go map-iteration order.
|
||||
*
|
||||
* Call this before building the uPlot config and aligned data — those two and click attribution
|
||||
* all index the list positionally, and uPlot draws a stacked bar's segments in series-index order.
|
||||
*/
|
||||
export function sortByMeanDesc<T>(
|
||||
items: T[],
|
||||
{ getValues, getKey }: SortByMeanDescOptions<T>,
|
||||
): T[] {
|
||||
return items
|
||||
.map((item) => ({ item, mean: finiteMean(getValues(item)) }))
|
||||
.sort((a, b) => {
|
||||
if (a.mean !== null && b.mean !== null && a.mean !== b.mean) {
|
||||
return b.mean - a.mean;
|
||||
}
|
||||
if ((a.mean === null) !== (b.mean === null)) {
|
||||
return a.mean === null ? 1 : -1;
|
||||
}
|
||||
return getKey(a.item).localeCompare(getKey(b.item));
|
||||
})
|
||||
.map(({ item }) => item);
|
||||
}
|
||||
@@ -119,6 +119,7 @@ function BasicInfo({
|
||||
<SeveritySelect
|
||||
getPopupContainer={popupContainer}
|
||||
defaultValue="critical"
|
||||
data-testid="alert-severity-select"
|
||||
onChange={(value: unknown | string): void => {
|
||||
const s = (value as string) || 'critical';
|
||||
setAlertDef({
|
||||
@@ -147,6 +148,7 @@ function BasicInfo({
|
||||
]}
|
||||
>
|
||||
<InputSmall
|
||||
data-testid="alert-name-input-v1"
|
||||
onChange={(e): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
@@ -161,6 +163,7 @@ function BasicInfo({
|
||||
name={['annotations', 'description']}
|
||||
>
|
||||
<TextareaMedium
|
||||
data-testid="alert-description-input"
|
||||
onChange={(e): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
|
||||
@@ -105,7 +105,7 @@ function QuerySection({
|
||||
{
|
||||
label: (
|
||||
<Tooltip title="Query Builder">
|
||||
<Button className="nav-btns">
|
||||
<Button className="nav-btns" data-testid="query-builder-tab">
|
||||
<Atom size={14} />
|
||||
<Typography.Text>Query Builder</Typography.Text>
|
||||
</Button>
|
||||
@@ -122,7 +122,11 @@ function QuerySection({
|
||||
: 'ClickHouse'
|
||||
}
|
||||
>
|
||||
<Button className="nav-btns" disabled={isAnomalyDetection}>
|
||||
<Button
|
||||
className="nav-btns"
|
||||
disabled={isAnomalyDetection}
|
||||
data-testid="clickhouse-tab"
|
||||
>
|
||||
<Terminal size={14} />
|
||||
<Typography.Text>ClickHouse Query</Typography.Text>
|
||||
</Button>
|
||||
@@ -162,7 +166,11 @@ function QuerySection({
|
||||
: 'ClickHouse'
|
||||
}
|
||||
>
|
||||
<Button className="nav-btns" disabled={isAnomalyDetection}>
|
||||
<Button
|
||||
className="nav-btns"
|
||||
disabled={isAnomalyDetection}
|
||||
data-testid="clickhouse-tab"
|
||||
>
|
||||
<Terminal size={14} />
|
||||
<Typography.Text>ClickHouse Query</Typography.Text>
|
||||
</Button>
|
||||
@@ -180,7 +188,11 @@ function QuerySection({
|
||||
: 'PromQL'
|
||||
}
|
||||
>
|
||||
<Button className="nav-btns" disabled={isAnomalyDetection}>
|
||||
<Button
|
||||
className="nav-btns"
|
||||
disabled={isAnomalyDetection}
|
||||
data-testid="promql-tab"
|
||||
>
|
||||
<PromQLIcon
|
||||
fillColor={isDarkMode ? Color.BG_VANILLA_200 : Color.BG_INK_300}
|
||||
/>
|
||||
|
||||
@@ -80,6 +80,7 @@ function RuleOptions({
|
||||
defaultValue={defaultCompareOp}
|
||||
value={alertDef.condition?.op}
|
||||
style={{ minWidth: '120px' }}
|
||||
data-testid="alert-threshold-op-select"
|
||||
onChange={(value: string | unknown): void => {
|
||||
const newOp = (value as string) || '';
|
||||
|
||||
@@ -116,6 +117,7 @@ function RuleOptions({
|
||||
defaultValue={defaultMatchType}
|
||||
style={{ minWidth: '130px' }}
|
||||
value={alertDef.condition?.matchType}
|
||||
data-testid="alert-threshold-match-type-select-v1"
|
||||
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
|
||||
>
|
||||
<Select.Option value="1">{t('option_atleastonce')}</Select.Option>
|
||||
@@ -177,6 +179,7 @@ function RuleOptions({
|
||||
style={{ minWidth: '120px' }}
|
||||
value={alertDef.evalWindow}
|
||||
onChange={onChangeEvalWindow}
|
||||
data-testid="alert-eval-window-select"
|
||||
>
|
||||
<Select.Option value="5m0s">{t('option_5min')}</Select.Option>
|
||||
<Select.Option value="10m0s">{t('option_10min')}</Select.Option>
|
||||
@@ -194,6 +197,7 @@ function RuleOptions({
|
||||
style={{ minWidth: '120px' }}
|
||||
value={alertDef.evalWindow}
|
||||
onChange={onChangeEvalWindow}
|
||||
data-testid="alert-eval-window-select"
|
||||
>
|
||||
<Select.Option value="5m0s">{t('option_5min')}</Select.Option>
|
||||
<Select.Option value="10m0s">{t('option_10min')}</Select.Option>
|
||||
@@ -395,6 +399,7 @@ function RuleOptions({
|
||||
value={alertDef?.condition?.target}
|
||||
onChange={onChange}
|
||||
type="number"
|
||||
data-testid="alert-threshold-target-input"
|
||||
onWheel={(e): void => e.currentTarget.blur()}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -844,8 +844,6 @@ function FormAlertRules({
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
|
||||
<div
|
||||
id="top"
|
||||
className={`form-alert-rules-container ${
|
||||
@@ -968,6 +966,7 @@ function FormAlertRules({
|
||||
!isChannelConfigurationValid ||
|
||||
queryStatus === 'error'
|
||||
}
|
||||
data-testid="alert-save-button"
|
||||
>
|
||||
{isNewRule ? t('button_createrule') : t('button_savechanges')}
|
||||
</ActionButton>
|
||||
@@ -981,6 +980,7 @@ function FormAlertRules({
|
||||
}
|
||||
type="default"
|
||||
onClick={onTestRuleHandler}
|
||||
data-testid="alert-test-button"
|
||||
>
|
||||
{' '}
|
||||
{t('button_testrule')}
|
||||
@@ -989,6 +989,7 @@ function FormAlertRules({
|
||||
disabled={loading || false}
|
||||
type="default"
|
||||
onClick={onCancelHandler}
|
||||
data-testid="alert-cancel-button"
|
||||
>
|
||||
{isNewRule && t('button_cancelchanges')}
|
||||
{ruleId && !isEmpty(ruleId) && t('button_discard')}
|
||||
@@ -998,6 +999,7 @@ function FormAlertRules({
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
testId="alert-save-confirm-dialog"
|
||||
open={isConfirmSaveOpen}
|
||||
onOpenChange={setIsConfirmSaveOpen}
|
||||
title={t('confirm_save_title')}
|
||||
|
||||
@@ -174,6 +174,7 @@ function LabelSelect({
|
||||
|
||||
<div style={{ display: 'flex', width: '100%' }}>
|
||||
<Input
|
||||
data-testid="alert-labels-input-v1"
|
||||
placeholder={renderPlaceholder()}
|
||||
onChange={handleLabelChange}
|
||||
onKeyUp={(e): void => {
|
||||
|
||||
@@ -13,15 +13,8 @@ import {
|
||||
import { AxiosError } from 'axios';
|
||||
import APIError from 'types/api/error';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import {
|
||||
QuickFilterChangeEventData,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraFilterCustomizedEvent,
|
||||
logInfraMonitoringListViewedEvent,
|
||||
} from 'constants/events';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import K8sBaseDetails, {
|
||||
K8sDetailsFilters,
|
||||
@@ -193,19 +186,6 @@ function Hosts(): JSX.Element {
|
||||
hostInitialLogTracesExpression(host),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleQuickFilterChange = useCallback(
|
||||
(data: QuickFilterChangeEventData): void => {
|
||||
logInfraFilterCustomizedEvent(
|
||||
InfraMonitoringEntity.HOSTS,
|
||||
'quick_filter',
|
||||
data.expression,
|
||||
data.filterItemKeys,
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const controlListPrefix = !showFilters ? (
|
||||
<div className={styles.quickFiltersToggleContainer}>
|
||||
<Button
|
||||
@@ -219,10 +199,6 @@ function Hosts(): JSX.Element {
|
||||
</div>
|
||||
) : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
logInfraMonitoringListViewedEvent(InfraMonitoringEntity.HOSTS);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.infraMonitoringContainer}>
|
||||
@@ -251,7 +227,6 @@ function Hosts(): JSX.Element {
|
||||
startUnixMilli,
|
||||
endUnixMilli,
|
||||
}}
|
||||
onQuickFilterChange={handleQuickFilterChange}
|
||||
/>
|
||||
</>
|
||||
</OverlayScrollbar>
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
min-width: 800px;
|
||||
min-width: 595px;
|
||||
|
||||
> * {
|
||||
min-width: 0;
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { ToggleGroup, ToggleGroupItem } from '@signozhq/ui/toggle-group';
|
||||
import { logInfraFilterCustomizedEvent } from 'constants/events';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import {
|
||||
StatusFilterValue,
|
||||
useInfraMonitoringPageListing,
|
||||
useInfraMonitoringStatusFilter,
|
||||
} from 'container/InfraMonitoringK8sV2/hooks';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
import styles from './StatusFilter.module.scss';
|
||||
|
||||
@@ -22,21 +19,11 @@ const statusOptions: Array<{
|
||||
function StatusFilter(): JSX.Element {
|
||||
const [statusFilter, setStatusFilter] = useInfraMonitoringStatusFilter();
|
||||
const [, setCurrentPage] = useInfraMonitoringPageListing();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const handleChange = (value: string): void => {
|
||||
if (value !== undefined) {
|
||||
void setStatusFilter(value === 'all' ? '' : (value as StatusFilterValue));
|
||||
void setCurrentPage(1);
|
||||
|
||||
const expression =
|
||||
currentQuery.builder.queryData[0]?.filter?.expression || '';
|
||||
logInfraFilterCustomizedEvent(
|
||||
InfraMonitoringEntity.HOSTS,
|
||||
'host_status_toggle',
|
||||
expression,
|
||||
value === 'all' ? [] : ['host_status'],
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { Progress } from '@signozhq/ui/progress';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import {
|
||||
InframonitoringtypesHostRecordDTO,
|
||||
InframonitoringtypesHostStatusDTO,
|
||||
@@ -9,7 +10,6 @@ import {
|
||||
import { K8sDetailsMetadataConfig } from 'container/InfraMonitoringK8sV2/Base/K8sBaseDetails';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { formatValueForExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { TextNoData } from 'container/InfraMonitoringK8sV2/components';
|
||||
import { SelectedItemParams } from 'container/InfraMonitoringK8sV2/hooks';
|
||||
import {
|
||||
getHostQueryPayload,
|
||||
@@ -70,7 +70,7 @@ export const hostDetailsMetadataConfig: HostDetailMetadataConfigType[] = [
|
||||
{value}
|
||||
</Badge>
|
||||
) : (
|
||||
<TextNoData type="typography" />
|
||||
<Typography.Text>-</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -175,7 +175,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
accessorFn: (row): number => row.cpu,
|
||||
width: { min: 220 },
|
||||
enableSort: true,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = value as number;
|
||||
return (
|
||||
<div className={styles.progressContainer}>
|
||||
@@ -183,7 +183,6 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
value={cpu}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
attribute="CPU metric"
|
||||
rowId={rowId}
|
||||
>
|
||||
<EntityProgressBar value={cpu} type="cpu" />
|
||||
</ValidateColumnValueWrapper>
|
||||
@@ -198,13 +197,14 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
tooltip="Excluding cache memory."
|
||||
docPath="/infrastructure-monitoring/host-monitoring#memory-usage"
|
||||
>
|
||||
Memory Usage (WSS)
|
||||
Memory Usage
|
||||
<br /> (WSS)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.memory,
|
||||
width: { min: 200 },
|
||||
enableSort: true,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
return (
|
||||
<div className={styles.progressContainer}>
|
||||
@@ -212,7 +212,6 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
value={memory}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
attribute="memory metric"
|
||||
rowId={rowId}
|
||||
>
|
||||
<EntityProgressBar value={memory} type="memory" />
|
||||
</ValidateColumnValueWrapper>
|
||||
@@ -220,33 +219,6 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'diskUsage',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#disk-usage">
|
||||
Disk Usage
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.diskUsage,
|
||||
width: { min: 200 },
|
||||
enableSort: true,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
const diskUsage = value as number;
|
||||
|
||||
return (
|
||||
<div className={styles.progressContainer}>
|
||||
<ValidateColumnValueWrapper
|
||||
value={diskUsage}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
attribute="disk usage metric"
|
||||
rowId={rowId}
|
||||
>
|
||||
<EntityProgressBar value={diskUsage} type="disk" />
|
||||
</ValidateColumnValueWrapper>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'wait',
|
||||
header: (): React.ReactNode => (
|
||||
@@ -257,7 +229,7 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
accessorFn: (row): number => row.wait,
|
||||
width: { min: 120 },
|
||||
enableSort: true,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const wait = value as number;
|
||||
|
||||
return (
|
||||
@@ -265,7 +237,6 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
value={wait}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
attribute="IOWait metric"
|
||||
rowId={rowId}
|
||||
>
|
||||
<TanStackTable.Text>
|
||||
{`${Number((wait * 100).toFixed(1))}%`}
|
||||
@@ -278,13 +249,14 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
id: 'load15',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#load-avg">
|
||||
Load Avg (15min)
|
||||
Load Avg
|
||||
<br /> (15min)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.load15,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const load15 = Number(value);
|
||||
|
||||
return (
|
||||
@@ -292,11 +264,36 @@ export const hostColumnsConfig: HostColumnConfigType[] = [
|
||||
value={load15}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
attribute="load average metric"
|
||||
rowId={rowId}
|
||||
>
|
||||
<TanStackTable.Text>{load15.toFixed(2)}</TanStackTable.Text>
|
||||
</ValidateColumnValueWrapper>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'diskUsage',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/host-monitoring#disk-usage">
|
||||
Disk Usage
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.diskUsage,
|
||||
width: { min: 200 },
|
||||
enableSort: true,
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const diskUsage = value as number;
|
||||
|
||||
return (
|
||||
<div className={styles.progressContainer}>
|
||||
<ValidateColumnValueWrapper
|
||||
value={diskUsage}
|
||||
entity={InfraMonitoringEntity.HOSTS}
|
||||
attribute="disk usage metric"
|
||||
>
|
||||
<EntityProgressBar value={diskUsage} type="disk" />
|
||||
</ValidateColumnValueWrapper>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
IQuickFiltersConfig,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { TriangleAlert } from '@signozhq/icons';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { CellValueTooltip } from 'container/InfraMonitoringK8sV2/components';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -21,7 +21,7 @@ export function HostnameCell({
|
||||
}): React.ReactElement {
|
||||
const isEmpty = !hostName || !hostName.trim();
|
||||
if (!isEmpty) {
|
||||
return <TanStackTable.Text>{hostName}</TanStackTable.Text>;
|
||||
return <CellValueTooltip value={hostName} />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
import { Card } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -21,9 +21,6 @@ import {
|
||||
getUserExpressionFromCombined,
|
||||
} from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8s/constants';
|
||||
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
@@ -36,14 +33,9 @@ import {
|
||||
import { getOldLogsOperatorFromNew } from 'hooks/logs/useActiveLog';
|
||||
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
|
||||
import useScrollToLog from 'hooks/logs/useScrollToLog';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
|
||||
import EntityEmptyState from '../EntityEmptyState/EntityEmptyState';
|
||||
@@ -142,7 +134,7 @@ function EntityLogsContent({
|
||||
if (validation.isValid) {
|
||||
querySearchOnRun(newUserExpression);
|
||||
|
||||
void logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category,
|
||||
@@ -170,45 +162,6 @@ function EntityLogsContent({
|
||||
virtuosoRef,
|
||||
});
|
||||
|
||||
const { updateAllQueriesOperators } = useQueryBuilder();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const handleOpenInExplorer = useCallback(
|
||||
(e: React.MouseEvent<Element, MouseEvent>, log: ILog) => {
|
||||
const baseQuery = updateAllQueriesOperators(
|
||||
initialQueriesMap[DataSource.LOGS],
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.LOGS,
|
||||
);
|
||||
|
||||
const queryParams = {
|
||||
[QueryParams.activeLogId]: `"${log?.id}"`,
|
||||
[QueryParams.startTime]: timeRange.startTime.toString(),
|
||||
[QueryParams.endTime]: timeRange.endTime.toString(),
|
||||
[QueryParams.compositeQuery]: JSON.stringify({
|
||||
...baseQuery,
|
||||
builder: {
|
||||
...baseQuery.builder,
|
||||
queryData: baseQuery.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
filter: { expression },
|
||||
})),
|
||||
},
|
||||
} satisfies Query),
|
||||
};
|
||||
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`, {
|
||||
newTab: !!e && isModifierKeyPressed(e),
|
||||
});
|
||||
},
|
||||
[
|
||||
timeRange.startTime,
|
||||
timeRange.endTime,
|
||||
expression,
|
||||
safeNavigate,
|
||||
updateAllQueriesOperators,
|
||||
],
|
||||
);
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(_: number, logToRender: ILog): JSX.Element => {
|
||||
return (
|
||||
@@ -334,7 +287,6 @@ function EntityLogsContent({
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
onScrollToLog={handleScrollToLog}
|
||||
handleOpenInExplorer={(e): void => handleOpenInExplorer(e, activeLog)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
text-wrap: auto;
|
||||
}
|
||||
|
||||
.infoIcon {
|
||||
|
||||
@@ -8,6 +8,7 @@ const DOCS_BASE_URL = `${process.env.DOCS_BASE_URL}/docs`;
|
||||
|
||||
interface ColumnHeaderProps {
|
||||
children?: React.ReactNode;
|
||||
title?: string;
|
||||
docPath?: string;
|
||||
tooltip?: string;
|
||||
className?: string;
|
||||
@@ -15,6 +16,7 @@ interface ColumnHeaderProps {
|
||||
|
||||
function ColumnHeader({
|
||||
children,
|
||||
title,
|
||||
docPath,
|
||||
tooltip,
|
||||
className,
|
||||
@@ -24,6 +26,16 @@ function ColumnHeader({
|
||||
return children;
|
||||
}
|
||||
|
||||
if (title) {
|
||||
const parts = title.split('\n');
|
||||
return parts.map((part, index) => (
|
||||
<div key={`${part}-${index}`}>
|
||||
{part}
|
||||
{index < parts.length - 1 && <br />}
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Copy, X } from '@signozhq/icons';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { X } from '@signozhq/icons';
|
||||
import { Divider } from '@signozhq/ui/divider';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DrawerWrapper, DrawerWrapperProps } from '@signozhq/ui/drawer';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Drawer } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
|
||||
import APIError from 'types/api/error';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import {
|
||||
GlobalTimeProvider,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
@@ -24,10 +22,8 @@ import LoadingContainer from '../LoadingContainer';
|
||||
|
||||
import K8sBaseDetailsContent from './K8sBaseDetailsContent';
|
||||
import { K8sBaseDetailsProps } from './types';
|
||||
import { useDrawerLifecycleStore } from './useDrawerLifecycleStore';
|
||||
|
||||
import styles from '../EntityDetailsUtils/entityDetails.module.scss';
|
||||
import { INFRA_MONITORING_DETAILS_CACHE_TIME } from 'constants/queryCacheTime';
|
||||
import '../EntityDetailsUtils/entityDetails.styles.scss';
|
||||
|
||||
export type {
|
||||
CustomTab,
|
||||
@@ -38,23 +34,6 @@ export type {
|
||||
K8sDetailsMetadataConfig,
|
||||
} from './types';
|
||||
|
||||
// TODO(H4ad): Improve this on component level
|
||||
const DRAWER_TRANSITION = { duration: 0.3, ease: [0.25, 0.1, 0.25, 1] };
|
||||
const DRAWER_MOTION_PROPS = {
|
||||
onOpenAutoFocus: (e: Event): void => e.preventDefault(),
|
||||
initial: { opacity: 0, transform: 'translateX(100%)' },
|
||||
animate: {
|
||||
opacity: 1,
|
||||
transform: 'translateX(0%)',
|
||||
transition: DRAWER_TRANSITION,
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
transform: 'translateX(100%)',
|
||||
transition: DRAWER_TRANSITION,
|
||||
},
|
||||
} as unknown as DrawerWrapperProps['drawerContentProps'];
|
||||
|
||||
export default function K8sBaseDetails<T>({
|
||||
category,
|
||||
eventCategory,
|
||||
@@ -79,6 +58,8 @@ export default function K8sBaseDetails<T>({
|
||||
(s) => s.getAutoRefreshQueryKey,
|
||||
);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const [selectedItemParams, setSelectedItemParams] =
|
||||
useInfraMonitoringSelectedItemParams();
|
||||
const selectedItem = selectedItemParams.selectedItem;
|
||||
@@ -120,8 +101,6 @@ export default function K8sBaseDetails<T>({
|
||||
|
||||
return fetchEntityData({ filter: { expression }, start, end }, signal);
|
||||
},
|
||||
cacheTime: INFRA_MONITORING_DETAILS_CACHE_TIME,
|
||||
staleTime: INFRA_MONITORING_DETAILS_CACHE_TIME,
|
||||
enabled: !!selectedItem,
|
||||
});
|
||||
|
||||
@@ -142,39 +121,10 @@ export default function K8sBaseDetails<T>({
|
||||
return getInitialEventsExpression(entity);
|
||||
}, [entity, getInitialEventsExpression]);
|
||||
|
||||
const markDrawerOpened = useDrawerLifecycleStore((s) => s.markOpened);
|
||||
const markDrawerClosed = useDrawerLifecycleStore((s) => s.markClosed);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedItem) {
|
||||
markDrawerOpened();
|
||||
} else {
|
||||
markDrawerClosed();
|
||||
}
|
||||
}, [selectedItem, markDrawerOpened, markDrawerClosed]);
|
||||
|
||||
const handleClose = useCallback((): void => {
|
||||
setSelectedItemParams(null);
|
||||
}, [setSelectedItemParams]);
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(open: boolean): void => {
|
||||
if (!open) {
|
||||
handleClose();
|
||||
}
|
||||
},
|
||||
[handleClose],
|
||||
);
|
||||
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const handleCopyId = useCallback((): void => {
|
||||
if (selectedItem) {
|
||||
copyToClipboard(selectedItem);
|
||||
toast.success('ID copied to clipboard', { position: 'bottom-left' });
|
||||
}
|
||||
}, [copyToClipboard, selectedItem]);
|
||||
|
||||
const entityName = entity ? getEntityName(entity) : '';
|
||||
|
||||
useEffect(() => {
|
||||
@@ -187,49 +137,31 @@ export default function K8sBaseDetails<T>({
|
||||
}
|
||||
}, [entity, eventCategory]);
|
||||
|
||||
// TODO(H4ad): Improve this on component level
|
||||
// DrawerWrapper types `title` as string but renders any ReactNode.
|
||||
const drawerTitle = (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={handleClose}
|
||||
data-testid="close-drawer-button"
|
||||
className={styles.closeButton}
|
||||
prefix={<X />}
|
||||
/>
|
||||
<Divider type="vertical" />
|
||||
<Typography.Text className={styles.title}>
|
||||
{entityName ||
|
||||
((isEntityError || hasResponseError) && 'Failed to load entity details') ||
|
||||
(isEntityLoading && 'Loading...') ||
|
||||
'-'}
|
||||
</Typography.Text>
|
||||
<TooltipSimple title="Copy ID">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
onClick={handleCopyId}
|
||||
data-testid="copy-id-button"
|
||||
>
|
||||
<Copy size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</>
|
||||
) as unknown as string;
|
||||
|
||||
return (
|
||||
<DrawerWrapper
|
||||
<Drawer
|
||||
width="70%"
|
||||
title={
|
||||
<>
|
||||
<Divider type="vertical" />
|
||||
<Typography.Text className="title">
|
||||
{entityName ||
|
||||
((isEntityError || hasResponseError) &&
|
||||
'Failed to load entity details') ||
|
||||
(isEntityLoading && 'Loading...') ||
|
||||
'-'}
|
||||
</Typography.Text>
|
||||
</>
|
||||
}
|
||||
placement="right"
|
||||
onClose={handleClose}
|
||||
open={!!selectedItem}
|
||||
onOpenChange={handleOpenChange}
|
||||
direction="right"
|
||||
title={drawerTitle}
|
||||
showCloseButton={false}
|
||||
drawerContentProps={DRAWER_MOTION_PROPS}
|
||||
className={styles.entityDetailDrawer}
|
||||
style={{
|
||||
overscrollBehavior: 'contain',
|
||||
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||
}}
|
||||
className="entity-detail-drawer"
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||
>
|
||||
{(isEntityLoading || !selectedItem) && <LoadingContainer />}
|
||||
|
||||
@@ -278,6 +210,6 @@ export default function K8sBaseDetails<T>({
|
||||
/>
|
||||
</GlobalTimeProvider>
|
||||
)}
|
||||
</DrawerWrapper>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Fragment, useEffect, useMemo, useRef } from 'react';
|
||||
import { Fragment, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
BarChart,
|
||||
ChevronsLeftRight,
|
||||
@@ -6,17 +6,12 @@ import {
|
||||
DraftingCompass,
|
||||
ScrollText,
|
||||
} from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraDrawerTabViewedEvent,
|
||||
logInfraExplorerNavigatedEvent,
|
||||
} from 'constants/events';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
@@ -47,9 +42,6 @@ import {
|
||||
|
||||
import { EntityCountsSection } from './components/EntityCountsSection/EntityCountsSection';
|
||||
import { K8sBaseDetailsContentProps } from './types';
|
||||
import { getDrawerDurationMs } from './useDrawerLifecycleStore';
|
||||
|
||||
import styles from '../EntityDetailsUtils/entityDetails.module.scss';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default function K8sBaseDetailsContent<T>({
|
||||
@@ -115,17 +107,6 @@ export default function K8sBaseDetailsContent<T>({
|
||||
|
||||
const effectiveView = hideDetailViewTabs ? VIEW_TYPES.METRICS : selectedView;
|
||||
|
||||
// Wait until the view is a valid tab so we don't log a pending value before
|
||||
// the validation effect above corrects an out-of-scope query param
|
||||
const hasLoggedDefaultTab = useRef(false);
|
||||
useEffect(() => {
|
||||
if (hasLoggedDefaultTab.current || !validTabs.includes(effectiveView)) {
|
||||
return;
|
||||
}
|
||||
hasLoggedDefaultTab.current = true;
|
||||
logInfraDrawerTabViewedEvent(category, effectiveView, true);
|
||||
}, [validTabs, effectiveView, category]);
|
||||
|
||||
const [, setLogFiltersParam] = useInfraMonitoringLogFilters();
|
||||
const [, setTracesFiltersParam] = useInfraMonitoringTracesFilters();
|
||||
const [, setEventsFiltersParam] = useInfraMonitoringEventsFilters();
|
||||
@@ -152,7 +133,6 @@ export default function K8sBaseDetailsContent<T>({
|
||||
category: eventCategory,
|
||||
view: value,
|
||||
});
|
||||
logInfraDrawerTabViewedEvent(category, value, false);
|
||||
};
|
||||
|
||||
const handleExplorePagesRedirect = (): void => {
|
||||
@@ -174,16 +154,6 @@ export default function K8sBaseDetailsContent<T>({
|
||||
view: selectedView,
|
||||
});
|
||||
|
||||
logInfraExplorerNavigatedEvent({
|
||||
entityType: category,
|
||||
destination:
|
||||
selectedView === VIEW_TYPES.LOGS ? 'logs_explorer' : 'traces_explorer',
|
||||
source: 'tab_cta_button',
|
||||
tab: selectedView,
|
||||
sourceKey: null,
|
||||
drawerDurationMsAtNavigation: getDrawerDurationMs(),
|
||||
});
|
||||
|
||||
if (selectedView === VIEW_TYPES.LOGS) {
|
||||
const fullExpression = combineInitialAndUserExpression(
|
||||
logsAndTracesInitialExpression,
|
||||
@@ -239,39 +209,34 @@ export default function K8sBaseDetailsContent<T>({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.entityDetailsEntity}>
|
||||
<div className={styles.entityDetailsGrid}>
|
||||
<div className={styles.labelsRow}>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
{metadataConfig.map((config) => (
|
||||
<Typography.Text
|
||||
key={config.label}
|
||||
color="muted"
|
||||
size="small"
|
||||
weight="medium"
|
||||
className={styles.entityDetailsMetadataLabel}
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
{config.label}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.valuesRow}>
|
||||
<div className="values-row">
|
||||
{metadataConfig.map((config) => {
|
||||
const value = config.getValue(entity);
|
||||
|
||||
if (config.render) {
|
||||
return config.render(value, entity);
|
||||
}
|
||||
|
||||
const displayValue = String(value);
|
||||
return (
|
||||
<Typography.Text
|
||||
key={config.label}
|
||||
size="small"
|
||||
weight="medium"
|
||||
className={styles.entityDetailsMetadataValue}
|
||||
className="entity-details-metadata-value"
|
||||
>
|
||||
{displayValue}
|
||||
{config.render ? (
|
||||
config.render(value, entity)
|
||||
) : (
|
||||
<Tooltip title={displayValue}>{displayValue}</Tooltip>
|
||||
)}
|
||||
</Typography.Text>
|
||||
);
|
||||
})}
|
||||
@@ -288,17 +253,15 @@ export default function K8sBaseDetailsContent<T>({
|
||||
selectedItem={selectedItem}
|
||||
filterExpression={getCountsFilterExpression(entity)}
|
||||
closeDrawer={handleClose}
|
||||
entityType={category}
|
||||
activeTab={selectedView}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!hideDetailViewTabs && (
|
||||
<div className={styles.viewsTabsContainer}>
|
||||
<div className="views-tabs-container">
|
||||
<ToggleGroupSimple
|
||||
type="single"
|
||||
className={styles.viewsTabs}
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
items={[
|
||||
@@ -307,7 +270,7 @@ export default function K8sBaseDetailsContent<T>({
|
||||
{
|
||||
value: VIEW_TYPES.METRICS,
|
||||
label: (
|
||||
<div className={styles.viewTitle}>
|
||||
<div className="view-title">
|
||||
<BarChart size={14} />
|
||||
Metrics
|
||||
</div>
|
||||
@@ -320,7 +283,7 @@ export default function K8sBaseDetailsContent<T>({
|
||||
{
|
||||
value: VIEW_TYPES.LOGS,
|
||||
label: (
|
||||
<div className={styles.viewTitle}>
|
||||
<div className="view-title">
|
||||
<ScrollText size={14} />
|
||||
Logs
|
||||
</div>
|
||||
@@ -333,7 +296,7 @@ export default function K8sBaseDetailsContent<T>({
|
||||
{
|
||||
value: VIEW_TYPES.TRACES,
|
||||
label: (
|
||||
<div className={styles.viewTitle}>
|
||||
<div className="view-title">
|
||||
<DraftingCompass size={14} />
|
||||
Traces
|
||||
</div>
|
||||
@@ -346,7 +309,7 @@ export default function K8sBaseDetailsContent<T>({
|
||||
{
|
||||
value: VIEW_TYPES.EVENTS,
|
||||
label: (
|
||||
<div className={styles.viewTitle}>
|
||||
<div className="view-title">
|
||||
<ChevronsLeftRight size={14} />
|
||||
Events
|
||||
</div>
|
||||
@@ -357,7 +320,7 @@ export default function K8sBaseDetailsContent<T>({
|
||||
...(customTabs?.map((tab) => ({
|
||||
value: tab.key,
|
||||
label: (
|
||||
<div className={styles.viewTitle}>
|
||||
<div className="view-title">
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
</div>
|
||||
@@ -367,30 +330,22 @@ export default function K8sBaseDetailsContent<T>({
|
||||
/>
|
||||
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<TooltipSimple title="Go to Logs Explorer" side="left" arrow>
|
||||
<Tooltip title="Go to Logs Explorer" placement="left">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
className={styles.compassButton}
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
>
|
||||
<Compass size={18} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<TooltipSimple title="Go to Traces Explorer" side="left" arrow>
|
||||
<Tooltip title="Go to Traces Explorer" placement="left">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
className={styles.compassButton}
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
>
|
||||
<Compass size={18} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -403,7 +358,6 @@ export default function K8sBaseDetailsContent<T>({
|
||||
getEntityQueryPayload={getEntityQueryPayload}
|
||||
category={category}
|
||||
queryKey={`${queryKeyPrefix}Metrics`}
|
||||
view={VIEW_TYPES.METRICS}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.LOGS && (
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQuery, useQueryClient } from 'react-query';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import TanStackTable, {
|
||||
SortState,
|
||||
TableColumnDef,
|
||||
useCalculatedPageSize,
|
||||
useHiddenColumnIds,
|
||||
useTableParams,
|
||||
} from 'components/TanStackTableView';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraColumnSortedEvent,
|
||||
logInfraTimeRangeCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useGlobalTimeStore } from 'store/globalTime';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
@@ -32,16 +27,11 @@ import {
|
||||
useInfraMonitoringSelectedItemParams,
|
||||
useInfraMonitoringStatusFilter,
|
||||
} from '../hooks';
|
||||
import {
|
||||
useInfraMonitoringFontSize,
|
||||
useInfraMonitoringLineClamp,
|
||||
} from './useInfraMonitoringTablePreferencesStore';
|
||||
import { useInfraMonitoringLineClamp } from '../components';
|
||||
import { K8sEmptyState } from './K8sEmptyState';
|
||||
import { K8sExpandedRow } from './K8sExpandedRow';
|
||||
import K8sOptionsSidePanel from './K8sOptionsSidePanel';
|
||||
import K8sHeader from './K8sHeader';
|
||||
import { K8sPaginationWarning } from './K8sPaginationWarning';
|
||||
import K8sTableToolbar from './K8sTableToolbar';
|
||||
import { K8sBaseFilters } from './types';
|
||||
import { getGroupedByMeta } from './utils';
|
||||
import { K8sInstrumentationChecksCallout } from './components/K8sInstrumentationChecksCallout/K8sInstrumentationChecksCallout';
|
||||
@@ -114,7 +104,6 @@ export function K8sBaseList<
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const expression = currentQuery.builder.queryData[0]?.filter?.expression || '';
|
||||
const lineClamp = useInfraMonitoringLineClamp();
|
||||
const fontSize = useInfraMonitoringFontSize();
|
||||
const [groupBy] = useInfraMonitoringGroupBy();
|
||||
const [orderBy] = useInfraMonitoringOrderBy();
|
||||
const [statusFilter] = useInfraMonitoringStatusFilter();
|
||||
@@ -124,7 +113,6 @@ export function K8sBaseList<
|
||||
|
||||
const columnStorageKey = `k8s-${entity}-columns`;
|
||||
const hiddenColumnIds = useHiddenColumnIds(columnStorageKey);
|
||||
const [isOptionsDrawerOpen, setIsOptionsDrawerOpen] = useState(false);
|
||||
|
||||
const { containerRef, calculatedPageSize } = useCalculatedPageSize({
|
||||
rowHeight: 42,
|
||||
@@ -229,14 +217,6 @@ export function K8sBaseList<
|
||||
void queryClient.cancelQueries({ queryKey });
|
||||
}, [queryClient, queryKey]);
|
||||
|
||||
const handleOpenOptionsDrawer = useCallback((): void => {
|
||||
setIsOptionsDrawerOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleCloseOptionsDrawer = useCallback((): void => {
|
||||
setIsOptionsDrawerOpen(false);
|
||||
}, []);
|
||||
|
||||
const pageData = data?.data ?? [];
|
||||
const totalCount = data?.total || 0;
|
||||
const hasFilters = !!expression?.trim();
|
||||
@@ -255,14 +235,6 @@ export function K8sBaseList<
|
||||
});
|
||||
}, [eventCategory, totalCount]);
|
||||
|
||||
const prevSelectedTimeRef = useRef(selectedTime);
|
||||
useEffect(() => {
|
||||
if (prevSelectedTimeRef.current !== selectedTime) {
|
||||
logInfraTimeRangeCustomizedEvent(entity, selectedTime);
|
||||
prevSelectedTimeRef.current = selectedTime;
|
||||
}
|
||||
}, [selectedTime, entity]);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(record: T, itemKey: TItemKey): void => {
|
||||
if (groupBy.length === 0) {
|
||||
@@ -374,7 +346,6 @@ export function K8sBaseList<
|
||||
extraQueryKeyParts={extraQueryKeyParts}
|
||||
getRowKey={getRowKey}
|
||||
getItemKey={getItemKey}
|
||||
detailsQueryKeyPrefix={detailsQueryKeyPrefix}
|
||||
/>
|
||||
),
|
||||
[
|
||||
@@ -384,7 +355,6 @@ export function K8sBaseList<
|
||||
getItemKey,
|
||||
expandedRowColumns,
|
||||
extraQueryKeyParts,
|
||||
detailsQueryKeyPrefix,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -393,15 +363,6 @@ export function K8sBaseList<
|
||||
[isGroupedByAttribute],
|
||||
);
|
||||
|
||||
const handleSort = useCallback(
|
||||
(sort: SortState | null): void => {
|
||||
if (sort) {
|
||||
logInfraColumnSortedEvent(entity, sort.columnName, sort.order, 'list');
|
||||
}
|
||||
},
|
||||
[entity],
|
||||
);
|
||||
|
||||
const showTableLoadingState = isLoading;
|
||||
|
||||
const emptyTableMessage: React.ReactNode = renderEmptyState?.({
|
||||
@@ -431,21 +392,17 @@ export function K8sBaseList<
|
||||
<>
|
||||
<K8sHeader
|
||||
controlListPrefix={controlListPrefix}
|
||||
leftFilters={leftFilters}
|
||||
entity={entity}
|
||||
showAutoRefresh={!selectedItem}
|
||||
columns={tableColumns}
|
||||
columnStorageKey={columnStorageKey}
|
||||
isFetching={isFetching}
|
||||
cancelQuery={cancelQuery}
|
||||
/>
|
||||
<div ref={containerRef} className={styles.tableContainer}>
|
||||
<K8sInstrumentationChecksCallout entity={entity} />
|
||||
|
||||
<K8sTableToolbar
|
||||
entity={entity}
|
||||
eventCategory={eventCategory}
|
||||
leftFilters={leftFilters}
|
||||
onOpenOptionsDrawer={handleOpenOptionsDrawer}
|
||||
/>
|
||||
|
||||
{isError && (
|
||||
<Typography>
|
||||
{data?.error?.toString() || 'Something went wrong'}
|
||||
@@ -466,7 +423,6 @@ export function K8sBaseList<
|
||||
getGroupKey={getGroupKeyFn}
|
||||
onRowClick={handleRowClick}
|
||||
onRowClickNewTab={handleRowClickNewTab}
|
||||
onSort={handleSort}
|
||||
renderExpandedRow={isGroupedByAttribute ? renderExpandedRow : undefined}
|
||||
getRowCanExpand={isGroupedByAttribute ? getRowCanExpand : undefined}
|
||||
className={cx(styles.k8SListTable)}
|
||||
@@ -484,21 +440,11 @@ export function K8sBaseList<
|
||||
onLimitChange: setLimit,
|
||||
}}
|
||||
plainTextCellLineClamp={lineClamp}
|
||||
cellTypographySize={fontSize}
|
||||
prefixPaginationContent={paginationWarningContent}
|
||||
paginationClassname={styles.paginationContainer}
|
||||
resetScrollKey={entity}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<K8sOptionsSidePanel
|
||||
open={isOptionsDrawerOpen}
|
||||
columns={tableColumns}
|
||||
storageKey={columnStorageKey}
|
||||
entity={entity}
|
||||
onClose={handleCloseOptionsDrawer}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQuery, useQueryClient } from 'react-query';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Querybuildertypesv5QueryWarnDataDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
@@ -21,7 +21,6 @@ import { useGlobalTimeStore } from 'store/globalTime';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
|
||||
|
||||
import { logInfraColumnSortedEvent } from 'constants/events';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
SelectedItemParams,
|
||||
@@ -31,8 +30,6 @@ import {
|
||||
useInfraMonitoringSelectedItemParams,
|
||||
} from '../hooks';
|
||||
import { K8sBaseFilters } from './types';
|
||||
import { useLogEventForColumnCustomized } from './useLogEventForColumnCustomized';
|
||||
import { useInfraMonitoringFontSize } from './useInfraMonitoringTablePreferencesStore';
|
||||
|
||||
import styles from './K8sExpandedRow.module.scss';
|
||||
import { buildExpressionFromGroupMeta } from './utils';
|
||||
@@ -65,14 +62,9 @@ export type K8sExpandedRowProps<T, TItemKey = string> = {
|
||||
getRowKey?: (record: T) => string;
|
||||
/** Function to get the item key used for selection. Defaults to getRowKey if not provided. */
|
||||
getItemKey?: (record: T) => TItemKey;
|
||||
/** Query key prefix for pre-caching detail data on row click */
|
||||
detailsQueryKeyPrefix?: string;
|
||||
};
|
||||
|
||||
export function K8sExpandedRow<
|
||||
T,
|
||||
TItemKey extends string | SelectedItemParams = string,
|
||||
>({
|
||||
export function K8sExpandedRow<T, TItemKey = string>({
|
||||
rowKey,
|
||||
groupMeta,
|
||||
entity,
|
||||
@@ -81,9 +73,7 @@ export function K8sExpandedRow<
|
||||
extraQueryKeyParts = [],
|
||||
getRowKey,
|
||||
getItemKey,
|
||||
detailsQueryKeyPrefix,
|
||||
}: K8sExpandedRowProps<T, TItemKey>): JSX.Element {
|
||||
const fontSize = useInfraMonitoringFontSize();
|
||||
const [, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
const [, setCurrentPage] = useInfraMonitoringPageListing();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
@@ -94,7 +84,6 @@ export function K8sExpandedRow<
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const urlQuery = useUrlQuery();
|
||||
const location = useLocation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const orderByParamKey = useMemo(
|
||||
() => `orderBy_${rowKey.replace(/[^a-zA-Z0-9]/g, '_')}`,
|
||||
@@ -116,13 +105,6 @@ export function K8sExpandedRow<
|
||||
|
||||
const storageKey = `k8s-${entity}-columns-expanded`;
|
||||
|
||||
useLogEventForColumnCustomized({
|
||||
entity,
|
||||
source: 'expanded',
|
||||
storageKey,
|
||||
columns: tableColumns,
|
||||
});
|
||||
|
||||
const expressionForRecord = useMemo(
|
||||
() => buildExpressionFromGroupMeta(parentExpression || '', groupMeta),
|
||||
[parentExpression, groupMeta],
|
||||
@@ -195,45 +177,18 @@ export function K8sExpandedRow<
|
||||
const expandedData = data?.data ?? [];
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(row: T, itemKey: TItemKey): void => {
|
||||
const params: SelectedItemParams =
|
||||
typeof itemKey === 'object' && itemKey !== null
|
||||
? itemKey
|
||||
: {
|
||||
selectedItem: itemKey,
|
||||
clusterName: null,
|
||||
namespaceName: null,
|
||||
};
|
||||
|
||||
if (detailsQueryKeyPrefix) {
|
||||
const detailQueryKey = getAutoRefreshQueryKey(
|
||||
selectedTime,
|
||||
`${detailsQueryKeyPrefix}EntityDetails`,
|
||||
params.selectedItem,
|
||||
params.clusterName,
|
||||
params.namespaceName,
|
||||
);
|
||||
queryClient.setQueryData(detailQueryKey, { data: row });
|
||||
}
|
||||
|
||||
setSelectedItemParams(params);
|
||||
},
|
||||
[
|
||||
setSelectedItemParams,
|
||||
detailsQueryKeyPrefix,
|
||||
getAutoRefreshQueryKey,
|
||||
selectedTime,
|
||||
queryClient,
|
||||
],
|
||||
);
|
||||
|
||||
const handleSort = useCallback(
|
||||
(sort: SortState | null): void => {
|
||||
if (sort) {
|
||||
logInfraColumnSortedEvent(entity, sort.columnName, sort.order, 'expanded');
|
||||
(_row: T, itemKey: TItemKey): void => {
|
||||
if (typeof itemKey === 'object' && itemKey !== null) {
|
||||
setSelectedItemParams(itemKey as unknown as SelectedItemParams);
|
||||
} else {
|
||||
setSelectedItemParams({
|
||||
selectedItem: itemKey as string,
|
||||
clusterName: null,
|
||||
namespaceName: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
[entity],
|
||||
[setSelectedItemParams],
|
||||
);
|
||||
|
||||
const handleViewAllClick = (): void => {
|
||||
@@ -302,7 +257,6 @@ export function K8sExpandedRow<
|
||||
getRowKey={getRowKey}
|
||||
getItemKey={getItemKey}
|
||||
onRowClick={handleRowClick}
|
||||
onSort={handleSort}
|
||||
enableQueryParams={{
|
||||
orderBy: orderByParamKey,
|
||||
}}
|
||||
@@ -310,7 +264,7 @@ export function K8sExpandedRow<
|
||||
className: styles.expandedTable,
|
||||
}}
|
||||
disableVirtualScroll
|
||||
cellTypographySize={fontSize}
|
||||
cellTypographySize="medium"
|
||||
/>
|
||||
</TanStackTableStateProvider>
|
||||
{!isLoading && expandedData.length > 0 && (
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
.drawer {
|
||||
--dialog-description-padding: 0px 0px var(--spacing-8) 0px;
|
||||
}
|
||||
|
||||
.columnItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.columnsTitle {
|
||||
color: var(--l2-foreground);
|
||||
font-size: var(--periscope-font-size-small, 11px);
|
||||
font-weight: var(--periscope-font-weight-medium, 500);
|
||||
text-transform: uppercase;
|
||||
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border-bottom: 1px solid var(--l2-border);
|
||||
|
||||
&:not(:first-of-type) {
|
||||
border-top: 1px solid var(--l2-border);
|
||||
}
|
||||
}
|
||||
|
||||
.columnsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
[data-slot='icon'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-slot='entity-group-header'] {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
[data-slot='column-header'] {
|
||||
display: flex;
|
||||
width: auto;
|
||||
|
||||
span {
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
br {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.columnItem {
|
||||
justify-content: flex-start !important;
|
||||
width: 100%;
|
||||
|
||||
> span {
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
.horizontalDivider {
|
||||
border-top: 1px solid var(--l1-border);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DrawerWrapper } from '@signozhq/ui/drawer';
|
||||
import {
|
||||
hideColumn,
|
||||
showColumn,
|
||||
TableColumnDef,
|
||||
useHiddenColumnIds,
|
||||
} from 'components/TanStackTableView';
|
||||
|
||||
import styles from './K8sFiltersSidePanel.module.scss';
|
||||
|
||||
type ColumnPickerItem = {
|
||||
id: string;
|
||||
label: ReactNode;
|
||||
canBeHidden: boolean;
|
||||
visibilityBehavior:
|
||||
| 'hidden-on-expand'
|
||||
| 'hidden-on-collapse'
|
||||
| 'always-visible';
|
||||
};
|
||||
|
||||
function renderHeader(header: string | (() => ReactNode)): ReactNode {
|
||||
return typeof header === 'function' ? header() : header;
|
||||
}
|
||||
|
||||
function toColumnPickerItems<T>(
|
||||
columns: TableColumnDef<T>[],
|
||||
): ColumnPickerItem[] {
|
||||
return columns.map((col) => ({
|
||||
id: col.id,
|
||||
label: renderHeader(col.header),
|
||||
canBeHidden: col.canBeHidden !== false && col.enableRemove !== false,
|
||||
visibilityBehavior: col.visibilityBehavior ?? 'always-visible',
|
||||
}));
|
||||
}
|
||||
|
||||
function K8sFiltersSidePanel<TData>({
|
||||
open,
|
||||
onClose,
|
||||
columns,
|
||||
storageKey,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
columns: TableColumnDef<TData>[];
|
||||
storageKey: string;
|
||||
}): JSX.Element {
|
||||
const columnPickerItems = useMemo(
|
||||
() => toColumnPickerItems(columns),
|
||||
[columns],
|
||||
);
|
||||
const hiddenColumnIds = useHiddenColumnIds(storageKey);
|
||||
|
||||
const addedColumns = useMemo(
|
||||
() =>
|
||||
columnPickerItems.filter(
|
||||
(column) =>
|
||||
!hiddenColumnIds.includes(column.id) &&
|
||||
column.visibilityBehavior !== 'hidden-on-collapse',
|
||||
),
|
||||
[columnPickerItems, hiddenColumnIds],
|
||||
);
|
||||
|
||||
const hiddenColumns = useMemo(
|
||||
() =>
|
||||
columnPickerItems.filter((column) => hiddenColumnIds.includes(column.id)),
|
||||
[columnPickerItems, hiddenColumnIds],
|
||||
);
|
||||
|
||||
const handleRemoveColumn = (columnId: string): void => {
|
||||
hideColumn(storageKey, columnId);
|
||||
};
|
||||
|
||||
const handleAddColumn = (columnId: string): void => {
|
||||
showColumn(storageKey, columnId);
|
||||
};
|
||||
|
||||
const drawerContent = (
|
||||
<>
|
||||
<div className={styles.columnsTitle}>Added Columns (Click to remove)</div>
|
||||
|
||||
<div className={styles.columnsList}>
|
||||
{addedColumns.map((column) => (
|
||||
<div className={styles.columnItem} key={column.id}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="none"
|
||||
className={styles.columnItem}
|
||||
disabled={!column.canBeHidden}
|
||||
data-testid={`remove-column-${column.id}`}
|
||||
onClick={(): void => handleRemoveColumn(column.id)}
|
||||
>
|
||||
{column.label}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.horizontalDivider} />
|
||||
|
||||
<div className={styles.columnsTitle}>Other Columns (Click to add)</div>
|
||||
|
||||
<div className={styles.columnsList}>
|
||||
{hiddenColumns.map((column) => (
|
||||
<div className={styles.columnItem} key={column.id}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="none"
|
||||
className={styles.columnItem}
|
||||
data-can-be-added="true"
|
||||
data-testid={`add-column-${column.id}`}
|
||||
onClick={(): void => handleAddColumn(column.id)}
|
||||
tabIndex={0}
|
||||
>
|
||||
{column.label}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open={open}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
title="Columns"
|
||||
direction="right"
|
||||
showCloseButton
|
||||
showOverlay={false}
|
||||
className={styles.drawer}
|
||||
>
|
||||
{drawerContent}
|
||||
</DrawerWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sFiltersSidePanel;
|
||||
@@ -5,10 +5,25 @@
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-4);
|
||||
width: 100%;
|
||||
|
||||
:global(.ant-select-selector) {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l2-border) !important;
|
||||
background-color: var(--l2-background) !important;
|
||||
|
||||
input {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:global([data-slot='badge'] .ant-typography) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.k8SListControlsRow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-4);
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
@@ -19,6 +34,27 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.k8SFiltersGroupByRow {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-4);
|
||||
min-width: 0;
|
||||
|
||||
@media (max-width: 1600px) {
|
||||
flex: 1 1 100%;
|
||||
order: 9;
|
||||
}
|
||||
}
|
||||
|
||||
.k8SAttributeSearchContainer {
|
||||
flex: 1 1 240px;
|
||||
max-width: 400px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.k8SRunButton {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
@@ -28,12 +64,17 @@
|
||||
}
|
||||
|
||||
.k8SDateTimeSelection {
|
||||
margin-left: auto;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
--date-time-selector-border-color: var(--l2-border);
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
flex: 1 1 100%;
|
||||
order: 10;
|
||||
}
|
||||
|
||||
:global(.timeSelection-input) {
|
||||
height: 32px;
|
||||
}
|
||||
@@ -54,3 +95,42 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.groupByLabel {
|
||||
min-width: max-content;
|
||||
font-size: var(--periscope-font-size-base, 13px);
|
||||
font-weight: var(--periscope-font-weight-regular, 400);
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
border-radius: 2px 0px 0px 2px;
|
||||
border: 1px solid var(--l2-border);
|
||||
border-right: none;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
|
||||
display: flex;
|
||||
height: 32px;
|
||||
padding: var(--spacing-3) var(--spacing-3) var(--spacing-3) var(--spacing-4);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.groupBySelect {
|
||||
width: 100%;
|
||||
|
||||
:global(.ant-select-selector) {
|
||||
border-left: none;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.k8SListControlsRight {
|
||||
min-width: 240px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Select } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
|
||||
import { useGetFieldsKeys } from 'api/generated/services/fields';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraFilterCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
TelemetrytypesFieldContextDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { TableColumnDef } from 'components/TanStackTableView';
|
||||
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { SlidersHorizontal } from '@signozhq/icons';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useGlobalTimeQueryInvalidate } from 'store/globalTime';
|
||||
import {
|
||||
useGlobalTimeQueryInvalidate,
|
||||
useGlobalTimeStore,
|
||||
} from 'store/globalTime';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
@@ -21,25 +31,37 @@ import {
|
||||
InfraMonitoringEntity,
|
||||
METRIC_NAMESPACE_BY_ENTITY,
|
||||
} from '../constants';
|
||||
import { useInfraMonitoringPageListing } from '../hooks';
|
||||
import {
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringPageListing,
|
||||
} from '../hooks';
|
||||
import K8sFiltersSidePanel from './K8sFiltersSidePanel';
|
||||
|
||||
import styles from './K8sHeader.module.scss';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
|
||||
interface K8sHeaderProps {
|
||||
interface K8sHeaderProps<TData> {
|
||||
controlListPrefix?: React.ReactNode;
|
||||
leftFilters?: React.ReactNode;
|
||||
entity: InfraMonitoringEntity;
|
||||
showAutoRefresh: boolean;
|
||||
columns: TableColumnDef<TData>[];
|
||||
columnStorageKey: string;
|
||||
isFetching?: boolean;
|
||||
cancelQuery: () => void;
|
||||
}
|
||||
|
||||
function K8sHeader({
|
||||
function K8sHeader<TData>({
|
||||
controlListPrefix,
|
||||
leftFilters,
|
||||
entity,
|
||||
showAutoRefresh,
|
||||
columns,
|
||||
columnStorageKey,
|
||||
isFetching = false,
|
||||
cancelQuery,
|
||||
}: K8sHeaderProps): JSX.Element {
|
||||
}: K8sHeaderProps<TData>): JSX.Element {
|
||||
const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false);
|
||||
// null = user never touched the search box; fall back to the current
|
||||
// query expression on Run so an untouched search doesn't wipe filters
|
||||
const stagedExpressionRef = useRef<string | null>(null);
|
||||
@@ -99,8 +121,6 @@ function K8sHeader({
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: entity,
|
||||
});
|
||||
|
||||
logInfraFilterCustomizedEvent(entity, 'search', finalExpression);
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -125,11 +145,101 @@ function K8sHeader({
|
||||
cancelQuery();
|
||||
}, [cancelQuery]);
|
||||
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
const startUnixMilli = Math.floor(minTime / NANO_SECOND_MULTIPLIER);
|
||||
const endUnixMilli = Math.floor(maxTime / NANO_SECOND_MULTIPLIER);
|
||||
|
||||
const { data: groupByFiltersData, isLoading: isLoadingGroupByFilters } =
|
||||
useGetFieldsKeys(
|
||||
{
|
||||
signal: TelemetrytypesSignalDTO.metrics,
|
||||
metricNamespace: METRIC_NAMESPACE_BY_ENTITY[entity],
|
||||
limit: 100,
|
||||
startUnixMilli,
|
||||
endUnixMilli,
|
||||
fieldContext: TelemetrytypesFieldContextDTO.resource,
|
||||
// the search text is intentionally not included
|
||||
// searchText: expression,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
queryKey: ['getFieldsKeys', entity, startUnixMilli, endUnixMilli],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const flatFieldKeys = useMemo(() => {
|
||||
const keys = groupByFiltersData?.data?.keys;
|
||||
if (!keys) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allKeys = Object.values(keys).flat();
|
||||
const seen = new Set<string>();
|
||||
|
||||
return allKeys.filter((field) => {
|
||||
if (seen.has(field.name)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(field.name);
|
||||
return true;
|
||||
});
|
||||
}, [groupByFiltersData]);
|
||||
|
||||
const groupByOptions = useMemo(
|
||||
() =>
|
||||
flatFieldKeys.map((field) => ({
|
||||
value: field.name,
|
||||
label: field.name,
|
||||
})),
|
||||
[flatFieldKeys],
|
||||
);
|
||||
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(value: string[]) => {
|
||||
void setCurrentPage(1);
|
||||
void setGroupBy(value);
|
||||
|
||||
void logEvent(InfraMonitoringEvents.GroupByChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: InfraMonitoringEvents.Pod,
|
||||
});
|
||||
},
|
||||
[setCurrentPage, setGroupBy],
|
||||
);
|
||||
|
||||
const onClickOutside = useCallback(() => {
|
||||
setIsFiltersSidePanelOpen(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.k8SListControls}>
|
||||
<div className={styles.k8SListControlsRow}>
|
||||
{controlListPrefix}
|
||||
|
||||
<div className={styles.k8SFiltersGroupByRow}>
|
||||
{leftFilters}
|
||||
|
||||
<div className={styles.k8SAttributeSearchContainer}>
|
||||
<div className={styles.groupByLabel}>Group by</div>
|
||||
<Select
|
||||
className={styles.groupBySelect}
|
||||
loading={isLoadingGroupByFilters}
|
||||
mode="multiple"
|
||||
value={groupBy}
|
||||
allowClear
|
||||
maxTagCount="responsive"
|
||||
placeholder="Search for attribute"
|
||||
options={groupByOptions}
|
||||
onChange={handleGroupByChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.k8SDateTimeSelection}>
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh={showAutoRefresh}
|
||||
@@ -145,6 +255,20 @@ function K8sHeader({
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
className={styles.k8SRunButton}
|
||||
/>
|
||||
|
||||
<TooltipSimple title="Click to add more columns to this table">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
data-testid="k8s-list-filters-button"
|
||||
onClick={(): void => setIsFiltersSidePanelOpen(true)}
|
||||
className={styles.k8SFiltersButton}
|
||||
>
|
||||
<SlidersHorizontal size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
|
||||
<div className={styles.k8SQbSearchContainer}>
|
||||
@@ -159,6 +283,13 @@ function K8sHeader({
|
||||
metricNamespace={METRIC_NAMESPACE_BY_ENTITY[entity]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<K8sFiltersSidePanel
|
||||
open={isFiltersSidePanelOpen}
|
||||
columns={columns}
|
||||
storageKey={columnStorageKey}
|
||||
onClose={onClickOutside}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
.drawer {
|
||||
// for some reason, the scrollbar is overriding this
|
||||
// for now, I will keep forcing but needs to investigate a global level
|
||||
// why all drawers are being override with l1-background
|
||||
background-color: var(--l2-background) !important;
|
||||
border-color: var(--l2-border) !important;
|
||||
--dialog-description-padding: 0px 0px var(--spacing-8) 0px;
|
||||
|
||||
[data-slot='drawer-description'] {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border-bottom: 1px solid var(--l2-border);
|
||||
|
||||
&:not(:first-of-type) {
|
||||
border-top: 1px solid var(--l2-border);
|
||||
}
|
||||
}
|
||||
|
||||
.sectionTitleText {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.fontSizeContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--spacing-4) 0;
|
||||
}
|
||||
|
||||
.fontSizeOption {
|
||||
width: 100%;
|
||||
--button-display: flex;
|
||||
--button-justify-content: space-between;
|
||||
--button-align-items: center;
|
||||
--button-padding: var(--spacing-4) var(--spacing-8);
|
||||
}
|
||||
|
||||
.fontSizeLabel {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.checkIcon {
|
||||
color: var(--accent-foreground);
|
||||
}
|
||||
|
||||
.lineClampContainer {
|
||||
padding: 12px;
|
||||
|
||||
--input-prefix-padding: var(--spacing-3);
|
||||
--input-suffix-padding: var(--spacing-3);
|
||||
}
|
||||
|
||||
.columnsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
[data-slot='icon'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-slot='entity-group-header'] {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
[data-slot='column-header'] {
|
||||
display: flex;
|
||||
width: auto;
|
||||
|
||||
span {
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
br {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.columnItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-2) var(--spacing-8);
|
||||
|
||||
> span {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
padding-top: var(--spacing-8);
|
||||
}
|
||||
}
|
||||
|
||||
.columnLabel {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.horizontalDivider {
|
||||
border-top: 1px solid var(--l1-border);
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
import { ChangeEvent, ReactNode, useCallback, useMemo } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DrawerWrapper } from '@signozhq/ui/drawer';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Check, Minus, Plus } from '@signozhq/icons';
|
||||
import {
|
||||
hideColumn,
|
||||
showColumn,
|
||||
TableColumnDef,
|
||||
useColumnOrder,
|
||||
useHiddenColumnIds,
|
||||
} from 'components/TanStackTableView';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
|
||||
import {
|
||||
FontSize,
|
||||
useInfraMonitoringTablePreferencesStore,
|
||||
} from './useInfraMonitoringTablePreferencesStore';
|
||||
import { useLogEventForColumnCustomized } from './useLogEventForColumnCustomized';
|
||||
import { sortByColumnOrder } from './utils';
|
||||
|
||||
import styles from './K8sOptionsSidePanel.module.scss';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
|
||||
type ColumnPickerItem = {
|
||||
id: string;
|
||||
label: ReactNode;
|
||||
canBeHidden: boolean;
|
||||
visibilityBehavior:
|
||||
| 'hidden-on-expand'
|
||||
| 'hidden-on-collapse'
|
||||
| 'always-visible';
|
||||
};
|
||||
|
||||
function renderHeader(header: string | (() => ReactNode)): ReactNode {
|
||||
return typeof header === 'function' ? header() : header;
|
||||
}
|
||||
|
||||
function toColumnPickerItems<T>(
|
||||
columns: TableColumnDef<T>[],
|
||||
): ColumnPickerItem[] {
|
||||
return columns.map((col) => ({
|
||||
id: col.id,
|
||||
label: renderHeader(col.header),
|
||||
canBeHidden: col.canBeHidden !== false && col.enableRemove !== false,
|
||||
visibilityBehavior: col.visibilityBehavior ?? 'always-visible',
|
||||
}));
|
||||
}
|
||||
|
||||
const FONT_SIZE_OPTIONS: { value: FontSize; label: string }[] = [
|
||||
{ value: 'small', label: 'Small' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'large', label: 'Large' },
|
||||
];
|
||||
|
||||
function K8sOptionsSidePanel<TData>({
|
||||
open,
|
||||
onClose,
|
||||
columns,
|
||||
storageKey,
|
||||
entity,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
columns: TableColumnDef<TData>[];
|
||||
storageKey: string;
|
||||
entity: InfraMonitoringEntity;
|
||||
}): JSX.Element {
|
||||
const columnPickerItems = useMemo(
|
||||
() => toColumnPickerItems(columns),
|
||||
[columns],
|
||||
);
|
||||
const hiddenColumnIds = useHiddenColumnIds(storageKey);
|
||||
const columnOrder = useColumnOrder(storageKey);
|
||||
|
||||
const lineClamp = useInfraMonitoringTablePreferencesStore((s) => s.lineClamp);
|
||||
const fontSize = useInfraMonitoringTablePreferencesStore((s) => s.fontSize);
|
||||
const increaseLineClamp = useInfraMonitoringTablePreferencesStore(
|
||||
(s) => s.increaseLineClamp,
|
||||
);
|
||||
const decreaseLineClamp = useInfraMonitoringTablePreferencesStore(
|
||||
(s) => s.decreaseLineClamp,
|
||||
);
|
||||
const setLineClamp = useInfraMonitoringTablePreferencesStore(
|
||||
(s) => s.setLineClamp,
|
||||
);
|
||||
const setFontSize = useInfraMonitoringTablePreferencesStore(
|
||||
(s) => s.setFontSize,
|
||||
);
|
||||
|
||||
const visibleColumnItems = useMemo(
|
||||
() =>
|
||||
columnPickerItems.filter(
|
||||
(column) => column.visibilityBehavior !== 'hidden-on-collapse',
|
||||
),
|
||||
[columnPickerItems],
|
||||
);
|
||||
|
||||
const orderedVisibleColumnItems = useMemo(
|
||||
() => sortByColumnOrder(visibleColumnItems, (col) => col.id, columnOrder),
|
||||
[visibleColumnItems, columnOrder],
|
||||
);
|
||||
|
||||
useLogEventForColumnCustomized({
|
||||
entity,
|
||||
source: 'list',
|
||||
storageKey,
|
||||
columns,
|
||||
});
|
||||
|
||||
const handleToggleColumn = (columnId: string, checked: boolean): void => {
|
||||
if (checked) {
|
||||
showColumn(storageKey, columnId);
|
||||
} else {
|
||||
hideColumn(storageKey, columnId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLineClampChange = useCallback(
|
||||
(value: ChangeEvent<HTMLInputElement>): void => {
|
||||
const valueAsNumber = value.target.valueAsNumber;
|
||||
|
||||
if (value && Number.isInteger(valueAsNumber)) {
|
||||
setLineClamp(valueAsNumber);
|
||||
}
|
||||
},
|
||||
[setLineClamp],
|
||||
);
|
||||
|
||||
const drawerContent = (
|
||||
<>
|
||||
<div className={styles.sectionTitle}>
|
||||
<Typography.Text size="sm" className={styles.sectionTitleText}>
|
||||
Font Size
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className={styles.fontSizeContainer}>
|
||||
{FONT_SIZE_OPTIONS.map((option) => (
|
||||
<Button
|
||||
key={option.value}
|
||||
variant="ghost"
|
||||
color="none"
|
||||
className={styles.fontSizeOption}
|
||||
data-testid={`font-size-${option.value}`}
|
||||
onClick={(): void => setFontSize(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
{fontSize === option.value && (
|
||||
<Check size={14} className={styles.checkIcon} />
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.horizontalDivider} />
|
||||
|
||||
<div className={styles.sectionTitle}>
|
||||
<Typography.Text size="sm" className={styles.sectionTitleText}>
|
||||
Max lines per row
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className={styles.lineClampContainer}>
|
||||
<Input
|
||||
min={1}
|
||||
max={10}
|
||||
value={lineClamp}
|
||||
onChange={handleLineClampChange}
|
||||
data-testid="line-clamp-input"
|
||||
type="number"
|
||||
prefix={
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="sm"
|
||||
className={styles.lineClampButton}
|
||||
data-testid="line-clamp-decrease"
|
||||
onClick={decreaseLineClamp}
|
||||
prefix={<Minus />}
|
||||
disabled={lineClamp <= 1}
|
||||
/>
|
||||
}
|
||||
suffix={
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="sm"
|
||||
className={styles.lineClampButton}
|
||||
data-testid="line-clamp-increase"
|
||||
onClick={increaseLineClamp}
|
||||
prefix={<Plus />}
|
||||
disabled={lineClamp >= 10}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.horizontalDivider} />
|
||||
|
||||
<div className={styles.sectionTitle}>
|
||||
<Typography.Text size="sm" className={styles.sectionTitleText}>
|
||||
Columns
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className={styles.columnsList}>
|
||||
{orderedVisibleColumnItems.map((column) => {
|
||||
const isVisible = !hiddenColumnIds.includes(column.id);
|
||||
const switchElement = (
|
||||
<Switch
|
||||
value={isVisible}
|
||||
disabled={!column.canBeHidden}
|
||||
data-testid={`toggle-column-${column.id}`}
|
||||
onChange={(checked): void => handleToggleColumn(column.id, checked)}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div className={styles.columnItem} key={column.id}>
|
||||
<Typography.Text size="sm" className={styles.columnLabel}>
|
||||
{column.label}
|
||||
</Typography.Text>
|
||||
{column.canBeHidden ? (
|
||||
switchElement
|
||||
) : (
|
||||
<TooltipSimple title="Required column cannot be hidden" arrow>
|
||||
{switchElement}
|
||||
</TooltipSimple>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open={open}
|
||||
onOpenChange={(isOpen): void => {
|
||||
if (!isOpen) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
title="Options"
|
||||
direction="right"
|
||||
width="narrow"
|
||||
showCloseButton
|
||||
showOverlay={false}
|
||||
className={styles.drawer}
|
||||
>
|
||||
{drawerContent}
|
||||
</DrawerWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sOptionsSidePanel;
|
||||
@@ -1,65 +0,0 @@
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
padding: 0px var(--spacing-4) var(--spacing-4) var(--spacing-4);
|
||||
background: var(--l1-background);
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.groupByContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
|
||||
:global(.ant-select-selector) {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l2-border) !important;
|
||||
background-color: var(--l2-background) !important;
|
||||
|
||||
input {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:global([data-slot='badge'] .ant-typography) {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.groupByLabel {
|
||||
min-width: max-content;
|
||||
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
|
||||
border: 1px solid var(--l2-border);
|
||||
border-right: none;
|
||||
|
||||
display: flex;
|
||||
height: 32px;
|
||||
padding: var(--spacing-3) var(--spacing-3) var(--spacing-3) var(--spacing-4);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.groupBySelect {
|
||||
width: 100%;
|
||||
|
||||
:global(.ant-select-selector) {
|
||||
border-left: none;
|
||||
border-top-left-radius: 0px !important;
|
||||
border-bottom-left-radius: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.toolbarButton {
|
||||
flex-shrink: 0;
|
||||
--button-padding: 0px;
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Select } from 'antd';
|
||||
import { Download, SlidersVertical } from '@signozhq/icons';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraGroupByCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import {
|
||||
useInfraMonitoringGroupBy,
|
||||
useInfraMonitoringPageListing,
|
||||
} from '../hooks';
|
||||
import { useInfraMonitoringGroupByData } from './useInfraMonitoringGroupByData';
|
||||
|
||||
import styles from './K8sTableToolbar.module.scss';
|
||||
|
||||
interface K8sTableToolbarProps {
|
||||
entity: InfraMonitoringEntity;
|
||||
eventCategory: InfraMonitoringEvents;
|
||||
leftFilters?: React.ReactNode;
|
||||
onOpenOptionsDrawer: () => void;
|
||||
onDownload?: () => void;
|
||||
}
|
||||
|
||||
function K8sTableToolbar({
|
||||
entity,
|
||||
eventCategory,
|
||||
leftFilters,
|
||||
onOpenOptionsDrawer,
|
||||
onDownload,
|
||||
}: K8sTableToolbarProps): JSX.Element {
|
||||
const { groupByOptions, isLoading: isLoadingGroupByFilters } =
|
||||
useInfraMonitoringGroupByData(entity);
|
||||
|
||||
const [groupBy, setGroupBy] = useInfraMonitoringGroupBy();
|
||||
const [, setCurrentPage] = useInfraMonitoringPageListing();
|
||||
|
||||
const handleGroupByChange = useCallback(
|
||||
(value: string[]) => {
|
||||
void setCurrentPage(1);
|
||||
void setGroupBy(value);
|
||||
|
||||
void logEvent(InfraMonitoringEvents.GroupByChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.ListPage,
|
||||
category: eventCategory,
|
||||
});
|
||||
|
||||
logInfraGroupByCustomizedEvent(entity, value);
|
||||
},
|
||||
[entity, eventCategory, setCurrentPage, setGroupBy],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.toolbar}>
|
||||
<div className={styles.groupByContainer}>
|
||||
<div className={styles.groupByLabel}>Group by</div>
|
||||
<Select
|
||||
className={styles.groupBySelect}
|
||||
loading={isLoadingGroupByFilters}
|
||||
mode="multiple"
|
||||
value={groupBy}
|
||||
allowClear
|
||||
maxTagCount="responsive"
|
||||
placeholder="Search for attribute"
|
||||
options={groupByOptions}
|
||||
onChange={handleGroupByChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.spacer} />
|
||||
|
||||
{leftFilters}
|
||||
|
||||
{onDownload && (
|
||||
<TooltipSimple title="Download">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
data-testid="k8s-table-download-button"
|
||||
onClick={onDownload}
|
||||
className={styles.toolbarButton}
|
||||
>
|
||||
<Download size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
|
||||
<TooltipSimple title="Options">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
color="secondary"
|
||||
data-testid="k8s-table-options-button"
|
||||
onClick={onOpenOptionsDrawer}
|
||||
className={styles.toolbarButton}
|
||||
>
|
||||
<SlidersVertical size={14} />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default K8sTableToolbar;
|
||||
@@ -1,187 +0,0 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { TableColumnDef, useColumnStore } from 'components/TanStackTableView';
|
||||
import { logInfraColumnCustomizedEvent } from 'constants/events';
|
||||
|
||||
import { InfraMonitoringEntity } from '../../constants';
|
||||
import { useInfraMonitoringTablePreferencesStore } from '../useInfraMonitoringTablePreferencesStore';
|
||||
import { useLogEventForColumnCustomized } from '../useLogEventForColumnCustomized';
|
||||
|
||||
jest.mock('constants/events', () => ({
|
||||
logInfraColumnCustomizedEvent: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockLogEvent = logInfraColumnCustomizedEvent as jest.Mock;
|
||||
|
||||
type TestRow = { id: string; name: string };
|
||||
|
||||
const col = (id: string): TableColumnDef<TestRow> => ({
|
||||
id,
|
||||
header: id,
|
||||
cell: (): null => null,
|
||||
});
|
||||
|
||||
const columns = [col('a'), col('b'), col('c')];
|
||||
|
||||
describe('useLogEventForColumnCustomized', () => {
|
||||
beforeEach(() => {
|
||||
useColumnStore.setState({ tables: {} });
|
||||
useInfraMonitoringTablePreferencesStore.setState({
|
||||
lineClamp: 1,
|
||||
fontSize: 'medium',
|
||||
});
|
||||
localStorage.clear();
|
||||
mockLogEvent.mockClear();
|
||||
});
|
||||
|
||||
it('does not emit on mount', () => {
|
||||
const storageKey = 'test-no-emit-on-mount';
|
||||
act(() => {
|
||||
useColumnStore.getState().initializeFromDefaults(storageKey, columns);
|
||||
});
|
||||
|
||||
renderHook(() =>
|
||||
useLogEventForColumnCustomized({
|
||||
entity: InfraMonitoringEntity.PODS,
|
||||
source: 'list',
|
||||
storageKey,
|
||||
columns,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockLogEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('emits when a column is hidden after mount', () => {
|
||||
const storageKey = 'test-emit-on-hide';
|
||||
act(() => {
|
||||
useColumnStore.getState().initializeFromDefaults(storageKey, columns);
|
||||
});
|
||||
|
||||
renderHook(() =>
|
||||
useLogEventForColumnCustomized({
|
||||
entity: InfraMonitoringEntity.PODS,
|
||||
source: 'list',
|
||||
storageKey,
|
||||
columns,
|
||||
}),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
useColumnStore.getState().hideColumn(storageKey, 'b');
|
||||
});
|
||||
|
||||
expect(mockLogEvent).toHaveBeenCalledTimes(1);
|
||||
expect(mockLogEvent).toHaveBeenCalledWith(
|
||||
InfraMonitoringEntity.PODS,
|
||||
['a', 'c'],
|
||||
'medium',
|
||||
1,
|
||||
'list',
|
||||
);
|
||||
});
|
||||
|
||||
it('emits when font size changes after mount', () => {
|
||||
const storageKey = 'test-emit-on-font-size';
|
||||
act(() => {
|
||||
useColumnStore.getState().initializeFromDefaults(storageKey, columns);
|
||||
});
|
||||
|
||||
renderHook(() =>
|
||||
useLogEventForColumnCustomized({
|
||||
entity: InfraMonitoringEntity.PODS,
|
||||
source: 'list',
|
||||
storageKey,
|
||||
columns,
|
||||
}),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
useInfraMonitoringTablePreferencesStore.getState().setFontSize('small');
|
||||
});
|
||||
|
||||
expect(mockLogEvent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Regression: K8sDynamicList keeps K8sBaseList (and the options side panel)
|
||||
// mounted across category switches, so this hook survives a Pods -> Nodes
|
||||
// switch with every input changed. The switch itself is not a customization
|
||||
// and must not emit.
|
||||
it('does not emit when the storage key changes (category switch)', () => {
|
||||
const podsKey = 'test-switch-pods';
|
||||
const nodesKey = 'test-switch-nodes';
|
||||
const nodeColumns = [col('x'), col('y')];
|
||||
act(() => {
|
||||
useColumnStore.getState().initializeFromDefaults(podsKey, columns);
|
||||
useColumnStore.getState().initializeFromDefaults(nodesKey, nodeColumns);
|
||||
});
|
||||
|
||||
const { rerender } = renderHook(
|
||||
(props) => useLogEventForColumnCustomized(props),
|
||||
{
|
||||
initialProps: {
|
||||
entity: InfraMonitoringEntity.PODS,
|
||||
source: 'list' as const,
|
||||
storageKey: podsKey,
|
||||
columns,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
rerender({
|
||||
entity: InfraMonitoringEntity.NODES,
|
||||
source: 'list' as const,
|
||||
storageKey: nodesKey,
|
||||
columns: nodeColumns,
|
||||
});
|
||||
|
||||
expect(mockLogEvent).not.toHaveBeenCalled();
|
||||
|
||||
// A real customization after the switch still emits, for the new entity
|
||||
act(() => {
|
||||
useColumnStore.getState().hideColumn(nodesKey, 'y');
|
||||
});
|
||||
|
||||
expect(mockLogEvent).toHaveBeenCalledTimes(1);
|
||||
expect(mockLogEvent).toHaveBeenCalledWith(
|
||||
InfraMonitoringEntity.NODES,
|
||||
['x'],
|
||||
'medium',
|
||||
1,
|
||||
'list',
|
||||
);
|
||||
});
|
||||
|
||||
// Regression: every expanded group row mounts its own instance sharing the
|
||||
// `k8s-<entity>-columns-expanded` storage key; one customization must not
|
||||
// emit once per row.
|
||||
it('emits once when multiple instances share a storage key', () => {
|
||||
const storageKey = 'test-shared-key';
|
||||
act(() => {
|
||||
useColumnStore.getState().initializeFromDefaults(storageKey, columns);
|
||||
});
|
||||
|
||||
const params = {
|
||||
entity: InfraMonitoringEntity.PODS,
|
||||
source: 'expanded' as const,
|
||||
storageKey,
|
||||
columns,
|
||||
};
|
||||
renderHook(() => useLogEventForColumnCustomized(params));
|
||||
renderHook(() => useLogEventForColumnCustomized(params));
|
||||
renderHook(() => useLogEventForColumnCustomized(params));
|
||||
|
||||
act(() => {
|
||||
useColumnStore.getState().hideColumn(storageKey, 'a');
|
||||
});
|
||||
|
||||
expect(mockLogEvent).toHaveBeenCalledTimes(1);
|
||||
|
||||
// A subsequent distinct customization emits again, exactly once
|
||||
act(() => {
|
||||
useColumnStore.getState().hideColumn(storageKey, 'b');
|
||||
});
|
||||
|
||||
expect(mockLogEvent).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Tooltip } from 'antd';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Compass } from '@signozhq/icons';
|
||||
import { TextNoData } from '../../../components/TextNoData';
|
||||
import { logInfraExplorerNavigatedEvent } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -15,7 +13,6 @@ import {
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS,
|
||||
InfraMonitoringEntity,
|
||||
} from '../../../constants';
|
||||
import { getDrawerDurationMs } from '../../useDrawerLifecycleStore';
|
||||
import styles from './EntityCountsSection.module.scss';
|
||||
|
||||
export interface EntityCountConfig<T> {
|
||||
@@ -30,8 +27,6 @@ interface EntityCountsSectionProps<T> {
|
||||
selectedItem: string;
|
||||
filterExpression: string;
|
||||
closeDrawer: () => void;
|
||||
entityType: InfraMonitoringEntity;
|
||||
activeTab: string;
|
||||
}
|
||||
|
||||
export function EntityCountsSection<T>({
|
||||
@@ -40,21 +35,7 @@ export function EntityCountsSection<T>({
|
||||
selectedItem,
|
||||
filterExpression,
|
||||
closeDrawer,
|
||||
entityType,
|
||||
activeTab,
|
||||
}: EntityCountsSectionProps<T>): JSX.Element {
|
||||
const handleCardNavigate = (cardLabel: string): void => {
|
||||
logInfraExplorerNavigatedEvent({
|
||||
entityType,
|
||||
destination: 'k8s_list',
|
||||
source: 'stats_card',
|
||||
tab: activeTab,
|
||||
sourceKey: cardLabel,
|
||||
drawerDurationMsAtNavigation: getDrawerDurationMs(),
|
||||
});
|
||||
closeDrawer();
|
||||
};
|
||||
|
||||
const buildNavigationUrl = (targetCategory: InfraMonitoringEntity): string => {
|
||||
const defaultQuery = initialQueriesMap[DataSource.METRICS];
|
||||
|
||||
@@ -135,26 +116,17 @@ export function EntityCountsSection<T>({
|
||||
>
|
||||
{config.label}
|
||||
</Typography.Text>
|
||||
{config.getValue(entity) ? (
|
||||
<Typography.Text
|
||||
className={styles.countValue}
|
||||
size="xl"
|
||||
weight="semibold"
|
||||
>
|
||||
{config.getValue(entity)}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<TextNoData type="typography" className={styles.countValue} />
|
||||
)}
|
||||
<Typography.Text className={styles.countValue} size="xl" weight="semibold">
|
||||
{config.getValue(entity) || '-'}
|
||||
</Typography.Text>
|
||||
<Link
|
||||
to={buildNavigationUrl(config.targetCategory)}
|
||||
onClick={(): void => handleCardNavigate(config.label)}
|
||||
onClick={closeDrawer}
|
||||
data-testid={`navigate-${config.label.toLowerCase().replace(/\s+/g, '-')}`}
|
||||
>
|
||||
<TooltipSimple
|
||||
<Tooltip
|
||||
title={`View ${config.label.toLowerCase()} of '${selectedItem}'`}
|
||||
side="top"
|
||||
arrow
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
size="icon"
|
||||
@@ -163,7 +135,7 @@ export function EntityCountsSection<T>({
|
||||
className={styles.navigateButton}
|
||||
prefix={<Compass size={14} />}
|
||||
/>
|
||||
</TooltipSimple>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
export interface IDrawerLifecycleStore {
|
||||
openedAt: number | null;
|
||||
markOpened: () => void;
|
||||
markClosed: () => void;
|
||||
getDrawerDurationMs: () => number | null;
|
||||
}
|
||||
|
||||
export const useDrawerLifecycleStore = create<IDrawerLifecycleStore>()(
|
||||
(set, get) => ({
|
||||
openedAt: null,
|
||||
markOpened: (): void => set({ openedAt: Date.now() }),
|
||||
markClosed: (): void => set({ openedAt: null }),
|
||||
getDrawerDurationMs: (): number | null => {
|
||||
const { openedAt } = get();
|
||||
return openedAt == null ? null : Date.now() - openedAt;
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const getDrawerDurationMs = (): number | null =>
|
||||
useDrawerLifecycleStore.getState().getDrawerDurationMs();
|
||||
@@ -1,73 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useGetFieldsKeys } from 'api/generated/services/fields';
|
||||
import {
|
||||
TelemetrytypesFieldContextDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { useGlobalTimeStore } from 'store/globalTime';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
|
||||
import {
|
||||
InfraMonitoringEntity,
|
||||
METRIC_NAMESPACE_BY_ENTITY,
|
||||
} from '../constants';
|
||||
|
||||
export interface GroupByOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface UseInfraMonitoringGroupByDataReturn {
|
||||
groupByOptions: GroupByOption[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function useInfraMonitoringGroupByData(
|
||||
entity: InfraMonitoringEntity,
|
||||
): UseInfraMonitoringGroupByDataReturn {
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
const startUnixMilli = Math.floor(minTime / NANO_SECOND_MULTIPLIER);
|
||||
const endUnixMilli = Math.floor(maxTime / NANO_SECOND_MULTIPLIER);
|
||||
|
||||
const { data: groupByFiltersData, isLoading } = useGetFieldsKeys(
|
||||
{
|
||||
signal: TelemetrytypesSignalDTO.metrics,
|
||||
metricNamespace: METRIC_NAMESPACE_BY_ENTITY[entity],
|
||||
limit: 100,
|
||||
startUnixMilli,
|
||||
endUnixMilli,
|
||||
fieldContext: TelemetrytypesFieldContextDTO.resource,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
queryKey: ['getFieldsKeys', entity, startUnixMilli, endUnixMilli],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const groupByOptions = useMemo(() => {
|
||||
const keys = groupByFiltersData?.data?.keys;
|
||||
if (!keys) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allKeys = Object.values(keys).flat();
|
||||
const seen = new Set<string>();
|
||||
|
||||
return allKeys
|
||||
.filter((field) => {
|
||||
if (seen.has(field.name)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(field.name);
|
||||
return true;
|
||||
})
|
||||
.map((field) => ({
|
||||
value: field.name,
|
||||
label: field.name,
|
||||
}));
|
||||
}, [groupByFiltersData]);
|
||||
|
||||
return { groupByOptions, isLoading };
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { CellTypographySize } from 'components/TanStackTableView';
|
||||
|
||||
export type FontSize = CellTypographySize;
|
||||
|
||||
export interface IInfraMonitoringTablePreferencesStore {
|
||||
lineClamp: number;
|
||||
fontSize: FontSize;
|
||||
increaseLineClamp: () => void;
|
||||
decreaseLineClamp: () => void;
|
||||
setLineClamp: (value: number) => void;
|
||||
setFontSize: (value: FontSize) => void;
|
||||
}
|
||||
|
||||
const MIN_LINE_CLAMP = 1;
|
||||
const MAX_LINE_CLAMP = 10;
|
||||
const DEFAULT_LINE_CLAMP = 1;
|
||||
const DEFAULT_FONT_SIZE: FontSize = 'medium';
|
||||
|
||||
export const useInfraMonitoringTablePreferencesStore =
|
||||
create<IInfraMonitoringTablePreferencesStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
lineClamp: DEFAULT_LINE_CLAMP,
|
||||
fontSize: DEFAULT_FONT_SIZE,
|
||||
increaseLineClamp: (): void => {
|
||||
const current = get().lineClamp;
|
||||
if (current < MAX_LINE_CLAMP) {
|
||||
set({ lineClamp: current + 1 });
|
||||
}
|
||||
},
|
||||
decreaseLineClamp: (): void => {
|
||||
const current = get().lineClamp;
|
||||
if (current > MIN_LINE_CLAMP) {
|
||||
set({ lineClamp: current - 1 });
|
||||
}
|
||||
},
|
||||
setLineClamp: (value: number): void => {
|
||||
if (value >= MIN_LINE_CLAMP && value <= MAX_LINE_CLAMP) {
|
||||
set({ lineClamp: value });
|
||||
}
|
||||
},
|
||||
setFontSize: (value: FontSize): void => {
|
||||
set({ fontSize: value });
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: '@signoz/infra-monitoring-table-preferences',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export const useInfraMonitoringLineClamp = (): number =>
|
||||
useInfraMonitoringTablePreferencesStore((s) => s.lineClamp);
|
||||
|
||||
export const useInfraMonitoringFontSize = (): FontSize =>
|
||||
useInfraMonitoringTablePreferencesStore((s) => s.fontSize);
|
||||
@@ -1,89 +0,0 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import {
|
||||
TableColumnDef,
|
||||
useColumnOrder,
|
||||
useHiddenColumnIds,
|
||||
} from 'components/TanStackTableView';
|
||||
import { logInfraColumnCustomizedEvent } from 'constants/events';
|
||||
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
|
||||
import {
|
||||
useInfraMonitoringFontSize,
|
||||
useInfraMonitoringLineClamp,
|
||||
} from './useInfraMonitoringTablePreferencesStore';
|
||||
import { sortByColumnOrder } from './utils';
|
||||
|
||||
interface UseEmitColumnCustomizedParams<TData> {
|
||||
entity: InfraMonitoringEntity;
|
||||
source: 'list' | 'expanded';
|
||||
storageKey: string;
|
||||
columns: TableColumnDef<TData>[];
|
||||
}
|
||||
|
||||
// Multiple instances of this hook can share a storageKey (every expanded group
|
||||
// row mounts one), so the last emitted payload is tracked per key to collapse
|
||||
// their simultaneous effect runs into a single event.
|
||||
const lastEmittedPayloadByKey = new Map<string, string>();
|
||||
|
||||
export function useLogEventForColumnCustomized<TData>({
|
||||
entity,
|
||||
source,
|
||||
storageKey,
|
||||
columns,
|
||||
}: UseEmitColumnCustomizedParams<TData>): void {
|
||||
const hiddenColumnIds = useHiddenColumnIds(storageKey);
|
||||
const columnOrder = useColumnOrder(storageKey);
|
||||
const fontSize = useInfraMonitoringFontSize();
|
||||
const lineClamp = useInfraMonitoringLineClamp();
|
||||
|
||||
const hiddenBehavior =
|
||||
source === 'expanded' ? 'hidden-on-expand' : 'hidden-on-collapse';
|
||||
|
||||
const orderedVisibleColumnIds = useMemo(() => {
|
||||
const visibleColumns = columns.filter(
|
||||
(col) =>
|
||||
(col.visibilityBehavior ?? 'always-visible') !== hiddenBehavior &&
|
||||
!hiddenColumnIds.includes(col.id),
|
||||
);
|
||||
return sortByColumnOrder(visibleColumns, (col) => col.id, columnOrder).map(
|
||||
(col) => col.id,
|
||||
);
|
||||
}, [columns, hiddenColumnIds, columnOrder, hiddenBehavior]);
|
||||
|
||||
const isFirstRender = useRef(true);
|
||||
const prevStorageKeyRef = useRef(storageKey);
|
||||
|
||||
// Re-arm the first-render skip when the table identity changes (the hook
|
||||
// survives category switches because K8sDynamicList keeps K8sBaseList
|
||||
// mounted), so the switch itself is not reported as a customization
|
||||
if (prevStorageKeyRef.current !== storageKey) {
|
||||
prevStorageKeyRef.current = storageKey;
|
||||
isFirstRender.current = true;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isFirstRender.current) {
|
||||
isFirstRender.current = false;
|
||||
return;
|
||||
}
|
||||
const dedupeKey = `${storageKey}:${source}`;
|
||||
const payload = JSON.stringify([
|
||||
entity,
|
||||
orderedVisibleColumnIds,
|
||||
fontSize,
|
||||
lineClamp,
|
||||
]);
|
||||
if (lastEmittedPayloadByKey.get(dedupeKey) === payload) {
|
||||
return;
|
||||
}
|
||||
lastEmittedPayloadByKey.set(dedupeKey, payload);
|
||||
logInfraColumnCustomizedEvent(
|
||||
entity,
|
||||
orderedVisibleColumnIds,
|
||||
fontSize,
|
||||
lineClamp,
|
||||
source,
|
||||
);
|
||||
}, [entity, source, storageKey, orderedVisibleColumnIds, fontSize, lineClamp]);
|
||||
}
|
||||
@@ -9,22 +9,6 @@ import {
|
||||
import { SelectedItemParams } from 'container/InfraMonitoringK8sV2/hooks';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
|
||||
|
||||
export function sortByColumnOrder<T>(
|
||||
items: T[],
|
||||
getId: (item: T) => string,
|
||||
columnOrder: string[],
|
||||
): T[] {
|
||||
if (columnOrder.length === 0) {
|
||||
return items;
|
||||
}
|
||||
const orderIndex = new Map(columnOrder.map((id, index) => [id, index]));
|
||||
return [...items].sort(
|
||||
(a, b) =>
|
||||
(orderIndex.get(getId(a)) ?? Number.MAX_SAFE_INTEGER) -
|
||||
(orderIndex.get(getId(b)) ?? Number.MAX_SAFE_INTEGER),
|
||||
);
|
||||
}
|
||||
|
||||
export function getGroupedByMeta<
|
||||
T extends { meta?: Record<string, string> | null },
|
||||
>(itemData: T, groupBy: string[]): Record<string, string> {
|
||||
|
||||
@@ -10,8 +10,8 @@ import EntityGroupHeader from '../Base/EntityGroupHeader';
|
||||
import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { formatBytes, getPodStatusItems } from '../commonUtils';
|
||||
import {
|
||||
CellValueTooltip,
|
||||
GroupedStatusCounts,
|
||||
TextNoData,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../components';
|
||||
import {
|
||||
@@ -75,9 +75,10 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-expand',
|
||||
cell: ({ value }): React.ReactNode => (
|
||||
<TanStackTable.Text>{value}</TanStackTable.Text>
|
||||
),
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const clusterName = value as string;
|
||||
return <CellValueTooltip value={clusterName} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'nodeCountsByReadiness',
|
||||
@@ -94,7 +95,7 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
|
||||
enableSort: false,
|
||||
cell: ({ row, rowId }): React.ReactNode => {
|
||||
if (!row.nodeCountsByReadiness) {
|
||||
return <TextNoData type="tanstack" />;
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -132,7 +133,7 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
|
||||
cell: ({ row, rowId }): React.ReactNode => {
|
||||
const podCountsByStatus = row.podCountsByStatus;
|
||||
if (!podCountsByStatus) {
|
||||
return <TextNoData type="tanstack" />;
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<GroupedStatusCounts
|
||||
@@ -146,17 +147,17 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
|
||||
id: 'cpu',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#cpu-usage-cores">
|
||||
CPU Usage (cores)
|
||||
CPU Usage
|
||||
<br /> (cores)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.clusterCPU,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={cpu}
|
||||
entity={InfraMonitoringEntity.CLUSTERS}
|
||||
attribute="CPU metric"
|
||||
@@ -170,17 +171,17 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
|
||||
id: 'cpu_allocatable',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#cpu-alloc-cores">
|
||||
CPU Allocatable (cores)
|
||||
CPU Allocatable
|
||||
<br /> (cores)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.clusterCPUAllocatable,
|
||||
width: { min: 200 },
|
||||
enableSort: true,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuAllocatable = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={cpuAllocatable}
|
||||
entity={InfraMonitoringEntity.CLUSTERS}
|
||||
attribute="CPU allocatable metric"
|
||||
@@ -194,17 +195,17 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
|
||||
id: 'memory',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#memory-usage-wss">
|
||||
Memory Usage (WSS)
|
||||
Memory Usage
|
||||
<br /> (WSS)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.clusterMemory,
|
||||
width: { min: 180 },
|
||||
enableSort: true,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={memory}
|
||||
entity={InfraMonitoringEntity.CLUSTERS}
|
||||
attribute="memory metric"
|
||||
@@ -218,17 +219,17 @@ export const k8sClustersColumnsConfig: ClusterTableColumnConfig[] = [
|
||||
id: 'memory_allocatable',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/clusters#memory-allocatable">
|
||||
Memory Allocatable
|
||||
Memory
|
||||
<br /> Allocatable
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.clusterMemoryAllocatable,
|
||||
width: { min: 180 },
|
||||
enableSort: true,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryAllocatable = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={memoryAllocatable}
|
||||
entity={InfraMonitoringEntity.CLUSTERS}
|
||||
attribute="memory allocatable metric"
|
||||
|
||||
@@ -9,9 +9,9 @@ import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import { formatBytes, getPodStatusItems } from '../commonUtils';
|
||||
import {
|
||||
CellValueTooltip,
|
||||
EntityProgressBar,
|
||||
GroupedStatusCounts,
|
||||
TextNoData,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../components';
|
||||
import {
|
||||
@@ -85,9 +85,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-expand',
|
||||
cell: ({ value }): React.ReactNode => (
|
||||
<TanStackTable.Text>{value}</TanStackTable.Text>
|
||||
),
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const daemonsetName = value as string;
|
||||
return <CellValueTooltip value={daemonsetName} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'namespaceName',
|
||||
@@ -101,9 +102,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
width: { min: 160 },
|
||||
enableSort: false,
|
||||
enableResize: true,
|
||||
cell: ({ value }): React.ReactNode => (
|
||||
<TanStackTable.Text>{value}</TanStackTable.Text>
|
||||
),
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const namespaceName = value as string;
|
||||
return <CellValueTooltip value={namespaceName} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'pod_counts_by_status',
|
||||
@@ -122,7 +124,7 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
cell: ({ row, rowId }): React.ReactNode => {
|
||||
const podCountsByStatus = row.podCountsByStatus;
|
||||
if (!podCountsByStatus) {
|
||||
return <TextNoData type="tanstack" />;
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<GroupedStatusCounts
|
||||
@@ -175,7 +177,8 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
id: 'cpu_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-req-usage-">
|
||||
CPU Request Usage (%)
|
||||
CPU Request
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.daemonSetCPURequest,
|
||||
@@ -183,11 +186,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuRequest = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={cpuRequest}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="CPU Request"
|
||||
@@ -201,18 +203,18 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
id: 'cpu_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-limit-usage-">
|
||||
CPU Limit Usage (%)
|
||||
CPU Limit
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.daemonSetCPULimit,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuLimit = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={cpuLimit}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="CPU Limit"
|
||||
@@ -226,7 +228,8 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
id: 'cpu',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#cpu-usage-cores">
|
||||
CPU Usage (cores)
|
||||
CPU Usage
|
||||
<br /> (cores)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.daemonSetCPU,
|
||||
@@ -234,11 +237,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={cpu}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="CPU metric"
|
||||
@@ -252,7 +254,8 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
id: 'memory_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-req-usage-">
|
||||
Memory Request Usage (%)
|
||||
Memory Request
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.daemonSetMemoryRequest,
|
||||
@@ -260,11 +263,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryRequest = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={memoryRequest}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="Memory Request"
|
||||
@@ -278,18 +280,18 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
id: 'memory_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-limit-usage-">
|
||||
Memory Limit Usage (%)
|
||||
Memory Limit
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.daemonSetMemoryLimit,
|
||||
width: { min: 190 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryLimit = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={memoryLimit}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="Memory Limit"
|
||||
@@ -303,18 +305,18 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
id: 'memory',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/daemonsets#mem-usage-wss">
|
||||
Memory Usage (WSS)
|
||||
Memory Usage
|
||||
<br /> (WSS)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.daemonSetMemory,
|
||||
width: { min: 180 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={memory}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="memory metric"
|
||||
@@ -335,11 +337,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
width: { min: 140 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const readyNodes = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={readyNodes}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="ready node"
|
||||
@@ -360,11 +361,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
width: { min: 140 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const currentNodes = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={currentNodes}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="current node"
|
||||
@@ -385,11 +385,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
width: { min: 140 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const desiredNodes = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={desiredNodes}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="desired node"
|
||||
@@ -410,11 +409,10 @@ export const k8sDaemonSetsColumnsConfig: DaemonSetTableColumnConfig[] = [
|
||||
width: { min: 140 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const misscheduledNodes = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={misscheduledNodes}
|
||||
entity={InfraMonitoringEntity.DAEMONSETS}
|
||||
attribute="misscheduled node"
|
||||
|
||||
@@ -9,9 +9,9 @@ import K8sGroupCell from '../Base/K8sGroupCell';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
import { formatBytes, getPodStatusItems } from '../commonUtils';
|
||||
import {
|
||||
CellValueTooltip,
|
||||
EntityProgressBar,
|
||||
GroupedStatusCounts,
|
||||
TextNoData,
|
||||
ValidateColumnValueWrapper,
|
||||
} from '../components';
|
||||
import {
|
||||
@@ -86,9 +86,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
enableMove: false,
|
||||
pin: 'left',
|
||||
visibilityBehavior: 'hidden-on-expand',
|
||||
cell: ({ value }): React.ReactNode => (
|
||||
<TanStackTable.Text>{value}</TanStackTable.Text>
|
||||
),
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const deploymentName = value as string;
|
||||
return <CellValueTooltip value={deploymentName} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'namespaceName',
|
||||
@@ -120,7 +121,7 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
cell: ({ row, rowId }): React.ReactNode => {
|
||||
const podCountsByStatus = row.podCountsByStatus;
|
||||
if (!podCountsByStatus) {
|
||||
return <TextNoData type="tanstack" />;
|
||||
return <TanStackTable.Text>-</TanStackTable.Text>;
|
||||
}
|
||||
return (
|
||||
<GroupedStatusCounts
|
||||
@@ -163,7 +164,8 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
id: 'cpu_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-req-usage-">
|
||||
CPU Request Usage (%)
|
||||
CPU Request
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.deploymentCPURequest,
|
||||
@@ -171,11 +173,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuRequest = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={cpuRequest}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="CPU Request"
|
||||
@@ -189,18 +190,18 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
id: 'cpu_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-limit-usage-">
|
||||
CPU Limit Usage (%)
|
||||
CPU Limit
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.deploymentCPULimit,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpuLimit = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={cpuLimit}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="CPU Limit"
|
||||
@@ -214,18 +215,18 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
id: 'cpu',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#cpu-usage-cores">
|
||||
CPU Usage (cores)
|
||||
CPU Usage
|
||||
<br /> (cores)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.deploymentCPU,
|
||||
width: { min: 160 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const cpu = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={cpu}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="CPU metric"
|
||||
@@ -239,7 +240,8 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
id: 'memory_request',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-req-usage-">
|
||||
Memory Request Usage (%)
|
||||
Memory Request
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.deploymentMemoryRequest,
|
||||
@@ -247,11 +249,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryRequest = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={memoryRequest}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="Memory Request"
|
||||
@@ -265,18 +266,18 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
id: 'memory_limit',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-limit-usage-">
|
||||
Memory Limit Usage (%)
|
||||
Memory Limit
|
||||
<br /> Usage (%)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.deploymentMemoryLimit,
|
||||
width: { min: 210 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memoryLimit = Number(value);
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={memoryLimit}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="Memory Limit"
|
||||
@@ -290,18 +291,18 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
id: 'memory',
|
||||
header: (): React.ReactNode => (
|
||||
<ColumnHeader docPath="/infrastructure-monitoring/kubernetes/deployments#mem-usage-wss">
|
||||
Memory Usage (WSS)
|
||||
Memory Usage
|
||||
<br /> (WSS)
|
||||
</ColumnHeader>
|
||||
),
|
||||
accessorFn: (row): number => row.deploymentMemory,
|
||||
width: { min: 180 },
|
||||
enableSort: true,
|
||||
enableResize: true,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const memory = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={memory}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="memory metric"
|
||||
@@ -322,11 +323,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
width: { min: 120 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const availablePods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={availablePods}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="available pod"
|
||||
@@ -347,11 +347,10 @@ export const k8sDeploymentsColumnsConfig: TableColumnDef<InframonitoringtypesDep
|
||||
width: { min: 120 },
|
||||
enableSort: true,
|
||||
defaultVisibility: false,
|
||||
cell: ({ value, rowId }): React.ReactNode => {
|
||||
cell: ({ value }): React.ReactNode => {
|
||||
const desiredPods = value as number;
|
||||
return (
|
||||
<ValidateColumnValueWrapper
|
||||
rowId={rowId}
|
||||
value={desiredPods}
|
||||
entity={InfraMonitoringEntity.DEPLOYMENTS}
|
||||
attribute="desired pod"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -3,10 +3,7 @@ import { Undo } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraDrawerTimeRangeCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
@@ -16,8 +13,6 @@ import {
|
||||
|
||||
import { useEntityDetailsTime } from './useEntityDetailsTime';
|
||||
|
||||
import styles from './EntityDateTimeSelector.module.scss';
|
||||
|
||||
interface EntityDateTimeSelectorProps {
|
||||
eventEntity: string;
|
||||
category: InfraMonitoringEntity;
|
||||
@@ -46,14 +41,13 @@ function EntityDateTimeSelector({
|
||||
view,
|
||||
interval,
|
||||
});
|
||||
logInfraDrawerTimeRangeCustomizedEvent(category, interval);
|
||||
handleTimeChange(interval, dateTimeRange);
|
||||
},
|
||||
[category, view, eventEntity, handleTimeChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className="entity-date-time-selector">
|
||||
{hasTimeChanged && (
|
||||
<TooltipSimple title="Reset to list time" side="bottom">
|
||||
<Button
|
||||
|
||||
@@ -16,10 +16,7 @@ import {
|
||||
combineInitialAndUserExpression,
|
||||
getUserExpressionFromCombined,
|
||||
} from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraDrawerFilterCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import Controls from 'container/Controls';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import LoadingContainer from 'container/InfraMonitoringK8sV2/LoadingContainer';
|
||||
@@ -127,13 +124,6 @@ function EntityEventsContent({
|
||||
view: InfraMonitoringEvents.EventsView,
|
||||
});
|
||||
|
||||
logInfraDrawerFilterCustomizedEvent(
|
||||
category,
|
||||
'events',
|
||||
newUserExpression || '',
|
||||
'search',
|
||||
);
|
||||
|
||||
refetch();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
.listContainer {
|
||||
flex: 1;
|
||||
height: calc(100vh - 330px) !important;
|
||||
height: calc(100vh - 312px) !important;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
import { Card } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -20,10 +20,7 @@ import {
|
||||
combineInitialAndUserExpression,
|
||||
getUserExpressionFromCombined,
|
||||
} from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
|
||||
import {
|
||||
InfraMonitoringEvents,
|
||||
logInfraDrawerFilterCustomizedEvent,
|
||||
} from 'constants/events';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
@@ -45,14 +42,6 @@ import { K8S_ENTITY_LOGS_EXPRESSION_KEY, useInfiniteEntityLogs } from './hooks';
|
||||
import { getEntityLogsQueryPayload } from './utils';
|
||||
|
||||
import styles from './EntityLogs.module.scss';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
interface Props {
|
||||
eventEntity: string;
|
||||
@@ -68,9 +57,6 @@ function EntityLogsContent({
|
||||
}: Omit<Props, 'initialExpression'>): JSX.Element {
|
||||
const { timeRange } = useEntityDetailsTime();
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||
const logDetailContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const expression = useExpression();
|
||||
const inputExpression = useInputExpression();
|
||||
@@ -99,10 +85,8 @@ function EntityLogsContent({
|
||||
: partExpression;
|
||||
|
||||
querySearchOnRun(newUser);
|
||||
|
||||
logInfraDrawerFilterCustomizedEvent(category, 'logs', newUser, 'logs');
|
||||
},
|
||||
[userExpression, querySearchOnRun, handleCloseLogDetail, category],
|
||||
[userExpression, querySearchOnRun, handleCloseLogDetail],
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -143,13 +127,6 @@ function EntityLogsContent({
|
||||
view: InfraMonitoringEvents.LogsView,
|
||||
});
|
||||
|
||||
logInfraDrawerFilterCustomizedEvent(
|
||||
category,
|
||||
'logs',
|
||||
newUserExpression || '',
|
||||
'search',
|
||||
);
|
||||
|
||||
refetch();
|
||||
}
|
||||
},
|
||||
@@ -171,44 +148,6 @@ function EntityLogsContent({
|
||||
virtuosoRef,
|
||||
});
|
||||
|
||||
const { updateAllQueriesOperators } = useQueryBuilder();
|
||||
|
||||
const handleOpenInExplorer = useCallback(
|
||||
(e: React.MouseEvent<Element, MouseEvent>, log: ILog) => {
|
||||
const baseQuery = updateAllQueriesOperators(
|
||||
initialQueriesMap[DataSource.LOGS],
|
||||
PANEL_TYPES.LIST,
|
||||
DataSource.LOGS,
|
||||
);
|
||||
|
||||
const queryParams = {
|
||||
[QueryParams.activeLogId]: `"${log?.id}"`,
|
||||
[QueryParams.startTime]: timeRange.startTime.toString(),
|
||||
[QueryParams.endTime]: timeRange.endTime.toString(),
|
||||
[QueryParams.compositeQuery]: JSON.stringify({
|
||||
...baseQuery,
|
||||
builder: {
|
||||
...baseQuery.builder,
|
||||
queryData: baseQuery.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
filter: { expression },
|
||||
})),
|
||||
},
|
||||
} satisfies Query),
|
||||
};
|
||||
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`, {
|
||||
newTab: !!e && isModifierKeyPressed(e),
|
||||
});
|
||||
},
|
||||
[
|
||||
timeRange.startTime,
|
||||
timeRange.endTime,
|
||||
expression,
|
||||
safeNavigate,
|
||||
updateAllQueriesOperators,
|
||||
],
|
||||
);
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(_: number, logToRender: ILog): JSX.Element => {
|
||||
return (
|
||||
@@ -318,7 +257,6 @@ function EntityLogsContent({
|
||||
{renderContent}
|
||||
</div>
|
||||
)}
|
||||
<div ref={logDetailContainerRef} data-log-detail-ignore="true" />
|
||||
{selectedTab && activeLog && (
|
||||
<LogDetail
|
||||
log={activeLog}
|
||||
@@ -329,10 +267,6 @@ function EntityLogsContent({
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
onScrollToLog={handleScrollToLog}
|
||||
handleOpenInExplorer={(e) => handleOpenInExplorer(e, activeLog)}
|
||||
getContainer={(): HTMLElement =>
|
||||
logDetailContainerRef.current || document.body
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.infoIcon {
|
||||
@@ -21,7 +20,6 @@
|
||||
align-items: center;
|
||||
color: var(--l2-foreground);
|
||||
transition: opacity 0.2s;
|
||||
margin-left: auto;
|
||||
|
||||
&:hover {
|
||||
color: var(--l3-foreground);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user