mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-21 15:50:27 +01:00
Compare commits
2 Commits
v0.80.0
...
issue_7376
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1075e6b84 | ||
|
|
93b7f40a24 |
@@ -1,7 +1,6 @@
|
||||
.git
|
||||
.github
|
||||
.vscode
|
||||
.devenv
|
||||
README.md
|
||||
deploy
|
||||
sample-apps
|
||||
|
||||
10
.github/workflows/build-enterprise.yaml
vendored
10
.github/workflows/build-enterprise.yaml
vendored
@@ -73,7 +73,7 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: frontend/.env
|
||||
key: enterprise-dotenv-${{ github.sha }}
|
||||
key: dotenv-${{ github.sha }}
|
||||
js-build:
|
||||
uses: signoz/primus.workflows/.github/workflows/js-build.yaml@main
|
||||
needs: prepare
|
||||
@@ -81,10 +81,10 @@ jobs:
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
JS_SRC: frontend
|
||||
JS_INPUT_ARTIFACT_CACHE_KEY: enterprise-dotenv-${{ github.sha }}
|
||||
JS_INPUT_ARTIFACT_CACHE_KEY: dotenv-${{ github.sha }}
|
||||
JS_INPUT_ARTIFACT_PATH: frontend/.env
|
||||
JS_OUTPUT_ARTIFACT_CACHE_KEY: enterprise-jsbuild-${{ github.sha }}
|
||||
JS_OUTPUT_ARTIFACT_PATH: frontend/build
|
||||
JS_OUTPUT_ARTIFACT_CACHE_KEY: jsbuild-${{ github.sha }}
|
||||
JS_OUTPUT_ARTIFACT_PATH: frontend/build
|
||||
DOCKER_BUILD: false
|
||||
DOCKER_MANIFEST: false
|
||||
go-build:
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_INPUT_ARTIFACT_CACHE_KEY: enterprise-jsbuild-${{ github.sha }}
|
||||
GO_INPUT_ARTIFACT_CACHE_KEY: jsbuild-${{ github.sha }}
|
||||
GO_INPUT_ARTIFACT_PATH: frontend/build
|
||||
GO_BUILD_CONTEXT: ./ee/query-service
|
||||
GO_BUILD_FLAGS: >-
|
||||
|
||||
21
.github/workflows/build-staging.yaml
vendored
21
.github/workflows/build-staging.yaml
vendored
@@ -64,13 +64,22 @@ jobs:
|
||||
run: |
|
||||
mkdir -p frontend
|
||||
echo 'CI=1' > frontend/.env
|
||||
echo 'TUNNEL_URL=https://telemetry.staging.signoz.cloud/tunnel' >> frontend/.env
|
||||
echo 'TUNNEL_DOMAIN=https://telemetry.staging.signoz.cloud' >> frontend/.env
|
||||
echo 'INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' >> frontend/.env
|
||||
echo 'SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> frontend/.env
|
||||
echo 'SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> frontend/.env
|
||||
echo 'SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> frontend/.env
|
||||
echo 'SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> frontend/.env
|
||||
echo 'SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> frontend/.env
|
||||
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
|
||||
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
|
||||
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
|
||||
echo 'CUSTOMERIO_ID="${{ secrets.CUSTOMERIO_ID }}"' >> frontend/.env
|
||||
echo 'CUSTOMERIO_SITE_ID="${{ secrets.CUSTOMERIO_SITE_ID }}"' >> frontend/.env
|
||||
- name: cache-dotenv
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: frontend/.env
|
||||
key: staging-dotenv-${{ github.sha }}
|
||||
key: dotenv-${{ github.sha }}
|
||||
js-build:
|
||||
uses: signoz/primus.workflows/.github/workflows/js-build.yaml@main
|
||||
needs: prepare
|
||||
@@ -78,9 +87,9 @@ jobs:
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
JS_SRC: frontend
|
||||
JS_INPUT_ARTIFACT_CACHE_KEY: staging-dotenv-${{ github.sha }}
|
||||
JS_INPUT_ARTIFACT_CACHE_KEY: dotenv-${{ github.sha }}
|
||||
JS_INPUT_ARTIFACT_PATH: frontend/.env
|
||||
JS_OUTPUT_ARTIFACT_CACHE_KEY: staging-jsbuild-${{ github.sha }}
|
||||
JS_OUTPUT_ARTIFACT_CACHE_KEY: jsbuild-${{ github.sha }}
|
||||
JS_OUTPUT_ARTIFACT_PATH: frontend/build
|
||||
DOCKER_BUILD: false
|
||||
DOCKER_MANIFEST: false
|
||||
@@ -90,7 +99,7 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_INPUT_ARTIFACT_CACHE_KEY: staging-jsbuild-${{ github.sha }}
|
||||
GO_INPUT_ARTIFACT_CACHE_KEY: jsbuild-${{ github.sha }}
|
||||
GO_INPUT_ARTIFACT_PATH: frontend/build
|
||||
GO_BUILD_CONTEXT: ./ee/query-service
|
||||
GO_BUILD_FLAGS: >-
|
||||
|
||||
55
.github/workflows/integrationci.yaml
vendored
55
.github/workflows/integrationci.yaml
vendored
@@ -1,55 +0,0 @@
|
||||
name: integrationci
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
pull_request_target:
|
||||
types:
|
||||
- labeled
|
||||
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
src:
|
||||
- bootstrap
|
||||
sqlstore-provider:
|
||||
- postgres
|
||||
- sqlite
|
||||
clickhouse-version:
|
||||
- 24.1.2-alpine
|
||||
- 24.12-alpine
|
||||
schema-migrator-version:
|
||||
- v0.111.38
|
||||
postgres-version:
|
||||
- 15
|
||||
if: |
|
||||
((github.event_name == 'pull_request' && ! github.event.pull_request.head.repo.fork && github.event.pull_request.user.login != 'dependabot[bot]' && ! contains(github.event.pull_request.labels.*.name, 'safe-to-test')) ||
|
||||
(github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'safe-to-test'))) && contains(github.event.pull_request.labels.*.name, 'safe-to-integrate')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.13
|
||||
- name: poetry
|
||||
run: |
|
||||
python -m pip install poetry==2.1.2
|
||||
python -m poetry config virtualenvs.in-project true
|
||||
cd tests/integration && poetry install --no-root
|
||||
- name: run
|
||||
run: |
|
||||
cd tests/integration && \
|
||||
poetry run pytest -ra \
|
||||
--basetemp=./tmp/ \
|
||||
-vv \
|
||||
--capture=no \
|
||||
src/${{matrix.src}} \
|
||||
--sqlstore-provider ${{matrix.sqlstore-provider}} \
|
||||
--postgres-version ${{matrix.postgres-version}} \
|
||||
--clickhouse-version ${{matrix.clickhouse-version}} \
|
||||
--schema-migrator-version ${{matrix.schema-migrator-version}}
|
||||
4
.github/workflows/prereleaser.yaml
vendored
4
.github/workflows/prereleaser.yaml
vendored
@@ -1,9 +1,9 @@
|
||||
name: prereleaser
|
||||
|
||||
on:
|
||||
# schedule every wednesday 6:30 AM UTC (12:00 PM IST)
|
||||
# schedule every wednesday 9:30 AM UTC (3pm IST)
|
||||
schedule:
|
||||
- cron: '30 6 * * 3'
|
||||
- cron: '30 9 * * 3'
|
||||
|
||||
# allow manual triggering of the workflow by a maintainer
|
||||
workflow_dispatch:
|
||||
|
||||
61
.github/workflows/staging-deployment.yaml
vendored
Normal file
61
.github/workflows/staging-deployment.yaml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: staging-deployment
|
||||
# Trigger deployment only on push to main branch
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy latest main branch to staging
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
steps:
|
||||
- id: 'auth'
|
||||
uses: 'google-github-actions/auth@v2'
|
||||
with:
|
||||
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
|
||||
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
|
||||
|
||||
- name: 'sdk'
|
||||
uses: 'google-github-actions/setup-gcloud@v2'
|
||||
|
||||
- name: 'ssh'
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
|
||||
GCP_ZONE: ${{ secrets.GCP_ZONE }}
|
||||
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
|
||||
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
|
||||
run: |
|
||||
read -r -d '' COMMAND <<EOF || true
|
||||
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
||||
echo "GITHUB_SHA: ${GITHUB_SHA}"
|
||||
export VERSION="${GITHUB_SHA:0:7}" # needed for child process to access it
|
||||
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
|
||||
export KAFKA_SPAN_EVAL="true"
|
||||
docker system prune --force --all
|
||||
OTELCOL_TAG=$(curl -s https://api.github.com/repos/SigNoz/signoz-otel-collector/releases/latest | jq -r '.tag_name // "not-found"')
|
||||
if [[ "${OTELCOL_TAG}" == "not-found" ]]; then
|
||||
echo "warning: unable to determine latest SigNoz OtelCollector release tag, skipping latest otelcol deployment"
|
||||
else
|
||||
export OTELCOL_TAG=${OTELCOL_TAG}
|
||||
docker pull signoz/signoz-otel-collector:${OTELCOL_TAG}
|
||||
docker pull signoz/signoz-schema-migrator:${OTELCOL_TAG}
|
||||
fi
|
||||
cd ~/signoz
|
||||
git status
|
||||
git add .
|
||||
git stash push -m "stashed on $(date --iso-8601=seconds)"
|
||||
git fetch origin
|
||||
git checkout ${GITHUB_BRANCH}
|
||||
git pull
|
||||
make docker-build-enterprise-amd64
|
||||
export VERSION="${GITHUB_SHA:0:7}-amd64"
|
||||
docker-compose -f deploy/docker/docker-compose.testing.yaml up --build -d
|
||||
EOF
|
||||
gcloud beta compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
|
||||
56
.github/workflows/testing-deployment.yaml
vendored
Normal file
56
.github/workflows/testing-deployment.yaml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: testing-deployment
|
||||
# Trigger deployment only on testing-deploy label on pull request
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy PR branch to testing
|
||||
runs-on: ubuntu-latest
|
||||
environment: testing
|
||||
if: ${{ github.event.label.name == 'testing-deploy' }}
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
steps:
|
||||
- id: 'auth'
|
||||
uses: 'google-github-actions/auth@v2'
|
||||
with:
|
||||
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
|
||||
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
|
||||
|
||||
- name: 'sdk'
|
||||
uses: 'google-github-actions/setup-gcloud@v2'
|
||||
|
||||
- name: 'ssh'
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
|
||||
GCP_ZONE: ${{ secrets.GCP_ZONE }}
|
||||
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
|
||||
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
|
||||
run: |
|
||||
read -r -d '' COMMAND <<EOF || true
|
||||
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
|
||||
echo "GITHUB_SHA: ${GITHUB_SHA}"
|
||||
export VERSION="${GITHUB_SHA:0:7}" # needed for child process to access it
|
||||
export DEV_BUILD="1"
|
||||
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
|
||||
docker system prune --force --all
|
||||
cd ~/signoz
|
||||
git status
|
||||
git add .
|
||||
git stash push -m "stashed on $(date --iso-8601=seconds)"
|
||||
git fetch origin
|
||||
git checkout main
|
||||
git pull
|
||||
# This is added to include the scenerio when new commit in PR is force-pushed
|
||||
git branch -D ${GITHUB_BRANCH}
|
||||
git checkout --track origin/${GITHUB_BRANCH}
|
||||
make docker-build-enterprise-amd64
|
||||
export VERSION="${GITHUB_SHA:0:7}-amd64"
|
||||
docker-compose -f deploy/docker/docker-compose.testing.yaml up --build -d
|
||||
EOF
|
||||
gcloud beta compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"
|
||||
147
.gitignore
vendored
147
.gitignore
vendored
@@ -80,153 +80,6 @@ deploy/common/clickhouse/user_scripts/
|
||||
|
||||
queries.active
|
||||
|
||||
# tmp
|
||||
**/tmp/**
|
||||
|
||||
# .devenv tmp files
|
||||
.devenv/**/tmp/**
|
||||
.qodo
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
### Python Patch ###
|
||||
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||
poetry.toml
|
||||
|
||||
# ruff
|
||||
.ruff_cache/
|
||||
|
||||
# LSP config files
|
||||
pyrightconfig.json
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/python
|
||||
32
Makefile
32
Makefile
@@ -10,7 +10,7 @@ COMMIT_SHORT_SHA ?= $(shell git rev-parse --short HEAD)
|
||||
BRANCH_NAME ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD))
|
||||
VERSION ?= $(BRANCH_NAME)-$(COMMIT_SHORT_SHA)
|
||||
TIMESTAMP ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
ARCHS ?= amd64 arm64
|
||||
ARCHS = amd64 arm64
|
||||
TARGET_DIR ?= $(shell pwd)/target
|
||||
|
||||
ZEUS_URL ?= https://api.signoz.cloud
|
||||
@@ -23,7 +23,6 @@ GO_BUILD_ARCHS_COMMUNITY = $(addprefix go-build-community-,$(ARCHS))
|
||||
GO_BUILD_CONTEXT_COMMUNITY = $(SRC)/pkg/query-service
|
||||
GO_BUILD_LDFLAGS_COMMUNITY = $(GO_BUILD_VERSION_LDFLAGS) -X github.com/SigNoz/signoz/pkg/version.variant=community
|
||||
GO_BUILD_ARCHS_ENTERPRISE = $(addprefix go-build-enterprise-,$(ARCHS))
|
||||
GO_BUILD_ARCHS_ENTERPRISE_RACE = $(addprefix go-build-enterprise-race-,$(ARCHS))
|
||||
GO_BUILD_CONTEXT_ENTERPRISE = $(SRC)/ee/query-service
|
||||
GO_BUILD_LDFLAGS_ENTERPRISE = $(GO_BUILD_VERSION_LDFLAGS) -X github.com/SigNoz/signoz/pkg/version.variant=enterprise $(GO_BUILD_LDFLAG_ZEUS_URL) $(GO_BUILD_LDFLAG_LICENSE_SIGNOZ_IO)
|
||||
|
||||
@@ -120,18 +119,6 @@ $(GO_BUILD_ARCHS_ENTERPRISE): go-build-enterprise-%: $(TARGET_DIR)
|
||||
CGO_ENABLED=1 GOARCH=$* GOOS=$(OS) go build -C $(GO_BUILD_CONTEXT_ENTERPRISE) -tags timetzdata -o $(TARGET_DIR)/$(OS)-$*/$(NAME) -ldflags "-linkmode external -extldflags '-static' -s -w $(GO_BUILD_LDFLAGS_ENTERPRISE)"; \
|
||||
fi
|
||||
|
||||
.PHONY: go-build-enterprise-race $(GO_BUILD_ARCHS_ENTERPRISE_RACE)
|
||||
go-build-enterprise-race: ## Builds the go backend server for enterprise with race
|
||||
go-build-enterprise-race: $(GO_BUILD_ARCHS_ENTERPRISE_RACE)
|
||||
$(GO_BUILD_ARCHS_ENTERPRISE_RACE): go-build-enterprise-race-%: $(TARGET_DIR)
|
||||
@mkdir -p $(TARGET_DIR)/$(OS)-$*
|
||||
@echo ">> building binary $(TARGET_DIR)/$(OS)-$*/$(NAME)"
|
||||
@if [ $* = "arm64" ]; then \
|
||||
CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOARCH=$* GOOS=$(OS) go build -C $(GO_BUILD_CONTEXT_ENTERPRISE) -race -tags timetzdata -o $(TARGET_DIR)/$(OS)-$*/$(NAME) -ldflags "-linkmode external -extldflags '-static' -s -w $(GO_BUILD_LDFLAGS_ENTERPRISE)"; \
|
||||
else \
|
||||
CGO_ENABLED=1 GOARCH=$* GOOS=$(OS) go build -C $(GO_BUILD_CONTEXT_ENTERPRISE) -race -tags timetzdata -o $(TARGET_DIR)/$(OS)-$*/$(NAME) -ldflags "-linkmode external -extldflags '-static' -s -w $(GO_BUILD_LDFLAGS_ENTERPRISE)"; \
|
||||
fi
|
||||
|
||||
##############################################################
|
||||
# js commands
|
||||
##############################################################
|
||||
@@ -180,20 +167,3 @@ docker-buildx-enterprise: go-build-enterprise js-build
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--push \
|
||||
--tag $(DOCKER_REGISTRY_ENTERPRISE):$(VERSION) $(SRC)
|
||||
|
||||
##############################################################
|
||||
# python commands
|
||||
##############################################################
|
||||
.PHONY: py-fmt
|
||||
py-fmt: ## Run black for integration tests
|
||||
@cd tests/integration && poetry run black .
|
||||
|
||||
.PHONY: py-lint
|
||||
py-lint: ## Run lint for integration tests
|
||||
@cd tests/integration && poetry run isort .
|
||||
@cd tests/integration && poetry run autoflake .
|
||||
@cd tests/integration && poetry run pylint .
|
||||
|
||||
.PHONY: py-test
|
||||
py-test: ## Runs integration tests
|
||||
@cd tests/integration && poetry run pytest --basetemp=./tmp/ -vv --capture=no src/
|
||||
@@ -174,7 +174,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.80.0
|
||||
image: signoz/signoz:v0.78.1
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
- --use-logs-new-schema=true
|
||||
@@ -208,7 +208,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.111.39
|
||||
image: signoz/signoz-otel-collector:v0.111.38
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
@@ -232,7 +232,7 @@ services:
|
||||
- signoz
|
||||
schema-migrator:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:v0.111.39
|
||||
image: signoz/signoz-schema-migrator:v0.111.38
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
@@ -110,7 +110,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.80.0
|
||||
image: signoz/signoz:v0.78.1
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
- --use-logs-new-schema=true
|
||||
@@ -143,7 +143,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.111.39
|
||||
image: signoz/signoz-otel-collector:v0.111.38
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
@@ -167,7 +167,7 @@ services:
|
||||
- signoz
|
||||
schema-migrator:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:v0.111.39
|
||||
image: signoz/signoz-schema-migrator:v0.111.38
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
||||
@@ -177,7 +177,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.80.0}
|
||||
image: signoz/signoz:${VERSION:-v0.78.1}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
@@ -212,7 +212,7 @@ services:
|
||||
# TODO: support otel-collector multiple replicas. Nginx/Traefik for loadbalancing?
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.39}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.38}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
@@ -238,7 +238,7 @@ services:
|
||||
condition: service_healthy
|
||||
schema-migrator-sync:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.39}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.38}
|
||||
container_name: schema-migrator-sync
|
||||
command:
|
||||
- sync
|
||||
@@ -249,7 +249,7 @@ services:
|
||||
condition: service_healthy
|
||||
schema-migrator-async:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.39}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.38}
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
|
||||
199
deploy/docker/docker-compose.testing.yaml
Normal file
199
deploy/docker/docker-compose.testing.yaml
Normal file
@@ -0,0 +1,199 @@
|
||||
version: "3"
|
||||
x-common: &common
|
||||
networks:
|
||||
- signoz-net
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
options:
|
||||
max-size: 50m
|
||||
max-file: "3"
|
||||
x-clickhouse-defaults: &clickhouse-defaults
|
||||
!!merge <<: *common
|
||||
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
|
||||
image: clickhouse/clickhouse-server:24.1.2-alpine
|
||||
tty: true
|
||||
labels:
|
||||
signoz.io/scrape: "true"
|
||||
signoz.io/port: "9363"
|
||||
signoz.io/path: "/metrics"
|
||||
depends_on:
|
||||
init-clickhouse:
|
||||
condition: service_completed_successfully
|
||||
zookeeper-1:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- wget
|
||||
- --spider
|
||||
- -q
|
||||
- 0.0.0.0:8123/ping
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
ulimits:
|
||||
nproc: 65535
|
||||
nofile:
|
||||
soft: 262144
|
||||
hard: 262144
|
||||
x-zookeeper-defaults: &zookeeper-defaults
|
||||
!!merge <<: *common
|
||||
image: bitnami/zookeeper:3.7.1
|
||||
user: root
|
||||
labels:
|
||||
signoz.io/scrape: "true"
|
||||
signoz.io/port: "9141"
|
||||
signoz.io/path: "/metrics"
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- curl -s -m 2 http://localhost:8080/commands/ruok | grep error | grep null
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
x-db-depend: &db-depend
|
||||
!!merge <<: *common
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
schema-migrator-sync:
|
||||
condition: service_completed_successfully
|
||||
services:
|
||||
init-clickhouse:
|
||||
!!merge <<: *common
|
||||
image: clickhouse/clickhouse-server:24.1.2-alpine
|
||||
container_name: signoz-init-clickhouse
|
||||
command:
|
||||
- bash
|
||||
- -c
|
||||
- |
|
||||
version="v0.0.1"
|
||||
node_os=$$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
node_arch=$$(uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/)
|
||||
echo "Fetching histogram-binary for $${node_os}/$${node_arch}"
|
||||
cd /tmp
|
||||
wget -O histogram-quantile.tar.gz "https://github.com/SigNoz/signoz/releases/download/histogram-quantile%2F$${version}/histogram-quantile_$${node_os}_$${node_arch}.tar.gz"
|
||||
tar -xvzf histogram-quantile.tar.gz
|
||||
mv histogram-quantile /var/lib/clickhouse/user_scripts/histogramQuantile
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
|
||||
zookeeper-1:
|
||||
!!merge <<: *zookeeper-defaults
|
||||
container_name: signoz-zookeeper-1
|
||||
ports:
|
||||
- "2181:2181"
|
||||
- "2888:2888"
|
||||
- "3888:3888"
|
||||
volumes:
|
||||
- zookeeper-1:/bitnami/zookeeper
|
||||
environment:
|
||||
- ZOO_SERVER_ID=1
|
||||
- ALLOW_ANONYMOUS_LOGIN=yes
|
||||
- ZOO_AUTOPURGE_INTERVAL=1
|
||||
- ZOO_ENABLE_PROMETHEUS_METRICS=yes
|
||||
- ZOO_PROMETHEUS_METRICS_PORT_NUMBER=9141
|
||||
clickhouse:
|
||||
!!merge <<: *clickhouse-defaults
|
||||
container_name: signoz-clickhouse
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "8123:8123"
|
||||
- "9181:9181"
|
||||
volumes:
|
||||
- ../common/clickhouse/config.xml:/etc/clickhouse-server/config.xml
|
||||
- ../common/clickhouse/users.xml:/etc/clickhouse-server/users.xml
|
||||
- ../common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
|
||||
- ../common/clickhouse/user_scripts:/var/lib/clickhouse/user_scripts/
|
||||
- ../common/clickhouse/cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
|
||||
- clickhouse:/var/lib/clickhouse/
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.78.1}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
- --gateway-url=https://api.staging.signoz.cloud
|
||||
- --use-logs-new-schema=true
|
||||
- --use-trace-new-schema=true
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
volumes:
|
||||
- ../common/signoz/prometheus.yml:/root/config/prometheus.yml
|
||||
- ../common/dashboards:/root/config/dashboards
|
||||
- sqlite:/var/lib/signoz/
|
||||
environment:
|
||||
- SIGNOZ_ALERTMANAGER_PROVIDER=signoz
|
||||
- SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
- SIGNOZ_SQLSTORE_SQLITE_PATH=/var/lib/signoz/signoz.db
|
||||
- DASHBOARDS_PATH=/root/config/dashboards
|
||||
- STORAGE=clickhouse
|
||||
- GODEBUG=netdns=go
|
||||
- TELEMETRY_ENABLED=true
|
||||
- DEPLOYMENT_TYPE=docker-standalone-amd
|
||||
- KAFKA_SPAN_EVAL=${KAFKA_SPAN_EVAL:-false}
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- wget
|
||||
- --spider
|
||||
- -q
|
||||
- localhost:8080/api/v1/health
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.38}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
- --manager-config=/etc/manager-config.yaml
|
||||
- --copy-path=/var/tmp/collector-config.yaml
|
||||
- --feature-gates=-pkg.translator.prometheus.NormalizeName
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
- ../common/signoz/otel-collector-opamp-config.yaml:/etc/manager-config.yaml
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||
- LOW_CARDINAL_EXCEPTION_GROUPING=false
|
||||
ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
- "4318:4318" # OTLP HTTP receiver
|
||||
depends_on:
|
||||
signoz:
|
||||
condition: service_healthy
|
||||
schema-migrator-sync:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.38}
|
||||
container_name: schema-migrator-sync
|
||||
command:
|
||||
- sync
|
||||
- --dsn=tcp://clickhouse:9000
|
||||
- --up=
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
restart: on-failure
|
||||
schema-migrator-async:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.38}
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
- --dsn=tcp://clickhouse:9000
|
||||
- --up=
|
||||
restart: on-failure
|
||||
networks:
|
||||
signoz-net:
|
||||
name: signoz-net
|
||||
volumes:
|
||||
clickhouse:
|
||||
name: signoz-clickhouse
|
||||
sqlite:
|
||||
name: signoz-sqlite
|
||||
zookeeper-1:
|
||||
name: signoz-zookeeper-1
|
||||
@@ -110,7 +110,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.80.0}
|
||||
image: signoz/signoz:${VERSION:-v0.78.1}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
@@ -144,7 +144,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.39}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.38}
|
||||
container_name: signoz-otel-collector
|
||||
command:
|
||||
- --config=/etc/otel-collector-config.yaml
|
||||
@@ -166,7 +166,7 @@ services:
|
||||
condition: service_healthy
|
||||
schema-migrator-sync:
|
||||
!!merge <<: *common
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.39}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.38}
|
||||
container_name: schema-migrator-sync
|
||||
command:
|
||||
- sync
|
||||
@@ -178,7 +178,7 @@ services:
|
||||
restart: on-failure
|
||||
schema-migrator-async:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.39}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.38}
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
FROM golang:1.22-bullseye
|
||||
|
||||
ARG OS="linux"
|
||||
ARG TARGETARCH
|
||||
ARG ZEUSURL
|
||||
|
||||
# This path is important for stacktraces
|
||||
WORKDIR $GOPATH/src/github.com/signoz/signoz
|
||||
WORKDIR /root
|
||||
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
g++ \
|
||||
gcc \
|
||||
libc6-dev \
|
||||
make \
|
||||
pkg-config \
|
||||
; \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY ./ee/ ./ee/
|
||||
COPY ./pkg/ ./pkg/
|
||||
COPY ./templates/email /root/templates
|
||||
|
||||
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
|
||||
|
||||
ENTRYPOINT ["/root/signoz"]
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/SigNoz/signoz/ee/query-service/license"
|
||||
"github.com/SigNoz/signoz/ee/query-service/usage"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/apis/fields"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference"
|
||||
preferencecore "github.com/SigNoz/signoz/pkg/modules/preference/core"
|
||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
@@ -75,7 +74,6 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler,
|
||||
UseLogsNewSchema: opts.UseLogsNewSchema,
|
||||
UseTraceNewSchema: opts.UseTraceNewSchema,
|
||||
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
|
||||
FieldsAPI: fields.NewAPI(signoz.TelemetryStore),
|
||||
Signoz: signoz,
|
||||
Preference: preference,
|
||||
})
|
||||
|
||||
@@ -153,11 +153,9 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
|
||||
func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
||||
ctx context.Context, orgId string, cloudProvider string,
|
||||
) (*types.User, *basemodel.ApiError) {
|
||||
cloudIntegrationUser := fmt.Sprintf("%s-integration", cloudProvider)
|
||||
email := fmt.Sprintf("%s@signoz.io", cloudIntegrationUser)
|
||||
cloudIntegrationUserId := fmt.Sprintf("%s-integration", cloudProvider)
|
||||
|
||||
// TODO(nitya): there should be orgId here
|
||||
integrationUserResult, apiErr := ah.AppDao().GetUserByEmail(ctx, email)
|
||||
integrationUserResult, apiErr := ah.AppDao().GetUser(ctx, cloudIntegrationUserId)
|
||||
if apiErr != nil {
|
||||
return nil, basemodel.WrapApiError(apiErr, "couldn't look for integration user")
|
||||
}
|
||||
@@ -172,9 +170,9 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
||||
)
|
||||
|
||||
newUser := &types.User{
|
||||
ID: uuid.New().String(),
|
||||
Name: cloudIntegrationUser,
|
||||
Email: email,
|
||||
ID: cloudIntegrationUserId,
|
||||
Name: fmt.Sprintf("%s integration", cloudProvider),
|
||||
Email: fmt.Sprintf("%s@signoz.io", cloudIntegrationUserId),
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
|
||||
@@ -5,18 +5,16 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
||||
"github.com/SigNoz/signoz/ee/types"
|
||||
eeTypes "github.com/SigNoz/signoz/ee/types"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
errorsV2 "github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/auth"
|
||||
baseconstants "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
"go.uber.org/zap"
|
||||
@@ -60,7 +58,7 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
|
||||
ah.Respond(w, &pat)
|
||||
}
|
||||
|
||||
func validatePATRequest(req eeTypes.GettablePAT) error {
|
||||
func validatePATRequest(req types.GettablePAT) error {
|
||||
if req.Role == "" || (req.Role != baseconstants.ViewerGroup && req.Role != baseconstants.EditorGroup && req.Role != baseconstants.AdminGroup) {
|
||||
return fmt.Errorf("valid role is required")
|
||||
}
|
||||
@@ -76,19 +74,12 @@ func validatePATRequest(req eeTypes.GettablePAT) error {
|
||||
func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background()
|
||||
|
||||
req := eeTypes.GettablePAT{}
|
||||
req := types.GettablePAT{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := valuer.NewUUID(idStr)
|
||||
if err != nil {
|
||||
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := auth.GetUserFromReqContext(r.Context())
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{
|
||||
@@ -98,25 +89,6 @@ func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//get the pat
|
||||
existingPAT, paterr := ah.AppDao().GetPATByID(ctx, user.OrgID, id)
|
||||
if paterr != nil {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, paterr.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// get the user
|
||||
createdByUser, usererr := ah.AppDao().GetUser(ctx, existingPAT.UserID)
|
||||
if usererr != nil {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, usererr.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(createdByUser.Email)) {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, "integration user pat cannot be updated"))
|
||||
return
|
||||
}
|
||||
|
||||
err = validatePATRequest(req)
|
||||
if err != nil {
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
@@ -124,6 +96,12 @@ func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
req.UpdatedByUserID = user.ID
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := valuer.NewUUID(idStr)
|
||||
if err != nil {
|
||||
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
|
||||
return
|
||||
}
|
||||
req.UpdatedAt = time.Now()
|
||||
zap.L().Info("Got Update PAT request", zap.Any("pat", req))
|
||||
var apierr basemodel.BaseApiError
|
||||
@@ -171,25 +149,6 @@ func (ah *APIHandler) revokePAT(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//get the pat
|
||||
existingPAT, paterr := ah.AppDao().GetPATByID(ctx, user.OrgID, id)
|
||||
if paterr != nil {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, paterr.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// get the user
|
||||
createdByUser, usererr := ah.AppDao().GetUser(ctx, existingPAT.UserID)
|
||||
if usererr != nil {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, usererr.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(createdByUser.Email)) {
|
||||
render.Error(w, errorsV2.Newf(errorsV2.TypeInvalidInput, errorsV2.CodeInvalidInput, "integration user pat cannot be updated"))
|
||||
return
|
||||
}
|
||||
|
||||
zap.L().Info("Revoke PAT with id", zap.String("id", id.StringValue()))
|
||||
if apierr := ah.AppDao().RevokePAT(ctx, user.OrgID, id, user.ID); apierr != nil {
|
||||
RespondError(w, apierr, nil)
|
||||
|
||||
@@ -367,7 +367,6 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
apiHandler.RegisterLogsRoutes(r, am)
|
||||
apiHandler.RegisterIntegrationRoutes(r, am)
|
||||
apiHandler.RegisterCloudIntegrationsRoutes(r, am)
|
||||
apiHandler.RegisterFieldsRoutes(r, am)
|
||||
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
||||
apiHandler.RegisterInfraMetricsRoutes(r, am)
|
||||
apiHandler.RegisterQueryRangeV4Routes(r, am)
|
||||
@@ -429,11 +428,11 @@ func (s *Server) initListeners() error {
|
||||
}
|
||||
|
||||
// Start listening on http and private http port concurrently
|
||||
func (s *Server) Start(ctx context.Context) error {
|
||||
func (s *Server) Start() error {
|
||||
|
||||
// initiate rule manager first
|
||||
if !s.serverOptions.DisableRules {
|
||||
s.ruleManager.Start(ctx)
|
||||
s.ruleManager.Start()
|
||||
} else {
|
||||
zap.L().Info("msg: Rules disabled as rules.disable is set to TRUE")
|
||||
}
|
||||
@@ -517,7 +516,7 @@ func (s *Server) Stop() error {
|
||||
s.opampServer.Stop()
|
||||
|
||||
if s.ruleManager != nil {
|
||||
s.ruleManager.Stop(context.Background())
|
||||
s.ruleManager.Stop()
|
||||
}
|
||||
|
||||
// stop usage manager
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
basedao "github.com/SigNoz/signoz/pkg/query-service/dao"
|
||||
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
ossTypes "github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/google/uuid"
|
||||
@@ -39,6 +40,7 @@ type ModelDao interface {
|
||||
UpdatePAT(ctx context.Context, orgID string, p types.GettablePAT, id valuer.UUID) basemodel.BaseApiError
|
||||
GetPAT(ctx context.Context, pat string) (*types.GettablePAT, basemodel.BaseApiError)
|
||||
GetPATByID(ctx context.Context, orgID string, id valuer.UUID) (*types.GettablePAT, basemodel.BaseApiError)
|
||||
GetUserByPAT(ctx context.Context, orgID string, token string) (*ossTypes.GettableUser, basemodel.BaseApiError)
|
||||
ListPATs(ctx context.Context, orgID string) ([]types.GettablePAT, basemodel.BaseApiError)
|
||||
RevokePAT(ctx context.Context, orgID string, id valuer.UUID, userID string) basemodel.BaseApiError
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||
@@ -43,7 +44,7 @@ func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (
|
||||
}
|
||||
|
||||
user := &types.User{
|
||||
ID: uuid.New().String(),
|
||||
ID: uuid.NewString(),
|
||||
Name: "",
|
||||
Email: email,
|
||||
Password: hash,
|
||||
@@ -161,7 +162,12 @@ func (m *modelDao) PrecheckLogin(ctx context.Context, email, sourceUrl string) (
|
||||
// find domain from email
|
||||
orgDomain, apierr := m.GetDomainByEmail(ctx, email)
|
||||
if apierr != nil {
|
||||
zap.L().Error("failed to get org domain from email", zap.String("email", email), zap.Error(apierr.ToError()))
|
||||
var emailDomain string
|
||||
emailComponents := strings.Split(email, "@")
|
||||
if len(emailComponents) > 0 {
|
||||
emailDomain = emailComponents[1]
|
||||
}
|
||||
zap.L().Error("failed to get org domain from email", zap.String("emailDomain", emailDomain), zap.Error(apierr.ToError()))
|
||||
return resp, apierr
|
||||
}
|
||||
|
||||
|
||||
@@ -196,3 +196,27 @@ func (m *modelDao) GetPATByID(ctx context.Context, orgID string, id valuer.UUID)
|
||||
|
||||
return &patWithUser, nil
|
||||
}
|
||||
|
||||
// deprecated
|
||||
func (m *modelDao) GetUserByPAT(ctx context.Context, orgID string, token string) (*ossTypes.GettableUser, basemodel.BaseApiError) {
|
||||
users := []ossTypes.GettableUser{}
|
||||
|
||||
if err := m.DB().NewSelect().
|
||||
Model(&users).
|
||||
Column("u.id", "u.name", "u.email", "u.password", "u.created_at", "u.profile_picture_url", "u.org_id", "u.group_id").
|
||||
Join("JOIN personal_access_tokens p ON u.id = p.user_id").
|
||||
Where("p.token = ?", token).
|
||||
Where("p.expires_at >= strftime('%s', 'now')").
|
||||
Where("p.org_id = ?", orgID).
|
||||
Scan(ctx); err != nil {
|
||||
return nil, model.InternalError(fmt.Errorf("failed to fetch user from PAT, err: %v", err))
|
||||
}
|
||||
|
||||
if len(users) != 1 {
|
||||
return nil, &model.ApiError{
|
||||
Typ: model.ErrorInternal,
|
||||
Err: fmt.Errorf("found zero or multiple users with same PAT token"),
|
||||
}
|
||||
}
|
||||
return &users[0], nil
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ func main() {
|
||||
zap.L().Fatal("Failed to create server", zap.Error(err))
|
||||
}
|
||||
|
||||
if err := server.Start(context.Background()); err != nil {
|
||||
if err := server.Start(); err != nil {
|
||||
zap.L().Fatal("Could not start server", zap.Error(err))
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/cache"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/common"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
|
||||
querierV2 "github.com/SigNoz/signoz/pkg/query-service/app/querier/v2"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/queryBuilder"
|
||||
@@ -53,7 +52,7 @@ type AnomalyRule struct {
|
||||
|
||||
func NewAnomalyRule(
|
||||
id string,
|
||||
p *ruletypes.PostableRule,
|
||||
p *baserules.PostableRule,
|
||||
reader interfaces.Reader,
|
||||
cache cache.Cache,
|
||||
opts ...baserules.RuleOption,
|
||||
@@ -61,7 +60,7 @@ func NewAnomalyRule(
|
||||
|
||||
zap.L().Info("creating new AnomalyRule", zap.String("id", id), zap.Any("opts", opts))
|
||||
|
||||
if p.RuleCondition.CompareOp == ruletypes.ValueIsBelow {
|
||||
if p.RuleCondition.CompareOp == baserules.ValueIsBelow {
|
||||
target := -1 * *p.RuleCondition.Target
|
||||
p.RuleCondition.Target = &target
|
||||
}
|
||||
@@ -118,7 +117,7 @@ func NewAnomalyRule(
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) Type() ruletypes.RuleType {
|
||||
func (r *AnomalyRule) Type() baserules.RuleType {
|
||||
return RuleTypeAnomaly
|
||||
}
|
||||
|
||||
@@ -158,7 +157,7 @@ func (r *AnomalyRule) GetSelectedQuery() string {
|
||||
return r.Condition().GetSelectedQueryName()
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, ts time.Time) (ruletypes.Vector, error) {
|
||||
func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, ts time.Time) (baserules.Vector, error) {
|
||||
|
||||
params, err := r.prepareQueryRange(ts)
|
||||
if err != nil {
|
||||
@@ -185,7 +184,7 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, ts time.Time) (rulet
|
||||
}
|
||||
}
|
||||
|
||||
var resultVector ruletypes.Vector
|
||||
var resultVector baserules.Vector
|
||||
|
||||
scoresJSON, _ := json.Marshal(queryResult.AnomalyScores)
|
||||
zap.L().Info("anomaly scores", zap.String("scores", string(scoresJSON)))
|
||||
@@ -214,7 +213,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
resultFPs := map[uint64]struct{}{}
|
||||
var alerts = make(map[uint64]*ruletypes.Alert, len(res))
|
||||
var alerts = make(map[uint64]*baserules.Alert, len(res))
|
||||
|
||||
for _, smpl := range res {
|
||||
l := make(map[string]string, len(smpl.Metric))
|
||||
@@ -226,7 +225,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
||||
threshold := valueFormatter.Format(r.TargetVal(), r.Unit())
|
||||
zap.L().Debug("Alert template data for rule", zap.String("name", r.Name()), zap.String("formatter", valueFormatter.Name()), zap.String("value", value), zap.String("threshold", threshold))
|
||||
|
||||
tmplData := ruletypes.AlertTemplateData(l, value, threshold)
|
||||
tmplData := baserules.AlertTemplateData(l, value, threshold)
|
||||
// Inject some convenience variables that are easier to remember for users
|
||||
// who are not used to Go's templating system.
|
||||
defs := "{{$labels := .Labels}}{{$value := .Value}}{{$threshold := .Threshold}}"
|
||||
@@ -234,7 +233,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
||||
// utility function to apply go template on labels and annotations
|
||||
expand := func(text string) string {
|
||||
|
||||
tmpl := ruletypes.NewTemplateExpander(
|
||||
tmpl := baserules.NewTemplateExpander(
|
||||
ctx,
|
||||
defs+text,
|
||||
"__alert_"+r.Name(),
|
||||
@@ -279,7 +278,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
alerts[h] = &ruletypes.Alert{
|
||||
alerts[h] = &baserules.Alert{
|
||||
Labels: lbs,
|
||||
QueryResultLables: resultLabels,
|
||||
Annotations: annotations,
|
||||
@@ -320,7 +319,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
||||
if _, ok := resultFPs[fp]; !ok {
|
||||
// If the alert was previously firing, keep it around for a given
|
||||
// retention time so it is reported as resolved to the AlertManager.
|
||||
if a.State == model.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > ruletypes.ResolvedRetention) {
|
||||
if a.State == model.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > baserules.ResolvedRetention) {
|
||||
delete(r.Active, fp)
|
||||
}
|
||||
if a.State != model.StateInactive {
|
||||
@@ -376,10 +375,10 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
||||
|
||||
func (r *AnomalyRule) String() string {
|
||||
|
||||
ar := ruletypes.PostableRule{
|
||||
ar := baserules.PostableRule{
|
||||
AlertName: r.Name(),
|
||||
RuleCondition: r.Condition(),
|
||||
EvalWindow: ruletypes.Duration(r.EvalWindow()),
|
||||
EvalWindow: baserules.Duration(r.EvalWindow()),
|
||||
Labels: r.Labels().Map(),
|
||||
Annotations: r.Annotations().Map(),
|
||||
PreferredChannels: r.PreferredChannels(),
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
|
||||
ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -19,7 +18,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
var task baserules.Task
|
||||
|
||||
ruleId := baserules.RuleIdFromTaskName(opts.TaskName)
|
||||
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
|
||||
if opts.Rule.RuleType == baserules.RuleTypeThreshold {
|
||||
// create a threshold rule
|
||||
tr, err := baserules.NewThresholdRule(
|
||||
ruleId,
|
||||
@@ -38,9 +37,9 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
rules = append(rules, tr)
|
||||
|
||||
// create ch rule task for evalution
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
|
||||
|
||||
} else if opts.Rule.RuleType == ruletypes.RuleTypeProm {
|
||||
} else if opts.Rule.RuleType == baserules.RuleTypeProm {
|
||||
|
||||
// create promql rule
|
||||
pr, err := baserules.NewPromRule(
|
||||
@@ -59,9 +58,9 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
rules = append(rules, pr)
|
||||
|
||||
// create promql rule task for evalution
|
||||
task = newTask(baserules.TaskTypeProm, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
task = newTask(baserules.TaskTypeProm, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
|
||||
|
||||
} else if opts.Rule.RuleType == ruletypes.RuleTypeAnomaly {
|
||||
} else if opts.Rule.RuleType == baserules.RuleTypeAnomaly {
|
||||
// create anomaly rule
|
||||
ar, err := NewAnomalyRule(
|
||||
ruleId,
|
||||
@@ -78,10 +77,10 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
rules = append(rules, ar)
|
||||
|
||||
// create anomaly rule task for evalution
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.RuleDB)
|
||||
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
|
||||
return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, baserules.RuleTypeProm, baserules.RuleTypeThreshold)
|
||||
}
|
||||
|
||||
return task, nil
|
||||
@@ -106,12 +105,12 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
}
|
||||
|
||||
// append name to indicate this is test alert
|
||||
parsedRule.AlertName = fmt.Sprintf("%s%s", alertname, ruletypes.TestAlertPostFix)
|
||||
parsedRule.AlertName = fmt.Sprintf("%s%s", alertname, baserules.TestAlertPostFix)
|
||||
|
||||
var rule baserules.Rule
|
||||
var err error
|
||||
|
||||
if parsedRule.RuleType == ruletypes.RuleTypeThreshold {
|
||||
if parsedRule.RuleType == baserules.RuleTypeThreshold {
|
||||
|
||||
// add special labels for test alerts
|
||||
parsedRule.Annotations[labels.AlertSummaryLabel] = fmt.Sprintf("The rule threshold is set to %.4f, and the observed metric value is {{$value}}.", *parsedRule.RuleCondition.Target)
|
||||
@@ -135,7 +134,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
return 0, basemodel.BadRequest(err)
|
||||
}
|
||||
|
||||
} else if parsedRule.RuleType == ruletypes.RuleTypeProm {
|
||||
} else if parsedRule.RuleType == baserules.RuleTypeProm {
|
||||
|
||||
// create promql rule
|
||||
rule, err = baserules.NewPromRule(
|
||||
@@ -153,7 +152,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
zap.L().Error("failed to prepare a new promql rule for test", zap.String("name", rule.Name()), zap.Error(err))
|
||||
return 0, basemodel.BadRequest(err)
|
||||
}
|
||||
} else if parsedRule.RuleType == ruletypes.RuleTypeAnomaly {
|
||||
} else if parsedRule.RuleType == baserules.RuleTypeAnomaly {
|
||||
// create anomaly rule
|
||||
rule, err = NewAnomalyRule(
|
||||
alertname,
|
||||
@@ -191,9 +190,9 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
|
||||
// newTask returns an appropriate group for
|
||||
// rule type
|
||||
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc, maintenanceStore ruletypes.MaintenanceStore, orgID string) baserules.Task {
|
||||
func newTask(taskType baserules.TaskType, name string, frequency time.Duration, rules []baserules.Rule, opts *baserules.ManagerOptions, notify baserules.NotifyFunc, ruleDB baserules.RuleDB) baserules.Task {
|
||||
if taskType == baserules.TaskTypeCh {
|
||||
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify, maintenanceStore, orgID)
|
||||
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify, ruleDB)
|
||||
}
|
||||
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify, maintenanceStore, orgID)
|
||||
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify, ruleDB)
|
||||
}
|
||||
|
||||
@@ -17,15 +17,13 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
Org = "org"
|
||||
User = "user"
|
||||
CloudIntegration = "cloud_integration"
|
||||
Org = "org"
|
||||
User = "user"
|
||||
)
|
||||
|
||||
var (
|
||||
OrgReference = `("org_id") REFERENCES "organizations" ("id")`
|
||||
UserReference = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE`
|
||||
CloudIntegrationReference = `("cloud_integration_id") REFERENCES "cloud_integration" ("id") ON DELETE CASCADE`
|
||||
OrgReference = `("org_id") REFERENCES "organizations" ("id")`
|
||||
UserReference = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE`
|
||||
)
|
||||
|
||||
type dialect struct {
|
||||
@@ -213,8 +211,6 @@ func (dialect *dialect) RenameTableAndModifyModel(ctx context.Context, bun bun.I
|
||||
fkReferences = append(fkReferences, OrgReference)
|
||||
} else if reference == User && !slices.Contains(fkReferences, UserReference) {
|
||||
fkReferences = append(fkReferences, UserReference)
|
||||
} else if reference == CloudIntegration && !slices.Contains(fkReferences, CloudIntegrationReference) {
|
||||
fkReferences = append(fkReferences, CloudIntegrationReference)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
"autoprefixer": "10.4.19",
|
||||
"babel-plugin-styled-components": "^1.12.0",
|
||||
"compression-webpack-plugin": "9.0.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"copy-webpack-plugin": "^8.1.0",
|
||||
"critters-webpack-plugin": "^3.0.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
@@ -255,7 +255,6 @@
|
||||
"body-parser": "1.20.3",
|
||||
"http-proxy-middleware": "3.0.3",
|
||||
"cross-spawn": "7.0.5",
|
||||
"cookie": "^0.7.1",
|
||||
"serialize-javascript": "6.0.2"
|
||||
"cookie": "^0.7.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/save';
|
||||
|
||||
@@ -8,7 +7,7 @@ import put from './put';
|
||||
const save = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
if (props.id && !isEmpty(props.id)) {
|
||||
if (props.id && props.id > 0) {
|
||||
return put({ ...props });
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ export default function ChatSupportGateway(): JSX.Element {
|
||||
},
|
||||
);
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const handleAddCreditCard = (): void => {
|
||||
logEvent('Add Credit card modal: Clicked', {
|
||||
source: `intercom icon`,
|
||||
@@ -53,7 +52,7 @@ export default function ChatSupportGateway(): JSX.Element {
|
||||
});
|
||||
|
||||
updateCreditCard({
|
||||
url: window.location.origin,
|
||||
url: window.location.href,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ function LaunchChatSupport({
|
||||
});
|
||||
|
||||
updateCreditCard({
|
||||
url: window.location.origin,
|
||||
url: window.location.href,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
VerticalAlignTopOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { cloneDeep, isFunction } from 'lodash-es';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
@@ -69,14 +68,10 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
<section className="header">
|
||||
<section className="left-actions">
|
||||
<FilterOutlined />
|
||||
<Typography.Text className="text">
|
||||
{lastQueryName ? 'Filters for' : 'Filters'}
|
||||
</Typography.Text>
|
||||
{lastQueryName && (
|
||||
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
||||
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Typography.Text className="text">Filters for</Typography.Text>
|
||||
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
||||
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
||||
</Tooltip>
|
||||
</section>
|
||||
|
||||
<section className="right-actions">
|
||||
@@ -94,33 +89,31 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
</section>
|
||||
)}
|
||||
|
||||
<TypicalOverlayScrollbar>
|
||||
<section className="filters">
|
||||
{config.map((filter) => {
|
||||
switch (filter.type) {
|
||||
case FiltersType.CHECKBOX:
|
||||
return (
|
||||
<Checkbox
|
||||
source={source}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
);
|
||||
case FiltersType.SLIDER:
|
||||
return <Slider filter={filter} />;
|
||||
// eslint-disable-next-line sonarjs/no-duplicated-branches
|
||||
default:
|
||||
return (
|
||||
<Checkbox
|
||||
source={source}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</section>
|
||||
</TypicalOverlayScrollbar>
|
||||
<section className="filters">
|
||||
{config.map((filter) => {
|
||||
switch (filter.type) {
|
||||
case FiltersType.CHECKBOX:
|
||||
return (
|
||||
<Checkbox
|
||||
source={source}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
);
|
||||
case FiltersType.SLIDER:
|
||||
return <Slider filter={filter} />;
|
||||
// eslint-disable-next-line sonarjs/no-duplicated-branches
|
||||
default:
|
||||
return (
|
||||
<Checkbox
|
||||
source={source}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
|
||||
import QuickFilters from '../QuickFilters';
|
||||
import { QuickFiltersSource } from '../types';
|
||||
import { QuickFiltersConfig } from './constants';
|
||||
|
||||
// Mock the useQueryBuilder hook
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: jest.fn(),
|
||||
}));
|
||||
// Mock the useGetAggregateValues hook
|
||||
jest.mock('hooks/queryBuilder/useGetAggregateValues', () => ({
|
||||
useGetAggregateValues: jest.fn(),
|
||||
}));
|
||||
|
||||
const handleFilterVisibilityChange = jest.fn();
|
||||
const redirectWithQueryBuilderData = jest.fn();
|
||||
|
||||
function TestQuickFilters(): JSX.Element {
|
||||
return (
|
||||
<MockQueryClientProvider>
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.EXCEPTIONS}
|
||||
config={QuickFiltersConfig}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
/>
|
||||
</MockQueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('Quick Filters', () => {
|
||||
beforeEach(() => {
|
||||
// Provide a mock implementation for useQueryBuilder
|
||||
(useQueryBuilder as jest.Mock).mockReturnValue({
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
queryName: 'Test Query',
|
||||
filters: { items: [{ key: 'test', value: 'value' }] },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
lastUsedQuery: 0,
|
||||
redirectWithQueryBuilderData,
|
||||
});
|
||||
|
||||
// Provide a mock implementation for useGetAggregateValues
|
||||
(useGetAggregateValues as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'success',
|
||||
payload: {
|
||||
stringAttributeValues: [
|
||||
'mq-kafka',
|
||||
'otel-demo',
|
||||
'otlp-python',
|
||||
'sample-flask',
|
||||
],
|
||||
numberAttributeValues: null,
|
||||
boolAttributeValues: null,
|
||||
},
|
||||
}, // Mocked API response
|
||||
isLoading: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders correctly with default props', () => {
|
||||
const { container } = render(<TestQuickFilters />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('displays the correct query name in the header', () => {
|
||||
render(<TestQuickFilters />);
|
||||
expect(screen.getByText('Filters for')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Query')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should add filter data to query when checkbox is clicked', () => {
|
||||
render(<TestQuickFilters />);
|
||||
const checkbox = screen.getByText('mq-kafka');
|
||||
fireEvent.click(checkbox);
|
||||
expect(redirectWithQueryBuilderData).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
builder: {
|
||||
queryData: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
filters: expect.objectContaining({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({
|
||||
key: 'deployment.environment',
|
||||
}),
|
||||
value: 'mq-kafka',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
}),
|
||||
); // sets composite query param
|
||||
});
|
||||
});
|
||||
@@ -1,382 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Quick Filters renders correctly with default props 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="quick-filters"
|
||||
>
|
||||
<section
|
||||
class="header"
|
||||
>
|
||||
<section
|
||||
class="left-actions"
|
||||
>
|
||||
<span
|
||||
aria-label="filter"
|
||||
class="anticon anticon-filter"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="filter"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M880.1 154H143.9c-24.5 0-39.8 26.7-27.5 48L349 597.4V838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V597.4L907.7 202c12.2-21.3-3.1-48-27.6-48zM603.4 798H420.6V642h182.9v156zm9.6-236.6l-9.5 16.6h-183l-9.5-16.6L212.7 226h598.6L613 561.4z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="ant-typography text css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
Filters for
|
||||
</span>
|
||||
<span
|
||||
class="ant-typography sync-tag css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
Test Query
|
||||
</span>
|
||||
</section>
|
||||
<section
|
||||
class="right-actions"
|
||||
>
|
||||
<span
|
||||
aria-label="sync"
|
||||
class="anticon anticon-sync sync-icon"
|
||||
role="img"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="sync"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M168 504.2c1-43.7 10-86.1 26.9-126 17.3-41 42.1-77.7 73.7-109.4S337 212.3 378 195c42.4-17.9 87.4-27 133.9-27s91.5 9.1 133.8 27A341.5 341.5 0 01755 268.8c9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47a8 8 0 003 14.1l175.7 43c5 1.2 9.9-2.6 9.9-7.7l.8-180.9c0-6.7-7.7-10.5-12.9-6.3l-56.4 44.1C765.8 155.1 646.2 92 511.8 92 282.7 92 96.3 275.6 92 503.8a8 8 0 008 8.2h60c4.4 0 7.9-3.5 8-7.8zm756 7.8h-60c-4.4 0-7.9 3.5-8 7.8-1 43.7-10 86.1-26.9 126-17.3 41-42.1 77.8-73.7 109.4A342.45 342.45 0 01512.1 856a342.24 342.24 0 01-243.2-100.8c-9.9-9.9-19.2-20.4-27.8-31.4l60.2-47a8 8 0 00-3-14.1l-175.7-43c-5-1.2-9.9 2.6-9.9 7.7l-.7 181c0 6.7 7.7 10.5 12.9 6.3l56.4-44.1C258.2 868.9 377.8 932 512.2 932c229.2 0 415.5-183.7 419.8-411.8a8 8 0 00-8-8.2z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<div
|
||||
class="divider-filter"
|
||||
/>
|
||||
<span
|
||||
aria-label="vertical-align-top"
|
||||
class="anticon anticon-vertical-align-top"
|
||||
role="img"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="vertical-align-top"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
style="transform: rotate(270deg);"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M859.9 168H164.1c-4.5 0-8.1 3.6-8.1 8v60c0 4.4 3.6 8 8.1 8h695.8c4.5 0 8.1-3.6 8.1-8v-60c0-4.4-3.6-8-8.1-8zM518.3 355a8 8 0 00-12.6 0l-112 141.7a7.98 7.98 0 006.3 12.9h73.9V848c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V509.7H624c6.7 0 10.4-7.7 6.3-12.9L518.3 355z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</section>
|
||||
</section>
|
||||
<div
|
||||
class="overlay-scrollbar"
|
||||
data-overlayscrollbars-initialize="true"
|
||||
>
|
||||
<div
|
||||
data-overlayscrollbars-contents=""
|
||||
>
|
||||
<section
|
||||
class="filters"
|
||||
>
|
||||
<div
|
||||
class="checkbox-filter"
|
||||
>
|
||||
<section
|
||||
class="filter-header-checkbox"
|
||||
>
|
||||
<section
|
||||
class="left-action"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-chevron-down"
|
||||
cursor="pointer"
|
||||
fill="none"
|
||||
height="13"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="13"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m6 9 6 6 6-6"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="ant-typography title css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
Environment
|
||||
</span>
|
||||
</section>
|
||||
<section
|
||||
class="right-action"
|
||||
>
|
||||
<span
|
||||
class="ant-typography clear-all css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
Clear All
|
||||
</span>
|
||||
</section>
|
||||
</section>
|
||||
<section
|
||||
class="search"
|
||||
>
|
||||
<input
|
||||
class="ant-input css-dev-only-do-not-override-2i2tap"
|
||||
placeholder="Filter values"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</section>
|
||||
<section
|
||||
class="values"
|
||||
>
|
||||
<div
|
||||
class="value"
|
||||
>
|
||||
<label
|
||||
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked check-box css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
<span
|
||||
class="ant-checkbox ant-wave-target css-dev-only-do-not-override-2i2tap ant-checkbox-checked"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="ant-checkbox-input"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span
|
||||
class="ant-checkbox-inner"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="checkbox-value-section"
|
||||
>
|
||||
<span
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line value-string css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
mq-kafka
|
||||
</span>
|
||||
<button
|
||||
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text only-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Only
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text toggle-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Toggle
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="value"
|
||||
>
|
||||
<label
|
||||
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked check-box css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
<span
|
||||
class="ant-checkbox ant-wave-target css-dev-only-do-not-override-2i2tap ant-checkbox-checked"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="ant-checkbox-input"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span
|
||||
class="ant-checkbox-inner"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="checkbox-value-section"
|
||||
>
|
||||
<span
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line value-string css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
otel-demo
|
||||
</span>
|
||||
<button
|
||||
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text only-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Only
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text toggle-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Toggle
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="value"
|
||||
>
|
||||
<label
|
||||
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked check-box css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
<span
|
||||
class="ant-checkbox ant-wave-target css-dev-only-do-not-override-2i2tap ant-checkbox-checked"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="ant-checkbox-input"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span
|
||||
class="ant-checkbox-inner"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="checkbox-value-section"
|
||||
>
|
||||
<span
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line value-string css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
otlp-python
|
||||
</span>
|
||||
<button
|
||||
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text only-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Only
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text toggle-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Toggle
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="value"
|
||||
>
|
||||
<label
|
||||
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked check-box css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
<span
|
||||
class="ant-checkbox ant-wave-target css-dev-only-do-not-override-2i2tap ant-checkbox-checked"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="ant-checkbox-input"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span
|
||||
class="ant-checkbox-inner"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="checkbox-value-section"
|
||||
>
|
||||
<span
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line value-string css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
sample-flask
|
||||
</span>
|
||||
<button
|
||||
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text only-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Only
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text toggle-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Toggle
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div
|
||||
class="checkbox-filter"
|
||||
>
|
||||
<section
|
||||
class="filter-header-checkbox"
|
||||
>
|
||||
<section
|
||||
class="left-action"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-chevron-right"
|
||||
cursor="pointer"
|
||||
fill="none"
|
||||
height="13"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="13"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m9 18 6-6-6-6"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="ant-typography title css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
Service Name
|
||||
</span>
|
||||
</section>
|
||||
<section
|
||||
class="right-action"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,30 +0,0 @@
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { FiltersType } from '../types';
|
||||
|
||||
export const QuickFiltersConfig = [
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Environment',
|
||||
attributeKey: {
|
||||
key: 'deployment.environment',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Service Name',
|
||||
attributeKey: {
|
||||
key: 'service.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
];
|
||||
@@ -40,5 +40,4 @@ export enum QuickFiltersSource {
|
||||
INFRA_MONITORING = 'infra-monitoring',
|
||||
TRACES_EXPLORER = 'traces-explorer',
|
||||
API_MONITORING = 'api-monitoring',
|
||||
EXCEPTIONS = 'exceptions',
|
||||
}
|
||||
|
||||
@@ -27,5 +27,4 @@ export enum LOCALSTORAGE {
|
||||
CELERY_OVERVIEW_COLUMNS = 'CELERY_OVERVIEW_COLUMNS',
|
||||
DONT_SHOW_SLOW_API_WARNING = 'DONT_SHOW_SLOW_API_WARNING',
|
||||
METRICS_LIST_OPTIONS = 'METRICS_LIST_OPTIONS',
|
||||
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
|
||||
}
|
||||
|
||||
@@ -398,23 +398,6 @@ export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
|
||||
],
|
||||
};
|
||||
|
||||
export enum OperatorConfigKeys {
|
||||
'EXCEPTIONS' = 'EXCEPTIONS',
|
||||
}
|
||||
|
||||
export const OPERATORS_CONFIG = {
|
||||
[OperatorConfigKeys.EXCEPTIONS]: [
|
||||
OPERATORS['='],
|
||||
OPERATORS['!='],
|
||||
OPERATORS.IN,
|
||||
OPERATORS.NIN,
|
||||
OPERATORS.EXISTS,
|
||||
OPERATORS.NOT_EXISTS,
|
||||
OPERATORS.CONTAINS,
|
||||
OPERATORS.NOT_CONTAINS,
|
||||
],
|
||||
};
|
||||
|
||||
export const HAVING_OPERATORS: string[] = [
|
||||
OPERATORS['='],
|
||||
OPERATORS['!='],
|
||||
|
||||
@@ -16,51 +16,3 @@ export const OperatorConversions: Array<{
|
||||
traceValue: 'NotIn',
|
||||
},
|
||||
];
|
||||
|
||||
// mapping from qb to exceptions
|
||||
export const CompositeQueryOperatorsConfig: Array<{
|
||||
label: string;
|
||||
metricValue: string;
|
||||
traceValue: OperatorValues;
|
||||
}> = [
|
||||
{
|
||||
label: 'in',
|
||||
metricValue: '=~',
|
||||
traceValue: 'In',
|
||||
},
|
||||
{
|
||||
label: 'nin',
|
||||
metricValue: '!~',
|
||||
traceValue: 'NotIn',
|
||||
},
|
||||
{
|
||||
label: '=',
|
||||
metricValue: '=',
|
||||
traceValue: 'Equals',
|
||||
},
|
||||
{
|
||||
label: '!=',
|
||||
metricValue: '!=',
|
||||
traceValue: 'NotEquals',
|
||||
},
|
||||
{
|
||||
label: 'exists',
|
||||
metricValue: '=~',
|
||||
traceValue: 'Exists',
|
||||
},
|
||||
{
|
||||
label: 'nexists',
|
||||
metricValue: '!~',
|
||||
traceValue: 'NotExists',
|
||||
},
|
||||
{
|
||||
label: 'contains',
|
||||
metricValue: '=~',
|
||||
traceValue: 'Contains',
|
||||
},
|
||||
{
|
||||
label: 'ncontains',
|
||||
metricValue: '!~',
|
||||
traceValue: 'NotContains',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -18,17 +18,16 @@ import getErrorCounts from 'api/errors/getErrorCounts';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||
import { convertCompositeQueryToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
||||
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
||||
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import history from 'lib/history';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -110,11 +109,10 @@ function AllErrors(): JSX.Element {
|
||||
);
|
||||
|
||||
const { queries } = useResourceAttribute();
|
||||
const compositeData = useGetCompositeQueryParam();
|
||||
|
||||
const [{ isLoading, data }, errorCountResponse] = useQueries([
|
||||
{
|
||||
queryKey: ['getAllErrors', updatedPath, maxTime, minTime, compositeData],
|
||||
queryKey: ['getAllErrors', updatedPath, maxTime, minTime, queries],
|
||||
queryFn: (): Promise<SuccessResponse<PayloadProps> | ErrorResponse> =>
|
||||
getAll({
|
||||
end: maxTime,
|
||||
@@ -125,9 +123,7 @@ function AllErrors(): JSX.Element {
|
||||
orderParam: getUpdatedParams,
|
||||
exceptionType: getUpdatedExceptionType,
|
||||
serviceName: getUpdatedServiceName,
|
||||
tags: convertCompositeQueryToTraceSelectedTags(
|
||||
compositeData?.builder.queryData?.[0]?.filters.items,
|
||||
),
|
||||
tags: convertRawQueriesToTraceSelectedTags(queries),
|
||||
}),
|
||||
enabled: !loading,
|
||||
},
|
||||
@@ -138,7 +134,7 @@ function AllErrors(): JSX.Element {
|
||||
minTime,
|
||||
getUpdatedExceptionType,
|
||||
getUpdatedServiceName,
|
||||
compositeData,
|
||||
queries,
|
||||
],
|
||||
queryFn: (): Promise<ErrorResponse | SuccessResponse<number>> =>
|
||||
getErrorCounts({
|
||||
@@ -146,9 +142,7 @@ function AllErrors(): JSX.Element {
|
||||
start: minTime,
|
||||
exceptionType: getUpdatedExceptionType,
|
||||
serviceName: getUpdatedServiceName,
|
||||
tags: convertCompositeQueryToTraceSelectedTags(
|
||||
compositeData?.builder.queryData?.[0]?.filters.items,
|
||||
),
|
||||
tags: convertRawQueriesToTraceSelectedTags(queries),
|
||||
}),
|
||||
enabled: !loading,
|
||||
},
|
||||
@@ -435,8 +429,12 @@ function AllErrors(): JSX.Element {
|
||||
[pathname],
|
||||
);
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!isUndefined(errorCountResponse.data?.payload)) {
|
||||
if (
|
||||
!logEventCalledRef.current &&
|
||||
!isUndefined(errorCountResponse.data?.payload)
|
||||
) {
|
||||
const selectedEnvironments = queries.find(
|
||||
(val) => val.tagKey === 'resource_deployment_environment',
|
||||
)?.tagValue;
|
||||
@@ -444,12 +442,9 @@ function AllErrors(): JSX.Element {
|
||||
logEvent('Exception: List page visited', {
|
||||
numberOfExceptions: errorCountResponse?.data?.payload,
|
||||
selectedEnvironments,
|
||||
resourceAttributeUsed: !!compositeData?.builder.queryData?.[0]?.filters
|
||||
.items?.length,
|
||||
tags: convertCompositeQueryToTraceSelectedTags(
|
||||
compositeData?.builder.queryData?.[0]?.filters.items,
|
||||
),
|
||||
resourceAttributeUsed: !!queries?.length,
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [errorCountResponse.data?.payload]);
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
import TimezoneProvider from 'providers/Timezone';
|
||||
import { Provider, useSelector } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import store from 'store';
|
||||
|
||||
import AllErrors from '../index';
|
||||
import {
|
||||
INIT_URL_WITH_COMMON_QUERY,
|
||||
MOCK_ERROR_LIST,
|
||||
TAG_FROM_QUERY,
|
||||
} from './constants';
|
||||
|
||||
jest.mock('hooks/useResourceAttribute', () =>
|
||||
jest.fn(() => ({
|
||||
queries: [],
|
||||
})),
|
||||
);
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
function Exceptions({ initUrl }: { initUrl?: string[] }): JSX.Element {
|
||||
return (
|
||||
<MemoryRouter initialEntries={initUrl ?? ['/exceptions']}>
|
||||
<TimezoneProvider>
|
||||
<Provider store={store}>
|
||||
<MockQueryClientProvider>
|
||||
<AllErrors />
|
||||
</MockQueryClientProvider>
|
||||
</Provider>
|
||||
</TimezoneProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
Exceptions.defaultProps = {
|
||||
initUrl: ['/exceptions'],
|
||||
};
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL;
|
||||
const listErrorsURL = `${BASE_URL}/api/v1/listErrors`;
|
||||
const countErrorsURL = `${BASE_URL}/api/v1/countErrors`;
|
||||
|
||||
const postListErrorsSpy = jest.fn();
|
||||
|
||||
describe('Exceptions - All Errors', () => {
|
||||
beforeEach(() => {
|
||||
(useSelector as jest.Mock).mockReturnValue({
|
||||
maxTime: 1000,
|
||||
minTime: 0,
|
||||
loading: false,
|
||||
});
|
||||
server.use(
|
||||
rest.post(listErrorsURL, async (req, res, ctx) => {
|
||||
const body = await req.json();
|
||||
postListErrorsSpy(body);
|
||||
return res(ctx.status(200), ctx.json(MOCK_ERROR_LIST));
|
||||
}),
|
||||
);
|
||||
server.use(
|
||||
rest.post(countErrorsURL, (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(540)),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('renders correctly with default props', async () => {
|
||||
render(<Exceptions />);
|
||||
const item = await screen.findByText(/redis timeout/i);
|
||||
expect(item).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should sort Error Message appropriately', async () => {
|
||||
render(<Exceptions />);
|
||||
await screen.findByText(/redis timeout/i);
|
||||
|
||||
const caretIconUp = screen.getAllByLabelText('caret-up')[0];
|
||||
const caretIconDown = screen.getAllByLabelText('caret-down')[0];
|
||||
|
||||
// sort by ascending
|
||||
expect(caretIconUp.className).not.toContain('active');
|
||||
fireEvent.click(caretIconUp);
|
||||
expect(caretIconUp.className).toContain('active');
|
||||
let queryParams = new URLSearchParams(window.location.search);
|
||||
expect(queryParams.get('order')).toBe('ascending');
|
||||
expect(queryParams.get('orderParam')).toBe('exceptionType');
|
||||
|
||||
// sort by descending
|
||||
expect(caretIconDown.className).not.toContain('active');
|
||||
fireEvent.click(caretIconDown);
|
||||
expect(caretIconDown.className).toContain('active');
|
||||
queryParams = new URLSearchParams(window.location.search);
|
||||
expect(queryParams.get('order')).toBe('descending');
|
||||
});
|
||||
|
||||
it('should call useQueries with exact composite query object', async () => {
|
||||
render(<Exceptions initUrl={[INIT_URL_WITH_COMMON_QUERY]} />);
|
||||
await screen.findByText(/redis timeout/i);
|
||||
expect(postListErrorsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tags: TAG_FROM_QUERY,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,94 +0,0 @@
|
||||
export const MOCK_USE_QUERIES_DATA = [
|
||||
{
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
data: {
|
||||
statusCode: 200,
|
||||
payload: [
|
||||
{
|
||||
exceptionType: '*errors.errorString',
|
||||
exceptionMessage: 'redis timeout',
|
||||
exceptionCount: 2510,
|
||||
lastSeen: '2025-04-14T18:27:57.797616374Z',
|
||||
firstSeen: '2025-04-14T17:58:00.262775497Z',
|
||||
serviceName: 'redis-manual',
|
||||
groupID: '511b9c91a92b9c5166ecb77235f5743b',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
status: 'success',
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
isIdle: false,
|
||||
data: {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
payload: 525,
|
||||
},
|
||||
dataUpdatedAt: 1744661020341,
|
||||
error: null,
|
||||
errorUpdatedAt: 0,
|
||||
failureCount: 0,
|
||||
errorUpdateCount: 0,
|
||||
isFetched: true,
|
||||
isFetchedAfterMount: true,
|
||||
isFetching: false,
|
||||
isRefetching: false,
|
||||
isLoadingError: false,
|
||||
isPlaceholderData: false,
|
||||
isPreviousData: false,
|
||||
isRefetchError: false,
|
||||
isStale: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const INIT_URL_WITH_COMMON_QUERY =
|
||||
'/exceptions?compositeQuery=%257B%2522queryType%2522%253A%2522builder%2522%252C%2522builder%2522%253A%257B%2522queryData%2522%253A%255B%257B%2522dataSource%2522%253A%2522traces%2522%252C%2522queryName%2522%253A%2522A%2522%252C%2522aggregateOperator%2522%253A%2522noop%2522%252C%2522aggregateAttribute%2522%253A%257B%2522id%2522%253A%2522----resource--false%2522%252C%2522dataType%2522%253A%2522%2522%252C%2522key%2522%253A%2522%2522%252C%2522isColumn%2522%253Afalse%252C%2522type%2522%253A%2522resource%2522%252C%2522isJSON%2522%253Afalse%257D%252C%2522timeAggregation%2522%253A%2522rate%2522%252C%2522spaceAggregation%2522%253A%2522sum%2522%252C%2522functions%2522%253A%255B%255D%252C%2522filters%2522%253A%257B%2522items%2522%253A%255B%257B%2522id%2522%253A%2522db118ac7-9313-4adb-963f-f31b5b32c496%2522%252C%2522op%2522%253A%2522in%2522%252C%2522key%2522%253A%257B%2522key%2522%253A%2522deployment.environment%2522%252C%2522dataType%2522%253A%2522string%2522%252C%2522type%2522%253A%2522resource%2522%252C%2522isColumn%2522%253Afalse%252C%2522isJSON%2522%253Afalse%257D%252C%2522value%2522%253A%2522mq-kafka%2522%257D%255D%252C%2522op%2522%253A%2522AND%2522%257D%252C%2522expression%2522%253A%2522A%2522%252C%2522disabled%2522%253Afalse%252C%2522stepInterval%2522%253A60%252C%2522having%2522%253A%255B%255D%252C%2522limit%2522%253Anull%252C%2522orderBy%2522%253A%255B%255D%252C%2522groupBy%2522%253A%255B%255D%252C%2522legend%2522%253A%2522%2522%252C%2522reduceTo%2522%253A%2522avg%2522%257D%255D%252C%2522queryFormulas%2522%253A%255B%255D%257D%252C%2522promql%2522%253A%255B%257B%2522name%2522%253A%2522A%2522%252C%2522query%2522%253A%2522%2522%252C%2522legend%2522%253A%2522%2522%252C%2522disabled%2522%253Afalse%257D%255D%252C%2522clickhouse_sql%2522%253A%255B%257B%2522name%2522%253A%2522A%2522%252C%2522legend%2522%253A%2522%2522%252C%2522disabled%2522%253Afalse%252C%2522query%2522%253A%2522%2522%257D%255D%252C%2522id%2522%253A%2522dd576d04-0822-476d-b0c2-807a7af2e5e7%2522%257D';
|
||||
|
||||
export const extractCompositeQueryObject = (
|
||||
url: string,
|
||||
): Record<string, unknown> | null => {
|
||||
try {
|
||||
const urlObj = new URL(`http://dummy-base${url}`); // Add dummy base to parse relative URL
|
||||
const encodedParam = urlObj.searchParams.get('compositeQuery');
|
||||
|
||||
if (!encodedParam) return null;
|
||||
|
||||
// Decode twice
|
||||
const firstDecode = decodeURIComponent(encodedParam);
|
||||
const secondDecode = decodeURIComponent(firstDecode);
|
||||
|
||||
// Parse JSON
|
||||
return JSON.parse(secondDecode);
|
||||
} catch (err) {
|
||||
console.error('Failed to extract compositeQuery:', err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const TAG_FROM_QUERY = [
|
||||
{
|
||||
BoolValues: [],
|
||||
Key: 'deployment.environment',
|
||||
NumberValues: [],
|
||||
Operator: 'In',
|
||||
StringValues: ['mq-kafka'],
|
||||
TagType: 'ResourceAttribute',
|
||||
},
|
||||
];
|
||||
|
||||
export const MOCK_ERROR_LIST = [
|
||||
{
|
||||
exceptionType: '*errors.errorString',
|
||||
exceptionMessage: 'redis timeout',
|
||||
exceptionCount: 2510,
|
||||
lastSeen: '2025-04-14T18:27:57.797616374Z',
|
||||
firstSeen: '2025-04-14T17:58:00.262775497Z',
|
||||
serviceName: 'redis-manual',
|
||||
groupID: '511b9c91a92b9c5166ecb77235f5743b',
|
||||
},
|
||||
];
|
||||
@@ -339,8 +339,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
|
||||
const isApiMonitoringView = (): boolean => routeKey === 'API_MONITORING';
|
||||
|
||||
const isExceptionsView = (): boolean => routeKey === 'ALL_ERROR';
|
||||
|
||||
const isTracesView = (): boolean =>
|
||||
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
|
||||
|
||||
@@ -663,8 +661,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
isMessagingQueues() ||
|
||||
isCloudIntegrationPage() ||
|
||||
isInfraMonitoring() ||
|
||||
isApiMonitoringView() ||
|
||||
isExceptionsView()
|
||||
isApiMonitoringView()
|
||||
? 0
|
||||
: '0 1rem',
|
||||
|
||||
|
||||
@@ -333,7 +333,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
});
|
||||
|
||||
updateCreditCard({
|
||||
url: window.location.origin,
|
||||
url: window.location.href,
|
||||
});
|
||||
} else {
|
||||
logEvent('Billing : Manage Billing', {
|
||||
@@ -342,7 +342,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
});
|
||||
|
||||
manageCreditCard({
|
||||
url: window.location.origin,
|
||||
url: window.location.href,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@@ -121,7 +121,7 @@ function CreateRules(): JSX.Element {
|
||||
alertType={alertType}
|
||||
formInstance={formInstance}
|
||||
initialValue={initValues}
|
||||
ruleId=""
|
||||
ruleId={0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
|
||||
|
||||
interface EditRulesProps {
|
||||
initialValue: AlertDef;
|
||||
ruleId: string;
|
||||
ruleId: number;
|
||||
}
|
||||
|
||||
export default EditRules;
|
||||
|
||||
@@ -11,7 +11,6 @@ import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
|
||||
import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { Atom, Play, Terminal } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -157,7 +156,7 @@ function QuerySection({
|
||||
runQuery();
|
||||
logEvent('Alert: Stage and run query', {
|
||||
dataSource: ALERTS_DATA_SOURCE_MAP[alertType],
|
||||
isNewRule: !ruleId || isEmpty(ruleId),
|
||||
isNewRule: !ruleId || ruleId === 0,
|
||||
ruleId,
|
||||
queryType: queryCategory,
|
||||
});
|
||||
@@ -231,7 +230,7 @@ interface QuerySectionProps {
|
||||
runQuery: VoidFunction;
|
||||
alertDef: AlertDef;
|
||||
panelType: PANEL_TYPES;
|
||||
ruleId: string;
|
||||
ruleId: number;
|
||||
}
|
||||
|
||||
export default QuerySection;
|
||||
|
||||
@@ -21,7 +21,7 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||
import { isEmpty, isEqual } from 'lodash-es';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { BellDot, ExternalLink } from 'lucide-react';
|
||||
import Tabs2 from 'periscope/components/Tabs2';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
@@ -121,7 +121,7 @@ function FormAlertRules({
|
||||
// use query client
|
||||
const ruleCache = useQueryClient();
|
||||
|
||||
const isNewRule = !ruleId || isEmpty(ruleId);
|
||||
const isNewRule = ruleId === 0;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [queryStatus, setQueryStatus] = useState<string>('');
|
||||
@@ -481,7 +481,7 @@ function FormAlertRules({
|
||||
|
||||
try {
|
||||
const apiReq =
|
||||
ruleId && !isEmpty(ruleId)
|
||||
ruleId && ruleId > 0
|
||||
? { data: postableAlert, id: ruleId }
|
||||
: { data: postableAlert };
|
||||
|
||||
@@ -491,7 +491,7 @@ function FormAlertRules({
|
||||
logData = {
|
||||
status: 'success',
|
||||
statusMessage:
|
||||
!ruleId || isEmpty(ruleId) ? t('rule_created') : t('rule_edited'),
|
||||
!ruleId || ruleId === 0 ? t('rule_created') : t('rule_edited'),
|
||||
};
|
||||
|
||||
notifications.success({
|
||||
@@ -543,7 +543,7 @@ function FormAlertRules({
|
||||
dataSource: ALERTS_DATA_SOURCE_MAP[postableAlert?.alertType as AlertTypes],
|
||||
channelNames: postableAlert?.preferredChannels,
|
||||
broadcastToAll: postableAlert?.broadcastToAll,
|
||||
isNewRule: !ruleId || isEmpty(ruleId),
|
||||
isNewRule: !ruleId || ruleId === 0,
|
||||
ruleId,
|
||||
queryType: currentQuery.queryType,
|
||||
alertId: postableAlert?.id,
|
||||
@@ -628,7 +628,7 @@ function FormAlertRules({
|
||||
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
|
||||
channelNames: postableAlert?.preferredChannels,
|
||||
broadcastToAll: postableAlert?.broadcastToAll,
|
||||
isNewRule: !ruleId || isEmpty(ruleId),
|
||||
isNewRule: !ruleId || ruleId === 0,
|
||||
ruleId,
|
||||
queryType: currentQuery.queryType,
|
||||
status: statusResponse.status,
|
||||
@@ -700,7 +700,7 @@ function FormAlertRules({
|
||||
alertDef?.broadcastToAll ||
|
||||
(alertDef.preferredChannels && alertDef.preferredChannels.length > 0);
|
||||
|
||||
const isRuleCreated = !ruleId || isEmpty(ruleId);
|
||||
const isRuleCreated = !ruleId || ruleId === 0;
|
||||
|
||||
function handleRedirection(option: AlertTypes): void {
|
||||
let url;
|
||||
@@ -716,7 +716,7 @@ function FormAlertRules({
|
||||
if (url) {
|
||||
logEvent('Alert: Check example alert clicked', {
|
||||
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
|
||||
isNewRule: !ruleId || isEmpty(ruleId),
|
||||
isNewRule: !ruleId || ruleId === 0,
|
||||
ruleId,
|
||||
queryType: currentQuery.queryType,
|
||||
link: url,
|
||||
@@ -881,8 +881,8 @@ function FormAlertRules({
|
||||
type="default"
|
||||
onClick={onCancelHandler}
|
||||
>
|
||||
{(!ruleId || isEmpty(ruleId)) && t('button_cancelchanges')}
|
||||
{ruleId && !isEmpty(ruleId) && t('button_discard')}
|
||||
{ruleId === 0 && t('button_cancelchanges')}
|
||||
{ruleId > 0 && t('button_discard')}
|
||||
</ActionButton>
|
||||
</ButtonContainer>
|
||||
</MainFormContainer>
|
||||
@@ -899,7 +899,7 @@ interface FormAlertRuleProps {
|
||||
alertType?: AlertTypes;
|
||||
formInstance: FormInstance;
|
||||
initialValue: AlertDef;
|
||||
ruleId: string;
|
||||
ruleId: number;
|
||||
}
|
||||
|
||||
export default FormAlertRules;
|
||||
|
||||
@@ -153,9 +153,7 @@ export default function AlertRules({
|
||||
<div className="alert-rule-item-name-container home-data-item-name-container">
|
||||
<img
|
||||
src={
|
||||
Math.random() % 2 === 0
|
||||
? '/Icons/eight-ball.svg'
|
||||
: '/Icons/circus-tent.svg'
|
||||
rule.id % 2 === 0 ? '/Icons/eight-ball.svg' : '/Icons/circus-tent.svg'
|
||||
}
|
||||
alt="alert-rules"
|
||||
className="alert-rules-img"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import './Home.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Alert, Button, Popover } from 'antd';
|
||||
import { Button, Popover } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
|
||||
import { K8sPodsListPayload } from 'api/infraMonitoring/getK8sPodsList';
|
||||
@@ -644,16 +644,6 @@ export default function Home(): JSX.Element {
|
||||
</div>
|
||||
|
||||
<div className="home-right-content">
|
||||
<div className="home-notifications-container">
|
||||
<div className="notification">
|
||||
<Alert
|
||||
message="We're transitioning alert rule IDs from integers to UUIDs on April 23, 2025. Both old and new alert links will continue to work after this change - existing notifications using integer IDs will remain functional while new alerts will use the UUID format."
|
||||
type="info"
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isWelcomeChecklistSkipped && !loadingUserPreferences && (
|
||||
<AnimatePresence initial={false}>
|
||||
<Card className="checklist-card">
|
||||
|
||||
@@ -24,7 +24,7 @@ function DeleteAlert({
|
||||
|
||||
const defaultErrorMessage = 'Something went wrong';
|
||||
|
||||
const onDeleteHandler = async (id: string): Promise<void> => {
|
||||
const onDeleteHandler = async (id: number): Promise<void> => {
|
||||
try {
|
||||
const response = await deleteAlerts({
|
||||
id,
|
||||
|
||||
@@ -25,7 +25,7 @@ function ToggleAlertState({
|
||||
const defaultErrorMessage = 'Something went wrong';
|
||||
|
||||
const onToggleHandler = async (
|
||||
id: string,
|
||||
id: number,
|
||||
disabled: boolean,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,6 @@ import AddToQueryHOC, {
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
|
||||
import { FontSize, OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import history from 'lib/history';
|
||||
@@ -35,6 +34,9 @@ import FieldRenderer from './FieldRenderer';
|
||||
import { TableViewActions } from './TableView/TableViewActions';
|
||||
import { filterKeyForField, findKeyPath, flattenObject } from './utils';
|
||||
|
||||
// Fields which should be restricted from adding it to query
|
||||
const RESTRICTED_FIELDS = ['timestamp'];
|
||||
|
||||
interface TableViewProps {
|
||||
logData: ILog;
|
||||
fieldSearchInput: string;
|
||||
@@ -247,7 +249,7 @@ function TableView({
|
||||
}
|
||||
|
||||
const fieldFilterKey = filterKeyForField(field);
|
||||
if (!RESTRICTED_SELECTED_FIELDS.includes(fieldFilterKey)) {
|
||||
if (!RESTRICTED_FIELDS.includes(fieldFilterKey)) {
|
||||
return (
|
||||
<AddToQueryHOC
|
||||
fieldKey={fieldFilterKey}
|
||||
|
||||
@@ -9,7 +9,6 @@ import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
|
||||
import dompurify from 'dompurify';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { ArrowDownToDot, ArrowUpFromDot, Ellipsis } from 'lucide-react';
|
||||
@@ -143,7 +142,7 @@ export function TableViewActions(
|
||||
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
|
||||
{renderFieldContent()}
|
||||
</CopyClipboardHOC>
|
||||
{!isListViewPanel && !RESTRICTED_SELECTED_FIELDS.includes(fieldFilterKey) && (
|
||||
{!isListViewPanel && (
|
||||
<span className="action-btn">
|
||||
<Tooltip title="Filter for value">
|
||||
<Button
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
|
||||
|
||||
import { TableViewActions } from '../TableViewActions';
|
||||
|
||||
// Mock the components and hooks
|
||||
jest.mock('components/Logs/CopyClipboardHOC', () => ({
|
||||
__esModule: true,
|
||||
default: ({ children }: { children: React.ReactNode }): JSX.Element => (
|
||||
<div className="CopyClipboardHOC">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('providers/Timezone', () => ({
|
||||
useTimezone: (): {
|
||||
formatTimezoneAdjustedTimestamp: (timestamp: string) => string;
|
||||
} => ({
|
||||
formatTimezoneAdjustedTimestamp: (timestamp: string): string => timestamp,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useLocation: (): {
|
||||
pathname: string;
|
||||
search: string;
|
||||
hash: string;
|
||||
state: null;
|
||||
} => ({
|
||||
pathname: '/test',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('TableViewActions', () => {
|
||||
const TEST_VALUE = 'test value';
|
||||
const ACTION_BUTTON_TEST_ID = '.action-btn';
|
||||
const defaultProps = {
|
||||
fieldData: {
|
||||
field: 'test-field',
|
||||
value: TEST_VALUE,
|
||||
},
|
||||
record: {
|
||||
key: 'test-key',
|
||||
field: 'test-field',
|
||||
value: TEST_VALUE,
|
||||
},
|
||||
isListViewPanel: false,
|
||||
isfilterInLoading: false,
|
||||
isfilterOutLoading: false,
|
||||
onClickHandler: jest.fn(),
|
||||
onGroupByAttribute: jest.fn(),
|
||||
};
|
||||
|
||||
it('should render without crashing', () => {
|
||||
render(
|
||||
<TableViewActions
|
||||
fieldData={defaultProps.fieldData}
|
||||
record={defaultProps.record}
|
||||
isListViewPanel={defaultProps.isListViewPanel}
|
||||
isfilterInLoading={defaultProps.isfilterInLoading}
|
||||
isfilterOutLoading={defaultProps.isfilterOutLoading}
|
||||
onClickHandler={defaultProps.onClickHandler}
|
||||
onGroupByAttribute={defaultProps.onGroupByAttribute}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText(TEST_VALUE)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render action buttons for restricted fields', () => {
|
||||
RESTRICTED_SELECTED_FIELDS.forEach((field) => {
|
||||
const { container } = render(
|
||||
<TableViewActions
|
||||
fieldData={{
|
||||
...defaultProps.fieldData,
|
||||
field,
|
||||
}}
|
||||
record={{
|
||||
...defaultProps.record,
|
||||
field,
|
||||
}}
|
||||
isListViewPanel={defaultProps.isListViewPanel}
|
||||
isfilterInLoading={defaultProps.isfilterInLoading}
|
||||
isfilterOutLoading={defaultProps.isfilterOutLoading}
|
||||
onClickHandler={defaultProps.onClickHandler}
|
||||
onGroupByAttribute={defaultProps.onGroupByAttribute}
|
||||
/>,
|
||||
);
|
||||
// Verify that action buttons are not rendered for restricted fields
|
||||
expect(
|
||||
container.querySelector(ACTION_BUTTON_TEST_ID),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render action buttons for non-restricted fields', () => {
|
||||
const { container } = render(
|
||||
<TableViewActions
|
||||
fieldData={defaultProps.fieldData}
|
||||
record={defaultProps.record}
|
||||
isListViewPanel={defaultProps.isListViewPanel}
|
||||
isfilterInLoading={defaultProps.isfilterInLoading}
|
||||
isfilterOutLoading={defaultProps.isfilterOutLoading}
|
||||
onClickHandler={defaultProps.onClickHandler}
|
||||
onGroupByAttribute={defaultProps.onGroupByAttribute}
|
||||
/>,
|
||||
);
|
||||
// Verify that action buttons are rendered for non-restricted fields
|
||||
expect(container.querySelector(ACTION_BUTTON_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render action buttons in list view panel', () => {
|
||||
const { container } = render(
|
||||
<TableViewActions
|
||||
fieldData={defaultProps.fieldData}
|
||||
record={defaultProps.record}
|
||||
isListViewPanel
|
||||
isfilterInLoading={defaultProps.isfilterInLoading}
|
||||
isfilterOutLoading={defaultProps.isfilterOutLoading}
|
||||
onClickHandler={defaultProps.onClickHandler}
|
||||
onGroupByAttribute={defaultProps.onGroupByAttribute}
|
||||
/>,
|
||||
);
|
||||
// Verify that action buttons are not rendered in list view panel
|
||||
expect(
|
||||
container.querySelector(ACTION_BUTTON_TEST_ID),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -138,7 +138,7 @@ export const deleteDowntimeHandler = ({
|
||||
export const createEditDowntimeSchedule = async (
|
||||
props: DowntimeScheduleUpdatePayload,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
if (props.id) {
|
||||
if (props.id && props.id > 0) {
|
||||
return updateDowntimeSchedule({ ...props });
|
||||
}
|
||||
return createDowntimeSchedule({ ...props.data });
|
||||
|
||||
@@ -453,7 +453,7 @@ export const Query = memo(function Query({
|
||||
</Col>
|
||||
)}
|
||||
<Col flex="1" className="qb-search-container">
|
||||
{[DataSource.LOGS, DataSource.TRACES].includes(query.dataSource) ? (
|
||||
{query.dataSource === DataSource.LOGS ? (
|
||||
<QueryBuilderSearchV2
|
||||
query={query}
|
||||
onChange={handleChangeTagFilters}
|
||||
|
||||
@@ -120,7 +120,6 @@ function SpanScopeSelector({ queryName }: SpanScopeSelectorProps): JSX.Element {
|
||||
<Select
|
||||
value={selectedScope}
|
||||
className="span-scope-selector"
|
||||
data-testid="span-scope-selector"
|
||||
onChange={handleScopeChange}
|
||||
options={SELECT_OPTIONS}
|
||||
/>
|
||||
@@ -56,6 +56,7 @@ import { PLACEHOLDER } from './constant';
|
||||
import ExampleQueriesRendererForLogs from './ExampleQueriesRendererForLogs';
|
||||
import OptionRenderer from './OptionRenderer';
|
||||
import OptionRendererForLogs from './OptionRendererForLogs';
|
||||
import SpanScopeSelector from './SpanScopeSelector';
|
||||
import { StyledCheckOutlined, TypographyText } from './style';
|
||||
import {
|
||||
convertExampleQueriesToOptions,
|
||||
@@ -83,6 +84,11 @@ function QueryBuilderSearch({
|
||||
pathname,
|
||||
]);
|
||||
|
||||
const isTracesExplorerPage = useMemo(
|
||||
() => pathname === ROUTES.TRACES_EXPLORER,
|
||||
[pathname],
|
||||
);
|
||||
|
||||
const [isEditingTag, setIsEditingTag] = useState(false);
|
||||
|
||||
const {
|
||||
@@ -483,6 +489,7 @@ function QueryBuilderSearch({
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
{isTracesExplorerPage && <SpanScopeSelector queryName={query.queryName} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import './QueryBuilderSearchV2.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
@@ -26,7 +25,6 @@ interface ICustomDropdownProps {
|
||||
exampleQueries: TagFilter[];
|
||||
onChange: (value: TagFilter) => void;
|
||||
currentFilterItem?: ITag;
|
||||
isLogsDataSource: boolean;
|
||||
}
|
||||
|
||||
export default function QueryBuilderSearchDropdown(
|
||||
@@ -40,14 +38,11 @@ export default function QueryBuilderSearchDropdown(
|
||||
exampleQueries,
|
||||
options,
|
||||
onChange,
|
||||
isLogsDataSource,
|
||||
} = props;
|
||||
const userOs = getUserOperatingSystem();
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cx('content', { 'non-logs-data-source': !isLogsDataSource })}
|
||||
>
|
||||
<div className="content">
|
||||
{!currentFilterItem?.key ? (
|
||||
<div className="suggested-filters">Suggested Filters</div>
|
||||
) : !currentFilterItem?.op ? (
|
||||
|
||||
@@ -11,11 +11,6 @@
|
||||
.rc-virtual-list-holder {
|
||||
height: 115px;
|
||||
}
|
||||
&.non-logs-data-source {
|
||||
.rc-virtual-list-holder {
|
||||
height: 256px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Select, Spin, Tag, Tooltip } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY,
|
||||
OperatorConfigKeys,
|
||||
OPERATORS,
|
||||
QUERY_BUILDER_OPERATORS_BY_TYPES,
|
||||
QUERY_BUILDER_SEARCH_VALUES,
|
||||
@@ -63,9 +62,7 @@ import {
|
||||
getTagToken,
|
||||
isInNInOperator,
|
||||
} from '../QueryBuilderSearch/utils';
|
||||
import { filterByOperatorConfig } from '../utils';
|
||||
import QueryBuilderSearchDropdown from './QueryBuilderSearchDropdown';
|
||||
import SpanScopeSelector from './SpanScopeSelector';
|
||||
import Suggestions from './Suggestions';
|
||||
|
||||
export interface ITag {
|
||||
@@ -91,7 +88,6 @@ interface QueryBuilderSearchV2Props {
|
||||
className?: string;
|
||||
suffixIcon?: React.ReactNode;
|
||||
hardcodedAttributeKeys?: BaseAutocompleteData[];
|
||||
operatorConfigKey?: OperatorConfigKeys;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
@@ -125,7 +121,6 @@ function QueryBuilderSearchV2(
|
||||
suffixIcon,
|
||||
whereClauseConfig,
|
||||
hardcodedAttributeKeys,
|
||||
operatorConfigKey,
|
||||
} = props;
|
||||
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
@@ -295,8 +290,7 @@ function QueryBuilderSearchV2(
|
||||
if (
|
||||
isObject(parsedValue) &&
|
||||
parsedValue?.key &&
|
||||
parsedValue?.key?.split(' ').length > 1 &&
|
||||
isLogsDataSource
|
||||
parsedValue?.key?.split(' ').length > 1
|
||||
) {
|
||||
setTags((prev) => [
|
||||
...prev,
|
||||
@@ -411,13 +405,7 @@ function QueryBuilderSearchV2(
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
currentFilterItem?.key,
|
||||
currentFilterItem?.op,
|
||||
currentState,
|
||||
isLogsDataSource,
|
||||
searchValue,
|
||||
],
|
||||
[currentFilterItem?.key, currentFilterItem?.op, currentState, searchValue],
|
||||
);
|
||||
|
||||
const handleSearch = useCallback((value: string) => {
|
||||
@@ -701,29 +689,12 @@ function QueryBuilderSearchV2(
|
||||
})),
|
||||
);
|
||||
} else {
|
||||
setDropdownOptions([
|
||||
// Add user typed option if it doesn't exist in the payload
|
||||
...(tagKey.trim().length > 0 &&
|
||||
!data?.payload?.attributeKeys?.some((val) => val.key === tagKey)
|
||||
? [
|
||||
{
|
||||
label: tagKey,
|
||||
value: {
|
||||
key: tagKey,
|
||||
dataType: DataTypes.EMPTY,
|
||||
type: '',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// Map existing attribute keys from payload
|
||||
...(data?.payload?.attributeKeys?.map((key) => ({
|
||||
setDropdownOptions(
|
||||
data?.payload?.attributeKeys?.map((key) => ({
|
||||
label: key.key,
|
||||
value: key,
|
||||
})) || []),
|
||||
]);
|
||||
})) || [],
|
||||
);
|
||||
}
|
||||
}
|
||||
if (currentState === DropdownState.OPERATOR) {
|
||||
@@ -746,11 +717,15 @@ function QueryBuilderSearchV2(
|
||||
op.label.startsWith(partialOperator.toLocaleUpperCase()),
|
||||
);
|
||||
}
|
||||
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
|
||||
setDropdownOptions(operatorOptions);
|
||||
} else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) {
|
||||
operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({
|
||||
label: operator,
|
||||
value: operator,
|
||||
}));
|
||||
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
|
||||
setDropdownOptions(operatorOptions);
|
||||
} else {
|
||||
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map(
|
||||
(operator) => ({
|
||||
@@ -764,12 +739,9 @@ function QueryBuilderSearchV2(
|
||||
op.label.startsWith(partialOperator.toLocaleUpperCase()),
|
||||
);
|
||||
}
|
||||
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
|
||||
setDropdownOptions(operatorOptions);
|
||||
}
|
||||
const filterOperatorOptions = filterByOperatorConfig(
|
||||
operatorOptions,
|
||||
operatorConfigKey,
|
||||
);
|
||||
setDropdownOptions([{ label: '', value: '' }, ...filterOperatorOptions]);
|
||||
}
|
||||
|
||||
if (currentState === DropdownState.ATTRIBUTE_VALUE) {
|
||||
@@ -802,7 +774,6 @@ function QueryBuilderSearchV2(
|
||||
isLogsDataSource,
|
||||
searchValue,
|
||||
suggestionsData?.payload?.attributes,
|
||||
operatorConfigKey,
|
||||
]);
|
||||
|
||||
// keep the query in sync with the selected tags in logs explorer page
|
||||
@@ -936,11 +907,6 @@ function QueryBuilderSearchV2(
|
||||
);
|
||||
};
|
||||
|
||||
const isTracesDataSource = useMemo(
|
||||
() => query.dataSource === DataSource.TRACES,
|
||||
[query.dataSource],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="query-builder-search-v2">
|
||||
<Select
|
||||
@@ -998,7 +964,6 @@ function QueryBuilderSearchV2(
|
||||
exampleQueries={suggestionsData?.payload?.example_queries || []}
|
||||
tags={tags}
|
||||
currentFilterItem={currentFilterItem}
|
||||
isLogsDataSource={isLogsDataSource}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
@@ -1025,7 +990,6 @@ function QueryBuilderSearchV2(
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
{isTracesDataSource && <SpanScopeSelector queryName={query.queryName} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1036,7 +1000,6 @@ QueryBuilderSearchV2.defaultProps = {
|
||||
suffixIcon: null,
|
||||
whereClauseConfig: {},
|
||||
hardcodedAttributeKeys: undefined,
|
||||
operatorConfigKey: undefined,
|
||||
};
|
||||
|
||||
export default QueryBuilderSearchV2;
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
RenderResult,
|
||||
screen,
|
||||
} from '@testing-library/react';
|
||||
import {
|
||||
initialQueriesMap,
|
||||
initialQueryBuilderFormValues,
|
||||
} from 'constants/queryBuilder';
|
||||
import { QueryBuilderContext } from 'providers/QueryBuilder';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import QueryBuilderSearchV2 from '../QueryBuilderSearchV2';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('Span scope selector', () => {
|
||||
it('should render span scope selector when data source is TRACES', () => {
|
||||
const { getByTestId } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<QueryBuilderSearchV2
|
||||
query={{
|
||||
...initialQueryBuilderFormValues,
|
||||
dataSource: DataSource.TRACES,
|
||||
}}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
expect(getByTestId('span-scope-selector')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render span scope selector for non-TRACES data sources', () => {
|
||||
const { queryByTestId } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<QueryBuilderSearchV2
|
||||
query={{
|
||||
...initialQueryBuilderFormValues,
|
||||
dataSource: DataSource.METRICS,
|
||||
}}
|
||||
onChange={jest.fn()}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
expect(queryByTestId('span-scope-selector')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const mockOnChange = jest.fn();
|
||||
const mockHandleRunQuery = jest.fn();
|
||||
const defaultProps = {
|
||||
query: {
|
||||
...initialQueriesMap.traces.builder.queryData[0],
|
||||
dataSource: DataSource.TRACES,
|
||||
queryName: 'traces_query',
|
||||
},
|
||||
onChange: mockOnChange,
|
||||
};
|
||||
|
||||
const renderWithContext = (props = {}): RenderResult => {
|
||||
const mergedProps = { ...defaultProps, ...props };
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<QueryBuilderContext.Provider
|
||||
value={
|
||||
{
|
||||
currentQuery: initialQueriesMap.traces,
|
||||
handleRunQuery: mockHandleRunQuery,
|
||||
} as any
|
||||
}
|
||||
>
|
||||
<QueryBuilderSearchV2 {...mergedProps} />
|
||||
</QueryBuilderContext.Provider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
const mockAggregateKeysData = {
|
||||
payload: {
|
||||
attributeKeys: [
|
||||
{
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
key: 'http.status',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
id: 'http.status--string--tag--false',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('hooks/queryBuilder/useGetAggregateKeys', () => ({
|
||||
useGetAggregateKeys: jest.fn(() => ({
|
||||
data: mockAggregateKeysData,
|
||||
isFetching: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
const mockAggregateValuesData = {
|
||||
payload: {
|
||||
stringAttributeValues: ['200', '404', '500'],
|
||||
numberAttributeValues: [200, 404, 500],
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('hooks/queryBuilder/useGetAggregateValues', () => ({
|
||||
useGetAggregateValues: jest.fn(() => ({
|
||||
data: mockAggregateValuesData,
|
||||
isFetching: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): any => ({
|
||||
safeNavigate: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Suggestion Key -> Operator -> Value Flow', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('should complete full flow from key selection to value', async () => {
|
||||
const { container } = renderWithContext();
|
||||
|
||||
// Get the combobox input specifically
|
||||
const combobox = container.querySelector(
|
||||
'.query-builder-search-v2 .ant-select-selection-search-input',
|
||||
) as HTMLInputElement;
|
||||
|
||||
// 1. Focus and type to trigger key suggestions
|
||||
await act(async () => {
|
||||
fireEvent.focus(combobox);
|
||||
fireEvent.change(combobox, { target: { value: 'http.' } });
|
||||
});
|
||||
|
||||
// Wait for dropdown to appear
|
||||
await screen.findByRole('listbox');
|
||||
|
||||
// 2. Select a key from suggestions
|
||||
const statusOption = await screen.findByText('http.status');
|
||||
await act(async () => {
|
||||
fireEvent.click(statusOption);
|
||||
});
|
||||
|
||||
// Should show operator suggestions
|
||||
expect(screen.getByText('=')).toBeInTheDocument();
|
||||
expect(screen.getByText('!=')).toBeInTheDocument();
|
||||
|
||||
// 3. Select an operator
|
||||
const equalsOption = screen.getByText('=');
|
||||
await act(async () => {
|
||||
fireEvent.click(equalsOption);
|
||||
});
|
||||
|
||||
// Should show value suggestions
|
||||
expect(screen.getByText('200')).toBeInTheDocument();
|
||||
expect(screen.getByText('404')).toBeInTheDocument();
|
||||
expect(screen.getByText('500')).toBeInTheDocument();
|
||||
|
||||
// 4. Select a value
|
||||
const valueOption = screen.getByText('200');
|
||||
await act(async () => {
|
||||
fireEvent.click(valueOption);
|
||||
});
|
||||
|
||||
// Verify final filter
|
||||
expect(mockOnChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ key: 'http.status' }),
|
||||
op: '=',
|
||||
value: '200',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,165 +0,0 @@
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
RenderResult,
|
||||
screen,
|
||||
} from '@testing-library/react';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { QueryBuilderContext } from 'providers/QueryBuilder';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import SpanScopeSelector from '../SpanScopeSelector';
|
||||
|
||||
const mockRedirectWithQueryBuilderData = jest.fn();
|
||||
|
||||
// Helper to create filter items
|
||||
const createSpanScopeFilter = (key: string): TagFilterItem => ({
|
||||
id: 'span-filter',
|
||||
key: {
|
||||
key,
|
||||
type: 'spanSearchScope',
|
||||
},
|
||||
op: '=',
|
||||
value: 'true',
|
||||
});
|
||||
|
||||
const defaultQuery = {
|
||||
...initialQueriesMap.traces,
|
||||
builder: {
|
||||
...initialQueriesMap.traces.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueriesMap.traces.builder.queryData[0],
|
||||
queryName: 'A',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Helper to create query with filters
|
||||
const createQueryWithFilters = (filters: TagFilterItem[]): Query => ({
|
||||
...defaultQuery,
|
||||
builder: {
|
||||
...defaultQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...defaultQuery.builder.queryData[0],
|
||||
filters: {
|
||||
items: filters,
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const renderWithContext = (
|
||||
queryName = 'A',
|
||||
initialQuery = defaultQuery,
|
||||
): RenderResult =>
|
||||
render(
|
||||
<QueryBuilderContext.Provider
|
||||
value={
|
||||
{
|
||||
currentQuery: initialQuery,
|
||||
redirectWithQueryBuilderData: mockRedirectWithQueryBuilderData,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} as any
|
||||
}
|
||||
>
|
||||
<SpanScopeSelector queryName={queryName} />
|
||||
</QueryBuilderContext.Provider>,
|
||||
);
|
||||
|
||||
describe('SpanScopeSelector', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render with default ALL_SPANS selected', () => {
|
||||
renderWithContext();
|
||||
expect(screen.getByText('All Spans')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when selecting different options', () => {
|
||||
const selectOption = (optionText: string): void => {
|
||||
const selector = screen.getByRole('combobox');
|
||||
fireEvent.mouseDown(selector);
|
||||
const option = screen.getByText(optionText);
|
||||
fireEvent.click(option);
|
||||
};
|
||||
|
||||
const assertFilterAdded = (
|
||||
updatedQuery: Query,
|
||||
expectedKey: string,
|
||||
): void => {
|
||||
const filters = updatedQuery.builder.queryData[0].filters.items;
|
||||
expect(filters).toContainEqual(
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({
|
||||
key: expectedKey,
|
||||
type: 'spanSearchScope',
|
||||
}),
|
||||
op: '=',
|
||||
value: 'true',
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
it('should remove span scope filters when selecting ALL_SPANS', () => {
|
||||
const queryWithSpanScope = createQueryWithFilters([
|
||||
createSpanScopeFilter('isRoot'),
|
||||
]);
|
||||
renderWithContext('A', queryWithSpanScope);
|
||||
|
||||
selectOption('All Spans');
|
||||
|
||||
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalled();
|
||||
const updatedQuery = mockRedirectWithQueryBuilderData.mock.calls[0][0];
|
||||
const filters = updatedQuery.builder.queryData[0].filters.items;
|
||||
expect(filters).not.toContainEqual(
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ type: 'spanSearchScope' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should add isRoot filter when selecting ROOT_SPANS', async () => {
|
||||
renderWithContext();
|
||||
await selectOption('Root Spans');
|
||||
|
||||
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalled();
|
||||
assertFilterAdded(
|
||||
mockRedirectWithQueryBuilderData.mock.calls[0][0],
|
||||
'isRoot',
|
||||
);
|
||||
});
|
||||
|
||||
it('should add isEntryPoint filter when selecting ENTRYPOINT_SPANS', () => {
|
||||
renderWithContext();
|
||||
selectOption('Entrypoint Spans');
|
||||
|
||||
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalled();
|
||||
assertFilterAdded(
|
||||
mockRedirectWithQueryBuilderData.mock.calls[0][0],
|
||||
'isEntryPoint',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when initializing with existing filters', () => {
|
||||
it.each([
|
||||
['Root Spans', 'isRoot'],
|
||||
['Entrypoint Spans', 'isEntryPoint'],
|
||||
])(
|
||||
'should initialize with %s selected when %s filter exists',
|
||||
async (expectedText, filterKey) => {
|
||||
const queryWithFilter = createQueryWithFilters([
|
||||
createSpanScopeFilter(filterKey),
|
||||
]);
|
||||
renderWithContext('A', queryWithFilter);
|
||||
expect(await screen.findByText(expectedText)).toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import { AttributeValuesMap } from 'components/ClientSideQBSearch/ClientSideQBSearch';
|
||||
import { OperatorConfigKeys, OPERATORS_CONFIG } from 'constants/queryBuilder';
|
||||
import { HAVING_FILTER_REGEXP } from 'constants/regExp';
|
||||
import { IOption } from 'hooks/useResourceAttribute/types';
|
||||
import uniqWith from 'lodash-es/unionWith';
|
||||
@@ -111,13 +110,3 @@ export const transformKeyValuesToAttributeValuesMap = (
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
export const filterByOperatorConfig = (
|
||||
options: IOption[],
|
||||
key?: OperatorConfigKeys,
|
||||
): IOption[] => {
|
||||
if (!key || !OPERATORS_CONFIG[key]) return options;
|
||||
return options.filter((option) =>
|
||||
OPERATORS_CONFIG[key].includes(option.label),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
.resourceAttributesFilter-container-v2 {
|
||||
margin: 8px;
|
||||
|
||||
.ant-select-selector {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400) !important;
|
||||
background-color: var(--bg-ink-300) !important;
|
||||
|
||||
input {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ant-tag .ant-typography {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import './ResourceAttributesFilter.styles.scss';
|
||||
|
||||
import { initialQueriesMap, OperatorConfigKeys } from 'constants/queryBuilder';
|
||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { useCallback } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
function ResourceAttributesFilter(): JSX.Element | null {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const query = currentQuery?.builder?.queryData[0] || null;
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query,
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
// initialise tab with default query.
|
||||
useShareBuilderUrl({
|
||||
...initialQueriesMap.traces,
|
||||
builder: {
|
||||
...initialQueriesMap.traces.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueriesMap.traces.builder.queryData[0],
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
|
||||
type: 'resource',
|
||||
},
|
||||
queryName: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const handleChangeTagFilters = useCallback(
|
||||
(value: IBuilderQuery['filters']) => {
|
||||
handleChangeQueryData('filters', value);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="resourceAttributesFilter-container-v2">
|
||||
<QueryBuilderSearchV2
|
||||
query={query}
|
||||
onChange={handleChangeTagFilters}
|
||||
operatorConfigKey={OperatorConfigKeys.EXCEPTIONS}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResourceAttributesFilter;
|
||||
@@ -230,7 +230,6 @@ export const routesToSkip = [
|
||||
ROUTES.CHANNELS_NEW,
|
||||
ROUTES.CHANNELS_EDIT,
|
||||
ROUTES.WORKSPACE_ACCESS_RESTRICTED,
|
||||
ROUTES.ALL_ERROR,
|
||||
];
|
||||
|
||||
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
.span-line-action-buttons {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
top: 50%;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-400);
|
||||
|
||||
.ant-btn-default {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 9px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
&.active-tab {
|
||||
background-color: var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
|
||||
.copy-span-btn {
|
||||
border-color: var(--bg-slate-400) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.span-line-action-buttons {
|
||||
border: 1px solid var(--bg-vanilla-400);
|
||||
background: var(--bg-vanilla-400);
|
||||
|
||||
.copy-span-btn {
|
||||
border-color: var(--bg-vanilla-400) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { useCopySpanLink } from 'hooks/trace/useCopySpanLink';
|
||||
import { render } from 'tests/test-utils';
|
||||
import { Span } from 'types/api/trace/getTraceV2';
|
||||
|
||||
import SpanLineActionButtons from '../index';
|
||||
|
||||
// Mock the useCopySpanLink hook
|
||||
jest.mock('hooks/trace/useCopySpanLink');
|
||||
|
||||
const mockSpan: Span = {
|
||||
spanId: 'test-span-id',
|
||||
name: 'test-span',
|
||||
serviceName: 'test-service',
|
||||
durationNano: 1000,
|
||||
timestamp: 1234567890,
|
||||
rootSpanId: 'test-root-span-id',
|
||||
parentSpanId: 'test-parent-span-id',
|
||||
traceId: 'test-trace-id',
|
||||
hasError: false,
|
||||
kind: 0,
|
||||
references: [],
|
||||
tagMap: {},
|
||||
event: [],
|
||||
rootName: 'test-root-name',
|
||||
statusMessage: 'test-status-message',
|
||||
statusCodeString: 'test-status-code-string',
|
||||
spanKind: 'test-span-kind',
|
||||
hasChildren: false,
|
||||
hasSibling: false,
|
||||
subTreeNodeCount: 0,
|
||||
level: 0,
|
||||
};
|
||||
|
||||
describe('SpanLineActionButtons', () => {
|
||||
beforeEach(() => {
|
||||
// Clear mock before each test
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders copy link button with correct icon', () => {
|
||||
(useCopySpanLink as jest.Mock).mockReturnValue({
|
||||
onSpanCopy: jest.fn(),
|
||||
});
|
||||
|
||||
render(<SpanLineActionButtons span={mockSpan} />);
|
||||
|
||||
// Check if the button is rendered
|
||||
const copyButton = screen.getByRole('button');
|
||||
expect(copyButton).toBeInTheDocument();
|
||||
|
||||
// Check if the link icon is rendered
|
||||
const linkIcon = screen.getByRole('img', { hidden: true });
|
||||
expect(linkIcon).toHaveClass('anticon anticon-link');
|
||||
});
|
||||
|
||||
it('calls onSpanCopy when copy button is clicked', () => {
|
||||
const mockOnSpanCopy = jest.fn();
|
||||
(useCopySpanLink as jest.Mock).mockReturnValue({
|
||||
onSpanCopy: mockOnSpanCopy,
|
||||
});
|
||||
|
||||
render(<SpanLineActionButtons span={mockSpan} />);
|
||||
|
||||
// Click the copy button
|
||||
const copyButton = screen.getByRole('button');
|
||||
fireEvent.click(copyButton);
|
||||
|
||||
// Verify the copy function was called
|
||||
expect(mockOnSpanCopy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('applies correct styling classes', () => {
|
||||
(useCopySpanLink as jest.Mock).mockReturnValue({
|
||||
onSpanCopy: jest.fn(),
|
||||
});
|
||||
|
||||
render(<SpanLineActionButtons span={mockSpan} />);
|
||||
|
||||
// Check if the main container has the correct class
|
||||
const container = screen
|
||||
.getByRole('button')
|
||||
.closest('.span-line-action-buttons');
|
||||
expect(container).toHaveClass('span-line-action-buttons');
|
||||
|
||||
// Check if the button has the correct class
|
||||
const copyButton = screen.getByRole('button');
|
||||
expect(copyButton).toHaveClass('copy-span-btn');
|
||||
});
|
||||
|
||||
it('copies span link to clipboard when copy button is clicked', () => {
|
||||
const mockSetCopy = jest.fn();
|
||||
const mockUrlQuery = {
|
||||
delete: jest.fn(),
|
||||
set: jest.fn(),
|
||||
toString: jest.fn().mockReturnValue('spanId=test-span-id'),
|
||||
};
|
||||
const mockPathname = '/test-path';
|
||||
const mockLocation = {
|
||||
origin: 'http://localhost:3000',
|
||||
};
|
||||
|
||||
// Mock window.location
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: mockLocation,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
// Mock useCopySpanLink hook
|
||||
(useCopySpanLink as jest.Mock).mockReturnValue({
|
||||
onSpanCopy: (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
mockUrlQuery.delete('spanId');
|
||||
mockUrlQuery.set('spanId', mockSpan.spanId);
|
||||
const link = `${
|
||||
window.location.origin
|
||||
}${mockPathname}?${mockUrlQuery.toString()}`;
|
||||
mockSetCopy(link);
|
||||
},
|
||||
});
|
||||
|
||||
render(<SpanLineActionButtons span={mockSpan} />);
|
||||
|
||||
// Click the copy button
|
||||
const copyButton = screen.getByRole('button');
|
||||
fireEvent.click(copyButton);
|
||||
|
||||
// Verify the copy function was called with correct link
|
||||
expect(mockSetCopy).toHaveBeenCalledWith(
|
||||
'http://localhost:3000/test-path?spanId=test-span-id',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import './SpanLineActionButtons.styles.scss';
|
||||
|
||||
import { LinkOutlined } from '@ant-design/icons';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { useCopySpanLink } from 'hooks/trace/useCopySpanLink';
|
||||
import { Span } from 'types/api/trace/getTraceV2';
|
||||
|
||||
export interface SpanLineActionButtonsProps {
|
||||
span: Span;
|
||||
}
|
||||
export default function SpanLineActionButtons({
|
||||
span,
|
||||
}: SpanLineActionButtonsProps): JSX.Element {
|
||||
const { onSpanCopy } = useCopySpanLink(span);
|
||||
|
||||
return (
|
||||
<div className="span-line-action-buttons">
|
||||
<Tooltip title="Copy Span Link">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<LinkOutlined size={14} />}
|
||||
onClick={onSpanCopy}
|
||||
className="copy-span-btn"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,10 +9,7 @@ import cx from 'classnames';
|
||||
import { TableV3 } from 'components/TableV3/TableV3';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
||||
import SpanLineActionButtons from 'container/TraceWaterfall/SpanLineActionButtons';
|
||||
import { IInterestedSpan } from 'container/TraceWaterfall/TraceWaterfall';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||
import {
|
||||
AlertCircle,
|
||||
@@ -28,7 +25,6 @@ import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Span } from 'types/api/trace/getTraceV2';
|
||||
import { toFixed } from 'utils/toFixed';
|
||||
@@ -151,7 +147,7 @@ function SpanOverview({
|
||||
);
|
||||
}
|
||||
|
||||
export function SpanDuration({
|
||||
function SpanDuration({
|
||||
span,
|
||||
traceMetadata,
|
||||
setSelectedSpan,
|
||||
@@ -170,40 +166,20 @@ export function SpanDuration({
|
||||
const leftOffset = ((span.timestamp - traceMetadata.startTime) * 1e2) / spread;
|
||||
const width = (span.durationNano * 1e2) / (spread * 1e6);
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
let color = generateColor(span.serviceName, themeColors.traceDetailColors);
|
||||
|
||||
if (span.hasError) {
|
||||
color = `var(--bg-cherry-500)`;
|
||||
}
|
||||
|
||||
const [hasActionButtons, setHasActionButtons] = useState(false);
|
||||
|
||||
const handleMouseEnter = (): void => {
|
||||
setHasActionButtons(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = (): void => {
|
||||
setHasActionButtons(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'span-duration',
|
||||
selectedSpan?.spanId === span.spanId ? 'interested-span' : '',
|
||||
)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={(): void => {
|
||||
setSelectedSpan(span);
|
||||
if (span?.spanId) {
|
||||
urlQuery.set('spanId', span?.spanId);
|
||||
}
|
||||
|
||||
safeNavigate({ search: urlQuery.toString() });
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -214,7 +190,6 @@ export function SpanDuration({
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
{hasActionButtons && <SpanLineActionButtons span={span} />}
|
||||
<Tooltip title={`${toFixed(time, 2)} ${timeUnitName}`}>
|
||||
<Typography.Text
|
||||
className="span-line-text"
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { render } from 'tests/test-utils';
|
||||
import { Span } from 'types/api/trace/getTraceV2';
|
||||
|
||||
import { SpanDuration } from '../Success';
|
||||
|
||||
// Mock the hooks
|
||||
jest.mock('hooks/useSafeNavigate');
|
||||
jest.mock('hooks/useUrlQuery');
|
||||
|
||||
const mockSpan: Span = {
|
||||
spanId: 'test-span-id',
|
||||
name: 'test-span',
|
||||
serviceName: 'test-service',
|
||||
durationNano: 1160000, // 1ms in nano
|
||||
timestamp: 1234567890,
|
||||
rootSpanId: 'test-root-span-id',
|
||||
parentSpanId: 'test-parent-span-id',
|
||||
traceId: 'test-trace-id',
|
||||
hasError: false,
|
||||
kind: 0,
|
||||
references: [],
|
||||
tagMap: {},
|
||||
event: [],
|
||||
rootName: 'test-root-name',
|
||||
statusMessage: 'test-status-message',
|
||||
statusCodeString: 'test-status-code-string',
|
||||
spanKind: 'test-span-kind',
|
||||
hasChildren: false,
|
||||
hasSibling: false,
|
||||
subTreeNodeCount: 0,
|
||||
level: 0,
|
||||
};
|
||||
|
||||
const mockTraceMetadata = {
|
||||
traceId: 'test-trace-id',
|
||||
startTime: 1234567000,
|
||||
endTime: 1234569000,
|
||||
hasMissingSpans: false,
|
||||
};
|
||||
|
||||
describe('SpanDuration', () => {
|
||||
const mockSetSelectedSpan = jest.fn();
|
||||
const mockUrlQuerySet = jest.fn();
|
||||
const mockSafeNavigate = jest.fn();
|
||||
const mockUrlQueryGet = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Mock URL query hook
|
||||
(useUrlQuery as jest.Mock).mockReturnValue({
|
||||
set: mockUrlQuerySet,
|
||||
get: mockUrlQueryGet,
|
||||
toString: () => 'spanId=test-span-id',
|
||||
});
|
||||
|
||||
// Mock safe navigate hook
|
||||
(useSafeNavigate as jest.Mock).mockReturnValue({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
});
|
||||
});
|
||||
|
||||
it('updates URL and selected span when clicked', () => {
|
||||
render(
|
||||
<SpanDuration
|
||||
span={mockSpan}
|
||||
traceMetadata={mockTraceMetadata}
|
||||
selectedSpan={undefined}
|
||||
setSelectedSpan={mockSetSelectedSpan}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Find and click the span duration element
|
||||
const spanElement = screen.getByText('1.16 ms');
|
||||
fireEvent.click(spanElement);
|
||||
|
||||
// Verify setSelectedSpan was called with the correct span
|
||||
expect(mockSetSelectedSpan).toHaveBeenCalledWith(mockSpan);
|
||||
|
||||
// Verify URL query was updated
|
||||
expect(mockUrlQuerySet).toHaveBeenCalledWith('spanId', 'test-span-id');
|
||||
|
||||
// Verify navigation was triggered
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith({
|
||||
search: 'spanId=test-span-id',
|
||||
});
|
||||
});
|
||||
|
||||
it('shows action buttons on hover', () => {
|
||||
render(
|
||||
<SpanDuration
|
||||
span={mockSpan}
|
||||
traceMetadata={mockTraceMetadata}
|
||||
selectedSpan={undefined}
|
||||
setSelectedSpan={mockSetSelectedSpan}
|
||||
/>,
|
||||
);
|
||||
|
||||
const spanElement = screen.getByText('1.16 ms');
|
||||
|
||||
// Initially, action buttons should not be visible
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
||||
|
||||
// Hover over the span
|
||||
fireEvent.mouseEnter(spanElement);
|
||||
|
||||
// Action buttons should now be visible
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
|
||||
// Mouse leave should hide the buttons
|
||||
fireEvent.mouseLeave(spanElement);
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies interested-span class when span is selected', () => {
|
||||
render(
|
||||
<SpanDuration
|
||||
span={mockSpan}
|
||||
traceMetadata={mockTraceMetadata}
|
||||
selectedSpan={mockSpan}
|
||||
setSelectedSpan={mockSetSelectedSpan}
|
||||
/>,
|
||||
);
|
||||
|
||||
const spanElement = screen.getByText('1.16 ms').closest('.span-duration');
|
||||
expect(spanElement).toHaveClass('interested-span');
|
||||
});
|
||||
});
|
||||
@@ -170,7 +170,11 @@ export const useOptions = (
|
||||
(option, index, self) =>
|
||||
index ===
|
||||
self.findIndex(
|
||||
(o) => o.label === option.label && o.value === option.value, // to remove duplicate & empty options from list
|
||||
(o) =>
|
||||
// to remove duplicate & empty options from list
|
||||
o.label === option.label &&
|
||||
o.value === option.value &&
|
||||
o.dataType?.toLowerCase() === option.dataType?.toLowerCase(), // handle case sensitivity
|
||||
) && option.value !== '',
|
||||
) || []
|
||||
).map((option) => {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { MouseEventHandler, useCallback } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Span } from 'types/api/trace/getTraceV2';
|
||||
|
||||
export const useCopySpanLink = (
|
||||
span?: Span,
|
||||
): { onSpanCopy: MouseEventHandler<HTMLElement> } => {
|
||||
const urlQuery = useUrlQuery();
|
||||
const { pathname } = useLocation();
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const onSpanCopy: MouseEventHandler<HTMLElement> = useCallback(
|
||||
(event) => {
|
||||
if (!span) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
urlQuery.delete('spanId');
|
||||
|
||||
if (span.spanId) {
|
||||
urlQuery.set('spanId', span?.spanId);
|
||||
}
|
||||
|
||||
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
|
||||
|
||||
setCopy(link);
|
||||
notifications.success({
|
||||
message: 'Copied to clipboard',
|
||||
});
|
||||
},
|
||||
[span, urlQuery, pathname, setCopy, notifications],
|
||||
);
|
||||
|
||||
return {
|
||||
onSpanCopy,
|
||||
};
|
||||
};
|
||||
@@ -2,10 +2,7 @@ import {
|
||||
getResourceAttributesTagKeys,
|
||||
getResourceAttributesTagValues,
|
||||
} from 'api/metrics/getResourceAttributes';
|
||||
import {
|
||||
CompositeQueryOperatorsConfig,
|
||||
OperatorConversions,
|
||||
} from 'constants/resourceAttributes';
|
||||
import { OperatorConversions } from 'constants/resourceAttributes';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import {
|
||||
@@ -52,32 +49,6 @@ export const convertOperatorLabelToTraceOperator = (
|
||||
OperatorConversions.find((operator) => operator.label === label)
|
||||
?.traceValue as OperatorValues;
|
||||
|
||||
export function convertOperatorLabelForExceptions(
|
||||
label: string,
|
||||
): OperatorValues {
|
||||
return CompositeQueryOperatorsConfig.find(
|
||||
(operator) => operator.label === label,
|
||||
)?.traceValue as OperatorValues;
|
||||
}
|
||||
|
||||
export function formatStringValuesForTrace(
|
||||
val: TagFilterItem['value'] = [],
|
||||
): string[] {
|
||||
return !Array.isArray(val) ? [String(val)] : val;
|
||||
}
|
||||
|
||||
export const convertCompositeQueryToTraceSelectedTags = (
|
||||
filterItems: TagFilterItem[] = [],
|
||||
): Tags[] =>
|
||||
filterItems.map((item) => ({
|
||||
Key: item?.key?.key,
|
||||
Operator: convertOperatorLabelForExceptions(item.op),
|
||||
StringValues: formatStringValuesForTrace(item?.value),
|
||||
NumberValues: [],
|
||||
BoolValues: [],
|
||||
TagType: 'ResourceAttribute',
|
||||
})) as Tags[];
|
||||
|
||||
export const convertRawQueriesToTraceSelectedTags = (
|
||||
queries: IResourceAttribute[],
|
||||
tagType = 'ResourceAttribute',
|
||||
|
||||
@@ -44,7 +44,7 @@ function AlertActionButtons({
|
||||
const { handleAlertDuplicate } = useAlertRuleDuplicate({
|
||||
alertDetails: (alertDetails as unknown) as AlertDef,
|
||||
});
|
||||
const { handleAlertDelete } = useAlertRuleDelete({ ruleId });
|
||||
const { handleAlertDelete } = useAlertRuleDelete({ ruleId: Number(ruleId) });
|
||||
const { handleAlertUpdate, isLoading } = useAlertRuleUpdate({
|
||||
alertDetails: (alertDetails as unknown) as AlertDef,
|
||||
setUpdatedName,
|
||||
|
||||
@@ -153,7 +153,7 @@ type Props = {
|
||||
export const useGetAlertRuleDetails = (): Props => {
|
||||
const { ruleId } = useAlertHistoryQueryParams();
|
||||
|
||||
const isValidRuleId = ruleId !== null && ruleId !== '';
|
||||
const isValidRuleId = ruleId !== null && String(ruleId).length !== 0;
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
@@ -163,7 +163,7 @@ export const useGetAlertRuleDetails = (): Props => {
|
||||
} = useQuery([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId], {
|
||||
queryFn: () =>
|
||||
get({
|
||||
id: ruleId || '',
|
||||
id: parseInt(ruleId || '', 10),
|
||||
}),
|
||||
enabled: isValidRuleId,
|
||||
refetchOnWindowFocus: false,
|
||||
@@ -204,7 +204,7 @@ export const useGetAlertRuleDetailsStats = (): GetAlertRuleDetailsStatsProps =>
|
||||
{
|
||||
queryFn: () =>
|
||||
ruleStats({
|
||||
id: ruleId || '',
|
||||
id: parseInt(ruleId || '', 10),
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
}),
|
||||
@@ -234,7 +234,7 @@ export const useGetAlertRuleDetailsTopContributors = (): GetAlertRuleDetailsTopC
|
||||
{
|
||||
queryFn: () =>
|
||||
topContributors({
|
||||
id: ruleId || '',
|
||||
id: parseInt(ruleId || '', 10),
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
}),
|
||||
@@ -287,7 +287,7 @@ export const useGetAlertRuleDetailsTimelineTable = ({
|
||||
{
|
||||
queryFn: () =>
|
||||
timelineTable({
|
||||
id: ruleId || '',
|
||||
id: parseInt(ruleId || '', 10),
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
limit: TIMELINE_TABLE_PAGE_SIZE,
|
||||
@@ -410,7 +410,7 @@ export const useAlertRuleStatusToggle = ({
|
||||
|
||||
const handleAlertStateToggle = (): void => {
|
||||
const args = {
|
||||
id: ruleId,
|
||||
id: parseInt(ruleId, 10),
|
||||
data: { disabled: alertRuleState !== 'disabled' },
|
||||
};
|
||||
toggleAlertState(args);
|
||||
@@ -512,7 +512,7 @@ export const useAlertRuleUpdate = ({
|
||||
export const useAlertRuleDelete = ({
|
||||
ruleId,
|
||||
}: {
|
||||
ruleId: string;
|
||||
ruleId: number;
|
||||
}): {
|
||||
handleAlertDelete: () => void;
|
||||
} => {
|
||||
@@ -560,7 +560,7 @@ export const useGetAlertRuleDetailsTimelineGraphData = (): GetAlertRuleDetailsTi
|
||||
{
|
||||
queryFn: () =>
|
||||
timelineGraph({
|
||||
id: ruleId || '',
|
||||
id: parseInt(ruleId || '', 10),
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
}),
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
.all-errors-page {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
.all-errors-quick-filter-section {
|
||||
width: 0%;
|
||||
flex-shrink: 0;
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.all-errors-right-section {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.ant-tabs {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
&.filter-visible {
|
||||
.all-errors-quick-filter-section {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.all-errors-right-section {
|
||||
width: calc(100% - 260px);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +1,18 @@
|
||||
import './AllErrors.styles.scss';
|
||||
|
||||
import { FilterOutlined } from '@ant-design/icons';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import cx from 'classnames';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import RouteTab from 'components/RouteTab';
|
||||
import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import ResourceAttributesFilterV2 from 'container/ResourceAttributeFilterV2/ResourceAttributesFilterV2';
|
||||
import Toolbar from 'container/Toolbar/Toolbar';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import ResourceAttributesFilter from 'container/ResourceAttributesFilter';
|
||||
import history from 'lib/history';
|
||||
import { isNull } from 'lodash-es';
|
||||
import { useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { routes } from './config';
|
||||
import { ExceptionsQuickFiltersConfig } from './utils';
|
||||
|
||||
function AllErrors(): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
const { handleRunQuery } = useQueryBuilder();
|
||||
|
||||
const [showFilters, setShowFilters] = useState<boolean>(() => {
|
||||
const localStorageValue = getLocalStorageKey(
|
||||
LOCALSTORAGE.SHOW_EXCEPTIONS_QUICK_FILTERS,
|
||||
);
|
||||
if (!isNull(localStorageValue)) {
|
||||
return localStorageValue === 'true';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const handleFilterVisibilityChange = (): void => {
|
||||
setLocalStorageApi(
|
||||
LOCALSTORAGE.SHOW_EXCEPTIONS_QUICK_FILTERS,
|
||||
String(!showFilters),
|
||||
);
|
||||
setShowFilters((prev) => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cx('all-errors-page', showFilters ? 'filter-visible' : '')}>
|
||||
{showFilters && (
|
||||
<section className={cx('all-errors-quick-filter-section')}>
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.EXCEPTIONS}
|
||||
config={ExceptionsQuickFiltersConfig}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
<section
|
||||
className={cx(
|
||||
'all-errors-right-section',
|
||||
showFilters ? 'filter-visible' : '',
|
||||
)}
|
||||
>
|
||||
<TypicalOverlayScrollbar>
|
||||
<>
|
||||
<Toolbar
|
||||
showAutoRefresh={false}
|
||||
leftActions={
|
||||
!showFilters ? (
|
||||
<Tooltip title="Show Filters">
|
||||
<Button onClick={handleFilterVisibilityChange} className="filter-btn">
|
||||
<FilterOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : undefined
|
||||
}
|
||||
rightActions={<RightToolbarActions onStageRunQuery={handleRunQuery} />}
|
||||
/>
|
||||
<ResourceAttributesFilterV2 />
|
||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||
</>
|
||||
</TypicalOverlayScrollbar>
|
||||
</section>
|
||||
</div>
|
||||
<>
|
||||
<ResourceAttributesFilter />
|
||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import {
|
||||
FiltersType,
|
||||
IQuickFiltersConfig,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export const ExceptionsQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Environment',
|
||||
dataSource: DataSource.TRACES,
|
||||
attributeKey: {
|
||||
key: 'deployment.environment',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Service Name',
|
||||
dataSource: DataSource.TRACES,
|
||||
attributeKey: {
|
||||
key: 'service.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Hostname',
|
||||
dataSource: DataSource.TRACES,
|
||||
attributeKey: {
|
||||
key: 'host.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'K8s Cluster Name',
|
||||
dataSource: DataSource.TRACES,
|
||||
attributeKey: {
|
||||
key: 'k8s.cluster.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'K8s Deployment Name',
|
||||
dataSource: DataSource.TRACES,
|
||||
attributeKey: {
|
||||
key: 'k8s.deployment.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'K8s Namespace Name',
|
||||
dataSource: DataSource.TRACES,
|
||||
attributeKey: {
|
||||
key: 'k8s.namespace.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'K8s Pod Name',
|
||||
dataSource: DataSource.TRACES,
|
||||
attributeKey: {
|
||||
key: 'k8s.pod.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
];
|
||||
@@ -34,7 +34,7 @@ function EditRules(): JSX.Element {
|
||||
{
|
||||
queryFn: () =>
|
||||
get({
|
||||
id: ruleId || '',
|
||||
id: parseInt(ruleId || '', 10),
|
||||
}),
|
||||
enabled: isValidRuleId,
|
||||
refetchOnMount: false,
|
||||
@@ -90,7 +90,10 @@ function EditRules(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="edit-rules-container">
|
||||
<EditRulesContainer ruleId={ruleId || ''} initialValue={data.payload.data} />
|
||||
<EditRulesContainer
|
||||
ruleId={parseInt(ruleId, 10)}
|
||||
initialValue={data.payload.data}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ export default function Support(): JSX.Element {
|
||||
});
|
||||
|
||||
updateCreditCard({
|
||||
url: window.location.origin,
|
||||
url: window.location.href,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ export default function WorkspaceBlocked(): JSX.Element {
|
||||
logEvent('Workspace Blocked: User Clicked Update Credit Card', {});
|
||||
|
||||
updateCreditCard({
|
||||
url: window.location.origin,
|
||||
url: window.location.href,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [updateCreditCard]);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import './LineClampedText.styles.scss';
|
||||
|
||||
import { Tooltip, TooltipProps } from 'antd';
|
||||
import { isBoolean } from 'lodash-es';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
function LineClampedText({
|
||||
@@ -9,7 +8,7 @@ function LineClampedText({
|
||||
lines,
|
||||
tooltipProps,
|
||||
}: {
|
||||
text: string | boolean;
|
||||
text: string;
|
||||
lines?: number;
|
||||
tooltipProps?: TooltipProps;
|
||||
}): JSX.Element {
|
||||
@@ -41,7 +40,7 @@ function LineClampedText({
|
||||
WebkitLineClamp: lines,
|
||||
}}
|
||||
>
|
||||
{isBoolean(text) ? String(text) : text}
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import LineClampedText from '../LineClampedText';
|
||||
|
||||
describe('LineClampedText', () => {
|
||||
// Reset all mocks after each test
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders string text correctly', () => {
|
||||
const text = 'Test text';
|
||||
render(<LineClampedText text={text} />);
|
||||
|
||||
expect(screen.getByText(text)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders empty string correctly', () => {
|
||||
const { container } = render(<LineClampedText text="" />);
|
||||
|
||||
// For empty strings, we need to check that a div exists
|
||||
// but it's harder to check for empty text directly with queries
|
||||
expect(container.textContent).toBe('');
|
||||
});
|
||||
|
||||
it('renders boolean text correctly - true', () => {
|
||||
render(<LineClampedText text />);
|
||||
|
||||
expect(screen.getByText('true')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders boolean text correctly - false', () => {
|
||||
render(<LineClampedText text={false} />);
|
||||
|
||||
expect(screen.getByText('false')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies line clamping with provided lines prop', () => {
|
||||
const text = 'Test text with multiple lines';
|
||||
const lines = 2;
|
||||
|
||||
render(<LineClampedText text={text} lines={lines} />);
|
||||
|
||||
// Verify the text is rendered correctly
|
||||
expect(screen.getByText(text)).toBeInTheDocument();
|
||||
|
||||
// Verify the component received the correct props
|
||||
expect((screen.getByText(text).style as any).WebkitLineClamp).toBe(
|
||||
String(lines),
|
||||
);
|
||||
});
|
||||
|
||||
it('uses default line count of 1 when lines prop is not provided', () => {
|
||||
const text = 'Test text';
|
||||
|
||||
render(<LineClampedText text={text} />);
|
||||
|
||||
// Verify the text is rendered correctly
|
||||
expect(screen.getByText(text)).toBeInTheDocument();
|
||||
|
||||
// Verify the default props
|
||||
expect(LineClampedText.defaultProps?.lines).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ export const defaultAlgorithm = 'standard';
|
||||
export const defaultSeasonality = 'hourly';
|
||||
|
||||
export interface AlertDef {
|
||||
id?: string;
|
||||
id?: number;
|
||||
alertType?: string;
|
||||
alert: string;
|
||||
ruleType?: string;
|
||||
|
||||
@@ -5,7 +5,7 @@ export interface Props {
|
||||
}
|
||||
|
||||
export interface GettableAlert extends AlertDef {
|
||||
id: string;
|
||||
id: number;
|
||||
alert: string;
|
||||
state: string;
|
||||
disabled: boolean;
|
||||
|
||||
@@ -7,6 +7,6 @@ export interface PatchProps {
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
id?: string;
|
||||
id?: number;
|
||||
data: PatchProps;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@ export type PayloadProps = {
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
id?: string;
|
||||
id?: number;
|
||||
data: AlertDef;
|
||||
}
|
||||
|
||||
@@ -6852,17 +6852,18 @@ copy-to-clipboard@^3.3.1, copy-to-clipboard@^3.3.3:
|
||||
dependencies:
|
||||
toggle-selection "^1.0.6"
|
||||
|
||||
copy-webpack-plugin@^11.0.0:
|
||||
version "11.0.0"
|
||||
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a"
|
||||
integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==
|
||||
copy-webpack-plugin@^8.1.0:
|
||||
version "8.1.1"
|
||||
resolved "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-8.1.1.tgz"
|
||||
integrity sha512-rYM2uzRxrLRpcyPqGceRBDpxxUV8vcDqIKxAUKfcnFpcrPxT5+XvhTxv7XLjo5AvEJFPdAE3zCogG2JVahqgSQ==
|
||||
dependencies:
|
||||
fast-glob "^3.2.11"
|
||||
glob-parent "^6.0.1"
|
||||
globby "^13.1.1"
|
||||
fast-glob "^3.2.5"
|
||||
glob-parent "^5.1.1"
|
||||
globby "^11.0.3"
|
||||
normalize-path "^3.0.0"
|
||||
schema-utils "^4.0.0"
|
||||
serialize-javascript "^6.0.0"
|
||||
p-limit "^3.1.0"
|
||||
schema-utils "^3.0.0"
|
||||
serialize-javascript "^5.0.1"
|
||||
|
||||
core-js-compat@^3.25.1:
|
||||
version "3.30.1"
|
||||
@@ -8739,7 +8740,7 @@ fast-diff@^1.1.2:
|
||||
resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz"
|
||||
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
|
||||
|
||||
fast-glob@^3.0.3, fast-glob@^3.2.11, fast-glob@^3.3.0:
|
||||
fast-glob@^3.0.3:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
|
||||
integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
|
||||
@@ -8750,7 +8751,7 @@ fast-glob@^3.0.3, fast-glob@^3.2.11, fast-glob@^3.3.0:
|
||||
merge2 "^1.3.0"
|
||||
micromatch "^4.0.8"
|
||||
|
||||
fast-glob@^3.2.9:
|
||||
fast-glob@^3.2.5, fast-glob@^3.2.9:
|
||||
version "3.2.12"
|
||||
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz"
|
||||
integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
|
||||
@@ -9306,20 +9307,13 @@ gl-preserve-state@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/gl-preserve-state/-/gl-preserve-state-1.0.0.tgz"
|
||||
integrity sha512-zQZ25l3haD4hvgJZ6C9+s0ebdkW9y+7U2qxvGu1uWOJh8a4RU+jURIKEQhf8elIlFpMH6CrAY2tH0mYrRjet3Q==
|
||||
|
||||
glob-parent@^5.1.2, glob-parent@~5.1.2:
|
||||
glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
|
||||
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
|
||||
dependencies:
|
||||
is-glob "^4.0.1"
|
||||
|
||||
glob-parent@^6.0.1:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
|
||||
integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
|
||||
dependencies:
|
||||
is-glob "^4.0.3"
|
||||
|
||||
glob-to-regexp@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz"
|
||||
@@ -9407,17 +9401,6 @@ globby@^11.0.3, globby@^11.1.0:
|
||||
merge2 "^1.4.1"
|
||||
slash "^3.0.0"
|
||||
|
||||
globby@^13.1.1:
|
||||
version "13.2.2"
|
||||
resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592"
|
||||
integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==
|
||||
dependencies:
|
||||
dir-glob "^3.0.1"
|
||||
fast-glob "^3.3.0"
|
||||
ignore "^5.2.4"
|
||||
merge2 "^1.4.1"
|
||||
slash "^4.0.0"
|
||||
|
||||
gopd@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz"
|
||||
@@ -10078,11 +10061,6 @@ ignore@^5.1.1, ignore@^5.1.8, ignore@^5.2.0:
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
|
||||
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
|
||||
|
||||
ignore@^5.2.4:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
|
||||
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
|
||||
|
||||
image-size@~0.5.0:
|
||||
version "0.5.5"
|
||||
resolved "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz"
|
||||
@@ -13423,7 +13401,7 @@ p-limit@^2.2.0:
|
||||
dependencies:
|
||||
p-try "^2.0.0"
|
||||
|
||||
p-limit@^3.0.2:
|
||||
p-limit@^3.0.2, p-limit@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz"
|
||||
integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
|
||||
@@ -15867,10 +15845,17 @@ send@0.19.0:
|
||||
range-parser "~1.2.1"
|
||||
statuses "2.0.1"
|
||||
|
||||
serialize-javascript@6.0.2, serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2"
|
||||
integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==
|
||||
serialize-javascript@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz"
|
||||
integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==
|
||||
dependencies:
|
||||
randombytes "^2.1.0"
|
||||
|
||||
serialize-javascript@^6.0.0, serialize-javascript@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz"
|
||||
integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==
|
||||
dependencies:
|
||||
randombytes "^2.1.0"
|
||||
|
||||
@@ -16027,11 +16012,6 @@ slash@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz"
|
||||
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
|
||||
|
||||
slash@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7"
|
||||
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
|
||||
|
||||
slice-ansi@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz"
|
||||
|
||||
6
go.mod
6
go.mod
@@ -10,8 +10,7 @@ require (
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.30.0
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
|
||||
github.com/SigNoz/signoz-otel-collector v0.111.39
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||
github.com/SigNoz/signoz-otel-collector v0.111.16
|
||||
github.com/antonmedv/expr v1.15.3
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
github.com/coreos/go-oidc/v3 v3.11.0
|
||||
@@ -90,9 +89,10 @@ require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
|
||||
github.com/ClickHouse/ch-go v0.63.1 // indirect
|
||||
github.com/ClickHouse/ch-go v0.61.5 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go v1.55.5 // indirect
|
||||
|
||||
14
go.sum
14
go.sum
@@ -85,8 +85,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mx
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/ClickHouse/ch-go v0.63.1 h1:s2JyZvWLTCSAGdtjMBBmAgQQHMco6pawLJMOXi0FODM=
|
||||
github.com/ClickHouse/ch-go v0.63.1/go.mod h1:I1kJJCL3WJcBMGe1m+HVK0+nREaG+JOYYBWjrDrF3R0=
|
||||
github.com/ClickHouse/ch-go v0.61.5 h1:zwR8QbYI0tsMiEcze/uIMK+Tz1D3XZXLdNrlaOpeEI4=
|
||||
github.com/ClickHouse/ch-go v0.61.5/go.mod h1:s1LJW/F/LcFs5HJnuogFMta50kKDO0lf9zzfrbl0RQg=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.30.0 h1:AG4D/hW39qa58+JHQIFOSnxyL46H6h2lrmGGk17dhFo=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.30.0/go.mod h1:i9ZQAojcayW3RsdCb3YR+n+wC2h65eJsZCscZ1Z1wyo=
|
||||
github.com/Code-Hex/go-generics-cache v1.5.1 h1:6vhZGc5M7Y/YD8cIUcY8kcuQLB4cHR7U+0KMqAA0KcU=
|
||||
@@ -100,10 +100,8 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkbj57eGXx8H3ZJ4zhmQXBnrW523ktj8=
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc=
|
||||
github.com/SigNoz/signoz-otel-collector v0.111.39-beta.1 h1:ZpSNrOZBOH2iCJIPeER5X0mfxOe64yP3JRX7FzBNfwY=
|
||||
github.com/SigNoz/signoz-otel-collector v0.111.39-beta.1/go.mod h1:DCu/D+lqhsPNSGS4IMD+4gn7q06TGzOCKazSy+GURVc=
|
||||
github.com/SigNoz/signoz-otel-collector v0.111.39 h1:Dl8QqZNAsj2atxP572OzsszPK0XPpd3LLPNPRAUJ5wo=
|
||||
github.com/SigNoz/signoz-otel-collector v0.111.39/go.mod h1:DCu/D+lqhsPNSGS4IMD+4gn7q06TGzOCKazSy+GURVc=
|
||||
github.com/SigNoz/signoz-otel-collector v0.111.16 h1:535uKH5Oux+35EsI+L3C6pnAP/Ye0PTCbVizXoL+VqE=
|
||||
github.com/SigNoz/signoz-otel-collector v0.111.16/go.mod h1:HJ4m0LY1MPsuZmuRF7Ixb+bY8rxgRzI0VXzOedESsjg=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
@@ -822,8 +820,8 @@ github.com/prometheus/prometheus v0.300.1/go.mod h1:gtTPY/XVyCdqqnjA3NzDMb0/nc5H
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/redis/go-redis/v9 v9.6.3 h1:8Dr5ygF1QFXRxIH/m3Xg9MMG1rS8YCtAgosrsewT6i0=
|
||||
github.com/redis/go-redis/v9 v9.6.3/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
|
||||
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
|
||||
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
|
||||
github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
|
||||
@@ -104,7 +104,7 @@ fullText
|
||||
* ...
|
||||
*/
|
||||
functionCall
|
||||
: (HAS | HASANY | HASALL) LPAREN functionParamList RPAREN
|
||||
: (HAS | HASANY | HASALL | HASNONE) LPAREN functionParamList RPAREN
|
||||
;
|
||||
|
||||
// Function parameters can be keys, single scalar values, or arrays
|
||||
@@ -182,6 +182,7 @@ OR : [Oo][Rr] ;
|
||||
HAS : [Hh][Aa][Ss] ;
|
||||
HASANY : [Hh][Aa][Ss][Aa][Nn][Yy] ;
|
||||
HASALL : [Hh][Aa][Ss][Aa][Ll][Ll] ;
|
||||
HASNONE : [Hh][Aa][Ss][Nn][Oo][Nn][Ee] ;
|
||||
|
||||
// Potential boolean constants
|
||||
BOOL
|
||||
@@ -204,7 +205,7 @@ QUOTED_TEXT
|
||||
// Keys can have letters, digits, underscores, dots, and bracket pairs
|
||||
// e.g. service.name, service.namespace, db.queries[].query_duration
|
||||
KEY
|
||||
: [a-zA-Z0-9_] [a-zA-Z0-9_.*[\]]*
|
||||
: [a-zA-Z0-9_] [a-zA-Z0-9_.[\]]*
|
||||
;
|
||||
|
||||
// Ignore whitespace
|
||||
@@ -217,4 +218,4 @@ fragment DIGIT
|
||||
: [0-9]
|
||||
;
|
||||
|
||||
FREETEXT : (~[ \t\r\n=()'"<>!,[\]])+ ;
|
||||
FREETEXT : (~[ \t\r\n=()'"<>![\]])+ ;
|
||||
@@ -3,6 +3,7 @@ package sqlalertmanagerstore
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strconv"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
@@ -190,9 +191,9 @@ func (store *config) ListAllChannels(ctx context.Context) ([]*alertmanagertypes.
|
||||
|
||||
func (store *config) GetMatchers(ctx context.Context, orgID string) (map[string][]string, error) {
|
||||
type matcher struct {
|
||||
bun.BaseModel `bun:"table:rule"`
|
||||
ID valuer.UUID `bun:"id,pk"`
|
||||
Data string `bun:"data"`
|
||||
bun.BaseModel `bun:"table:rules"`
|
||||
ID int `bun:"id,pk"`
|
||||
Data string `bun:"data"`
|
||||
}
|
||||
|
||||
matchers := []matcher{}
|
||||
@@ -212,7 +213,7 @@ func (store *config) GetMatchers(ctx context.Context, orgID string) (map[string]
|
||||
for _, matcher := range matchers {
|
||||
receivers := gjson.Get(matcher.Data, "preferredChannels").Array()
|
||||
for _, receiver := range receivers {
|
||||
matchersMap[matcher.ID.StringValue()] = append(matchersMap[matcher.ID.StringValue()], receiver.String())
|
||||
matchersMap[strconv.Itoa(matcher.ID)] = append(matchersMap[strconv.Itoa(matcher.ID)], receiver.String())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,25 +25,6 @@ type postableAlert struct {
|
||||
Receivers []string `json:"receivers"`
|
||||
}
|
||||
|
||||
func (pa *postableAlert) MarshalJSON() ([]byte, error) {
|
||||
// Marshal the embedded PostableAlert to get its JSON representation.
|
||||
alertJSON, err := json.Marshal(pa.PostableAlert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Unmarshal that JSON into a map so we can add extra fields.
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(alertJSON, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add the Receivers field.
|
||||
m["receivers"] = pa.Receivers
|
||||
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
const (
|
||||
alertsPath string = "/v1/alerts"
|
||||
routesPath string = "/v1/routes"
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package legacyalertmanager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
|
||||
"github.com/prometheus/alertmanager/api/v2/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestProvider_TestAlert(t *testing.T) {
|
||||
pa := &postableAlert{
|
||||
PostableAlert: &alertmanagertypes.PostableAlert{
|
||||
Alert: models.Alert{
|
||||
Labels: models.LabelSet{
|
||||
"alertname": "test",
|
||||
},
|
||||
GeneratorURL: "http://localhost:9090/graph?g0.expr=up&g0.tab=1",
|
||||
},
|
||||
Annotations: models.LabelSet{
|
||||
"summary": "test",
|
||||
},
|
||||
},
|
||||
Receivers: []string{"receiver1", "receiver2"},
|
||||
}
|
||||
|
||||
body, err := json.Marshal(pa)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal postable alert: %v", err)
|
||||
}
|
||||
|
||||
assert.Contains(t, string(body), "receiver1")
|
||||
assert.Contains(t, string(body), "receiver2")
|
||||
}
|
||||
@@ -160,17 +160,6 @@ func (service *Service) newServer(ctx context.Context, orgID string) (*alertmana
|
||||
return nil, err
|
||||
}
|
||||
|
||||
beforeCompareAndSelectHash := config.StoreableConfig().Hash
|
||||
config, err = service.compareAndSelectConfig(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if beforeCompareAndSelectHash == config.StoreableConfig().Hash {
|
||||
service.settings.Logger().Debug("skipping config store update for org", "orgID", orgID, "hash", config.StoreableConfig().Hash)
|
||||
return server, nil
|
||||
}
|
||||
|
||||
err = service.configStore.Set(ctx, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -202,38 +191,6 @@ func (service *Service) getConfig(ctx context.Context, orgID string) (*alertmana
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (service *Service) compareAndSelectConfig(ctx context.Context, incomingConfig *alertmanagertypes.Config) (*alertmanagertypes.Config, error) {
|
||||
channels, err := service.configStore.ListChannels(ctx, incomingConfig.StoreableConfig().OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
matchers, err := service.configStore.GetMatchers(ctx, incomingConfig.StoreableConfig().OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := alertmanagertypes.NewConfigFromChannels(service.config.Global, service.config.Route, channels, incomingConfig.StoreableConfig().OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for ruleID, receivers := range matchers {
|
||||
err = config.CreateRuleIDMatcher(ruleID, receivers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if incomingConfig.StoreableConfig().Hash != config.StoreableConfig().Hash {
|
||||
service.settings.Logger().InfoContext(ctx, "mismatch found, updating config to match channels and matchers")
|
||||
return config, nil
|
||||
}
|
||||
|
||||
return incomingConfig, nil
|
||||
|
||||
}
|
||||
|
||||
// getServer returns the server for the given orgID. It should be called with the lock held.
|
||||
func (service *Service) getServer(orgID string) (*alertmanagerserver.Server, error) {
|
||||
server, ok := service.servers[orgID]
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrylogs"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymetadata"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrymetrics"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrytraces"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
telemetryMetadataStore telemetrytypes.MetadataStore
|
||||
}
|
||||
|
||||
func NewAPI(telemetryStore telemetrystore.TelemetryStore) *API {
|
||||
|
||||
telemetryMetadataStore := telemetrymetadata.NewTelemetryMetaStore(
|
||||
telemetryStore,
|
||||
telemetrytraces.DBName,
|
||||
telemetrytraces.TagAttributesV2TableName,
|
||||
telemetrytraces.SpanIndexV3TableName,
|
||||
telemetrymetrics.DBName,
|
||||
telemetrymetrics.TimeseriesV41weekTableName,
|
||||
telemetrymetrics.TimeseriesV41weekLocalTableName,
|
||||
telemetrylogs.DBName,
|
||||
telemetrylogs.LogsV2TableName,
|
||||
telemetrylogs.TagAttributesV2TableName,
|
||||
telemetrymetadata.DBName,
|
||||
telemetrymetadata.AttributesMetadataLocalTableName,
|
||||
)
|
||||
|
||||
return &API{
|
||||
telemetryStore: telemetryStore,
|
||||
telemetryMetadataStore: telemetryMetadataStore,
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) GetFieldsKeys(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
type fieldKeysResponse struct {
|
||||
Keys map[string][]*telemetrytypes.TelemetryFieldKey `json:"keys"`
|
||||
Complete bool `json:"complete"`
|
||||
}
|
||||
|
||||
bodyBytes, _ := io.ReadAll(r.Body)
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
ctx := r.Context()
|
||||
|
||||
fieldKeySelector, err := parseFieldKeyRequest(r)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
keys, err := api.telemetryMetadataStore.GetKeys(ctx, fieldKeySelector)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
response := fieldKeysResponse{
|
||||
Keys: keys,
|
||||
Complete: len(keys) < fieldKeySelector.Limit,
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (api *API) GetFieldsValues(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
type fieldValuesResponse struct {
|
||||
Values *telemetrytypes.TelemetryFieldValues `json:"values"`
|
||||
Complete bool `json:"complete"`
|
||||
}
|
||||
|
||||
bodyBytes, _ := io.ReadAll(r.Body)
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
ctx := r.Context()
|
||||
|
||||
fieldValueSelector, err := parseFieldValueRequest(r)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
allValues, err := api.telemetryMetadataStore.GetAllValues(ctx, fieldValueSelector)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
relatedValues, err := api.telemetryMetadataStore.GetRelatedValues(ctx, fieldValueSelector)
|
||||
if err != nil {
|
||||
// we don't want to return error if we fail to get related values for some reason
|
||||
zap.L().Error("failed to get related values", zap.Error(err))
|
||||
relatedValues = []string{}
|
||||
}
|
||||
|
||||
values := &telemetrytypes.TelemetryFieldValues{
|
||||
StringValues: allValues.StringValues,
|
||||
NumberValues: allValues.NumberValues,
|
||||
RelatedValues: relatedValues,
|
||||
}
|
||||
|
||||
response := fieldValuesResponse{
|
||||
Values: values,
|
||||
Complete: len(values.StringValues) < fieldValueSelector.Limit &&
|
||||
len(values.BoolValues) < fieldValueSelector.Limit &&
|
||||
len(values.NumberValues) < fieldValueSelector.Limit &&
|
||||
len(values.RelatedValues) < fieldValueSelector.Limit,
|
||||
}
|
||||
|
||||
render.Success(w, http.StatusOK, response)
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package fields
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
func parseFieldKeyRequest(r *http.Request) (*telemetrytypes.FieldKeySelector, error) {
|
||||
var req telemetrytypes.FieldKeySelector
|
||||
var signal telemetrytypes.Signal
|
||||
var err error
|
||||
|
||||
signalStr := r.URL.Query().Get("signal")
|
||||
if signalStr != "" {
|
||||
signal = telemetrytypes.Signal{String: valuer.NewString(signalStr)}
|
||||
} else {
|
||||
signal = telemetrytypes.SignalUnspecified
|
||||
}
|
||||
|
||||
if r.URL.Query().Get("limit") != "" {
|
||||
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse limit")
|
||||
}
|
||||
req.Limit = limit
|
||||
} else {
|
||||
req.Limit = 1000
|
||||
}
|
||||
|
||||
var startUnixMilli, endUnixMilli int64
|
||||
|
||||
if r.URL.Query().Get("startUnixMilli") != "" {
|
||||
startUnixMilli, err := strconv.ParseInt(r.URL.Query().Get("startUnixMilli"), 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse startUnixMilli")
|
||||
}
|
||||
// Round down to the nearest 6 hours (21600000 milliseconds)
|
||||
startUnixMilli -= startUnixMilli % 21600000
|
||||
}
|
||||
if r.URL.Query().Get("endUnixMilli") != "" {
|
||||
endUnixMilli, err = strconv.ParseInt(r.URL.Query().Get("endUnixMilli"), 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse endUnixMilli")
|
||||
}
|
||||
}
|
||||
|
||||
// Parse fieldContext directly instead of using JSON unmarshalling.
|
||||
var fieldContext telemetrytypes.FieldContext
|
||||
fieldContextStr := r.URL.Query().Get("fieldContext")
|
||||
if fieldContextStr != "" {
|
||||
fieldContext = telemetrytypes.FieldContext{String: valuer.NewString(fieldContextStr)}
|
||||
}
|
||||
|
||||
// Parse fieldDataType directly instead of using JSON unmarshalling.
|
||||
var fieldDataType telemetrytypes.FieldDataType
|
||||
fieldDataTypeStr := r.URL.Query().Get("fieldDataType")
|
||||
if fieldDataTypeStr != "" {
|
||||
fieldDataType = telemetrytypes.FieldDataType{String: valuer.NewString(fieldDataTypeStr)}
|
||||
}
|
||||
|
||||
metricName := r.URL.Query().Get("metricName")
|
||||
var metricContext *telemetrytypes.MetricContext
|
||||
if metricName != "" {
|
||||
metricContext = &telemetrytypes.MetricContext{
|
||||
MetricName: metricName,
|
||||
}
|
||||
}
|
||||
|
||||
name := r.URL.Query().Get("name")
|
||||
|
||||
req = telemetrytypes.FieldKeySelector{
|
||||
StartUnixMilli: startUnixMilli,
|
||||
EndUnixMilli: endUnixMilli,
|
||||
Signal: signal,
|
||||
Name: name,
|
||||
FieldContext: fieldContext,
|
||||
FieldDataType: fieldDataType,
|
||||
Limit: req.Limit,
|
||||
SelectorMatchType: telemetrytypes.FieldSelectorMatchTypeFuzzy,
|
||||
MetricContext: metricContext,
|
||||
}
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func parseFieldValueRequest(r *http.Request) (*telemetrytypes.FieldValueSelector, error) {
|
||||
keySelector, err := parseFieldKeyRequest(r)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse field key request")
|
||||
}
|
||||
|
||||
existingQuery := r.URL.Query().Get("existingQuery")
|
||||
value := r.URL.Query().Get("value")
|
||||
|
||||
// Parse limit for fieldValue request, fallback to default 50 if parsing fails.
|
||||
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
if err != nil {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
req := telemetrytypes.FieldValueSelector{
|
||||
FieldKeySelector: keySelector,
|
||||
ExistingQuery: existingQuery,
|
||||
Value: value,
|
||||
Limit: limit,
|
||||
}
|
||||
|
||||
return &req, nil
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user