Compare commits
89 Commits
v0.78.0-cl
...
button-com
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
661a05f4a4 | ||
|
|
940313d28b | ||
|
|
9815ec7d81 | ||
|
|
a7cad0f1a5 | ||
|
|
a624b4758d | ||
|
|
ee5684b130 | ||
|
|
2f8da5957b | ||
|
|
3f6f77d0e2 | ||
|
|
5bceffbeaa | ||
|
|
9e449e2858 | ||
|
|
b60588a749 | ||
|
|
c322657666 | ||
|
|
a1846c008a | ||
|
|
a6824db622 | ||
|
|
e6f69aa74c | ||
|
|
a9c09f33cb | ||
|
|
9eb2196617 | ||
|
|
131759ec96 | ||
|
|
365a3e250f | ||
|
|
f3a1f3cc20 | ||
|
|
ae509b4ae9 | ||
|
|
43e2be0333 | ||
|
|
20a40b33ce | ||
|
|
a9b07c4b47 | ||
|
|
2a5c7cc0ab | ||
|
|
afb18b8142 | ||
|
|
9a580915e6 | ||
|
|
0944af3d31 | ||
|
|
9338efcefc | ||
|
|
6b9e0ce799 | ||
|
|
d4c3c24849 | ||
|
|
30d935a768 | ||
|
|
073d42c416 | ||
|
|
f11b9644cf | ||
|
|
87922e9577 | ||
|
|
8412727414 | ||
|
|
f0a95503d9 | ||
|
|
16e0fa2eef | ||
|
|
2fa944d254 | ||
|
|
b0d19035a4 | ||
|
|
054dea366e | ||
|
|
aaf0b597dc | ||
|
|
19372c8194 | ||
|
|
eb74adad44 | ||
|
|
d5c04e1342 | ||
|
|
2b9632c8fd | ||
|
|
24920ae903 | ||
|
|
6f096632a2 | ||
|
|
a42eacec4b | ||
|
|
e723399f7f | ||
|
|
48936bed9b | ||
|
|
ee70474cc7 | ||
|
|
c3fa7144ee | ||
|
|
5dd02a5b8e | ||
|
|
c0f01e4cb9 | ||
|
|
fed84cb50a | ||
|
|
80545c4d07 | ||
|
|
0b1faec092 | ||
|
|
ba6f31b1c3 | ||
|
|
eed92978a4 | ||
|
|
41cbd316b5 | ||
|
|
8d7d33393d | ||
|
|
8d143b44b1 | ||
|
|
423aebd6eb | ||
|
|
8d630707af | ||
|
|
a5b52431b7 | ||
|
|
0138d757c8 | ||
|
|
844195b84f | ||
|
|
8ff05b2e8f | ||
|
|
c8c56c544e | ||
|
|
1c43655336 | ||
|
|
c269c8c6b8 | ||
|
|
3142b6cc6d | ||
|
|
58e141685a | ||
|
|
e17f63a50c | ||
|
|
838ef5dcc5 | ||
|
|
e53d3d1269 | ||
|
|
2330420c0d | ||
|
|
65ac277074 | ||
|
|
b7982ca348 | ||
|
|
2748b49a44 | ||
|
|
7345027762 | ||
|
|
68f874e433 | ||
|
|
54a82b1664 | ||
|
|
93dc585145 | ||
|
|
6a143efd2c | ||
|
|
0116eb20ab | ||
|
|
79e9d1b357 | ||
|
|
b89ce82e25 |
27
.devenv/docker/postgres/compose.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
services:
|
||||
|
||||
postgres:
|
||||
image: postgres:15
|
||||
container_name: postgres
|
||||
environment:
|
||||
POSTGRES_DB: signoz
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: password
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"pg_isready",
|
||||
"-d",
|
||||
"signoz",
|
||||
"-U",
|
||||
"postgres"
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
restart: on-failure
|
||||
ports:
|
||||
- "127.0.0.1:5432:5432/tcp"
|
||||
volumes:
|
||||
- ${PWD}/fs/tmp/var/lib/postgresql/data/:/var/lib/postgresql/data/
|
||||
@@ -1,6 +1,7 @@
|
||||
.git
|
||||
.github
|
||||
.vscode
|
||||
.devenv
|
||||
README.md
|
||||
deploy
|
||||
sample-apps
|
||||
|
||||
42
.github/workflows/README.md
vendored
@@ -1,42 +0,0 @@
|
||||
# Github actions
|
||||
|
||||
## Testing the UI manually on each PR
|
||||
|
||||
First we need to make sure the UI is ready
|
||||
* Check the `Start tunnel` step in `e2e-k8s/deploy-on-k3s-cluster` job and make sure you see `your url is: https://pull-<number>-signoz.loca.lt`
|
||||
* This job will run until the PR is merged or closed to keep the local tunneling alive
|
||||
- github will cancel this job if the PR wasn't merged after 6h
|
||||
- if the job was cancel, go to the action and press `Re-run all jobs`
|
||||
|
||||
Now you can open your browser at https://pull-<number>-signoz.loca.lt and check the UI.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
To run GitHub workflow, a few environment variables needs to add in GitHub secrets
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th> Variables </th>
|
||||
<th> Description </th>
|
||||
<th> Example </th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> REPONAME </td>
|
||||
<td> Provide the DockerHub user/organisation name of the image. </td>
|
||||
<td> signoz</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> DOCKERHUB_USERNAME </td>
|
||||
<td> Docker hub username </td>
|
||||
<td> signoz</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> DOCKERHUB_TOKEN </td>
|
||||
<td> Docker hub password/token with push permission </td>
|
||||
<td> **** </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td> SONAR_TOKEN </td>
|
||||
<td> <a href="https://sonarcloud.io">SonarCloud</a> token </td>
|
||||
<td> **** </td>
|
||||
</tr>
|
||||
81
.github/workflows/build-community.yaml
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
name: build-community
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
PRIMUS_HOME: .primus
|
||||
MAKE: make --no-print-directory --makefile=.primus/src/make/main.mk
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.build-info.outputs.version }}
|
||||
hash: ${{ steps.build-info.outputs.hash }}
|
||||
time: ${{ steps.build-info.outputs.time }}
|
||||
branch: ${{ steps.build-info.outputs.branch }}
|
||||
steps:
|
||||
- name: self-checkout
|
||||
uses: actions/checkout@v4
|
||||
- id: token
|
||||
name: github-token-gen
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.PRIMUS_APP_ID }}
|
||||
private-key: ${{ secrets.PRIMUS_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
- name: primus-checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: signoz/primus
|
||||
ref: main
|
||||
path: .primus
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
- name: build-info
|
||||
run: |
|
||||
echo "version=$($MAKE info-version)" >> $GITHUB_OUTPUT
|
||||
echo "hash=$($MAKE info-commit-short)" >> $GITHUB_OUTPUT
|
||||
echo "time=$($MAKE info-timestamp)" >> $GITHUB_OUTPUT
|
||||
echo "branch=$($MAKE info-branch)" >> $GITHUB_OUTPUT
|
||||
js-build:
|
||||
uses: signoz/primus.workflows/.github/workflows/js-build.yaml@main
|
||||
needs: prepare
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
JS_SRC: frontend
|
||||
JS_OUTPUT_ARTIFACT_CACHE_KEY: community-jsbuild-${{ github.sha }}
|
||||
JS_OUTPUT_ARTIFACT_PATH: frontend/build
|
||||
DOCKER_BUILD: false
|
||||
DOCKER_MANIFEST: false
|
||||
go-build:
|
||||
uses: signoz/primus.workflows/.github/workflows/go-build.yaml@main
|
||||
needs: [prepare, js-build]
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_NAME: signoz-community
|
||||
GO_INPUT_ARTIFACT_CACHE_KEY: community-jsbuild-${{ github.sha }}
|
||||
GO_INPUT_ARTIFACT_PATH: frontend/build
|
||||
GO_BUILD_CONTEXT: ./pkg/query-service
|
||||
GO_BUILD_FLAGS: >-
|
||||
-tags timetzdata
|
||||
-ldflags='-linkmode external -extldflags \"-static\" -s -w
|
||||
-X github.com/SigNoz/signoz/pkg/version.version=${{ needs.prepare.outputs.version }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.variant=community
|
||||
-X github.com/SigNoz/signoz/pkg/version.hash=${{ needs.prepare.outputs.hash }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.time=${{ needs.prepare.outputs.time }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}'
|
||||
GO_CGO_ENABLED: 1
|
||||
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
|
||||
DOCKER_DOCKERFILE_PATH: ./pkg/query-service/Dockerfile.multi-arch
|
||||
DOCKER_MANIFEST: true
|
||||
DOCKER_PROVIDERS: dockerhub
|
||||
113
.github/workflows/build-enterprise.yaml
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
name: build-enterprise
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
PRIMUS_HOME: .primus
|
||||
MAKE: make --no-print-directory --makefile=.primus/src/make/main.mk
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
docker_providers: ${{ steps.set-docker-providers.outputs.providers }}
|
||||
version: ${{ steps.build-info.outputs.version }}
|
||||
hash: ${{ steps.build-info.outputs.hash }}
|
||||
time: ${{ steps.build-info.outputs.time }}
|
||||
branch: ${{ steps.build-info.outputs.branch }}
|
||||
steps:
|
||||
- name: self-checkout
|
||||
uses: actions/checkout@v4
|
||||
- id: token
|
||||
name: github-token-gen
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.PRIMUS_APP_ID }}
|
||||
private-key: ${{ secrets.PRIMUS_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
- name: primus-checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: signoz/primus
|
||||
ref: main
|
||||
path: .primus
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
- name: build-info
|
||||
id: build-info
|
||||
run: |
|
||||
echo "version=$($MAKE info-version)" >> $GITHUB_OUTPUT
|
||||
echo "hash=$($MAKE info-commit-short)" >> $GITHUB_OUTPUT
|
||||
echo "time=$($MAKE info-timestamp)" >> $GITHUB_OUTPUT
|
||||
echo "branch=$($MAKE info-branch)" >> $GITHUB_OUTPUT
|
||||
- name: set-docker-providers
|
||||
id: set-docker-providers
|
||||
run: |
|
||||
if [[ ${{ github.event.ref }} =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+$ || ${{ github.event.ref }} =~ ^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
|
||||
echo "providers=dockerhub gcp" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "providers=gcp" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: create-dotenv
|
||||
run: |
|
||||
mkdir -p frontend
|
||||
echo 'CI=1' > 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: enterprise-dotenv-${{ github.sha }}
|
||||
js-build:
|
||||
uses: signoz/primus.workflows/.github/workflows/js-build.yaml@main
|
||||
needs: prepare
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
JS_SRC: frontend
|
||||
JS_INPUT_ARTIFACT_CACHE_KEY: enterprise-dotenv-${{ github.sha }}
|
||||
JS_INPUT_ARTIFACT_PATH: frontend/.env
|
||||
JS_OUTPUT_ARTIFACT_CACHE_KEY: enterprise-jsbuild-${{ github.sha }}
|
||||
JS_OUTPUT_ARTIFACT_PATH: frontend/build
|
||||
DOCKER_BUILD: false
|
||||
DOCKER_MANIFEST: false
|
||||
go-build:
|
||||
uses: signoz/primus.workflows/.github/workflows/go-build.yaml@main
|
||||
needs: [prepare, js-build]
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_INPUT_ARTIFACT_CACHE_KEY: enterprise-jsbuild-${{ github.sha }}
|
||||
GO_INPUT_ARTIFACT_PATH: frontend/build
|
||||
GO_BUILD_CONTEXT: ./ee/query-service
|
||||
GO_BUILD_FLAGS: >-
|
||||
-tags timetzdata
|
||||
-ldflags='-linkmode external -extldflags \"-static\" -s -w
|
||||
-X github.com/SigNoz/signoz/pkg/version.version=${{ needs.prepare.outputs.version }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.variant=enterprise
|
||||
-X github.com/SigNoz/signoz/pkg/version.hash=${{ needs.prepare.outputs.hash }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.time=${{ needs.prepare.outputs.time }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.signoz.cloud
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1'
|
||||
GO_CGO_ENABLED: 1
|
||||
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
|
||||
DOCKER_DOCKERFILE_PATH: ./ee/query-service/Dockerfile.multi-arch
|
||||
DOCKER_MANIFEST: true
|
||||
DOCKER_PROVIDERS: ${{ needs.prepare.outputs.docker_providers }}
|
||||
122
.github/workflows/build-staging.yaml
vendored
Normal file
@@ -0,0 +1,122 @@
|
||||
name: build-staging
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
PRIMUS_HOME: .primus
|
||||
MAKE: make --no-print-directory --makefile=.primus/src/make/main.mk
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.event.label.name, 'staging:') || github.event.ref == 'refs/heads/main' }}
|
||||
outputs:
|
||||
version: ${{ steps.build-info.outputs.version }}
|
||||
hash: ${{ steps.build-info.outputs.hash }}
|
||||
time: ${{ steps.build-info.outputs.time }}
|
||||
branch: ${{ steps.build-info.outputs.branch }}
|
||||
deployment: ${{ steps.build-info.outputs.deployment }}
|
||||
steps:
|
||||
- name: self-checkout
|
||||
uses: actions/checkout@v4
|
||||
- id: token
|
||||
name: github-token-gen
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.PRIMUS_APP_ID }}
|
||||
private-key: ${{ secrets.PRIMUS_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
- name: primus-checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: signoz/primus
|
||||
ref: main
|
||||
path: .primus
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
- name: build-info
|
||||
id: build-info
|
||||
run: |
|
||||
echo "version=$($MAKE info-version)" >> $GITHUB_OUTPUT
|
||||
echo "hash=$($MAKE info-commit-short)" >> $GITHUB_OUTPUT
|
||||
echo "time=$($MAKE info-timestamp)" >> $GITHUB_OUTPUT
|
||||
echo "branch=$($MAKE info-branch)" >> $GITHUB_OUTPUT
|
||||
|
||||
staging_label="${{ github.event.label.name }}"
|
||||
if [[ "${staging_label}" == "staging:"* ]]; then
|
||||
deployment=${staging_label#"staging:"}
|
||||
elif [[ "${{ github.event.ref }}" == "refs/heads/main" ]]; then
|
||||
deployment="staging"
|
||||
else
|
||||
echo "error: not able to determine deployment - please verify the PR label or the branch"
|
||||
exit 1
|
||||
fi
|
||||
echo "deployment=${deployment}" >> $GITHUB_OUTPUT
|
||||
- name: create-dotenv
|
||||
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
|
||||
- name: cache-dotenv
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: frontend/.env
|
||||
key: staging-dotenv-${{ github.sha }}
|
||||
js-build:
|
||||
uses: signoz/primus.workflows/.github/workflows/js-build.yaml@main
|
||||
needs: prepare
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
JS_SRC: frontend
|
||||
JS_INPUT_ARTIFACT_CACHE_KEY: staging-dotenv-${{ github.sha }}
|
||||
JS_INPUT_ARTIFACT_PATH: frontend/.env
|
||||
JS_OUTPUT_ARTIFACT_CACHE_KEY: staging-jsbuild-${{ github.sha }}
|
||||
JS_OUTPUT_ARTIFACT_PATH: frontend/build
|
||||
DOCKER_BUILD: false
|
||||
DOCKER_MANIFEST: false
|
||||
go-build:
|
||||
uses: signoz/primus.workflows/.github/workflows/go-build.yaml@main
|
||||
needs: [prepare, js-build]
|
||||
secrets: inherit
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GO_INPUT_ARTIFACT_CACHE_KEY: staging-jsbuild-${{ github.sha }}
|
||||
GO_INPUT_ARTIFACT_PATH: frontend/build
|
||||
GO_BUILD_CONTEXT: ./ee/query-service
|
||||
GO_BUILD_FLAGS: >-
|
||||
-tags timetzdata
|
||||
-ldflags='-linkmode external -extldflags \"-static\" -s -w
|
||||
-X github.com/SigNoz/signoz/pkg/version.version=${{ needs.prepare.outputs.version }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.variant=enterprise
|
||||
-X github.com/SigNoz/signoz/pkg/version.hash=${{ needs.prepare.outputs.hash }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.time=${{ needs.prepare.outputs.time }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.staging.signoz.cloud
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.staging.signoz.cloud/api/v1'
|
||||
GO_CGO_ENABLED: 1
|
||||
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
|
||||
DOCKER_DOCKERFILE_PATH: ./ee/query-service/Dockerfile.multi-arch
|
||||
DOCKER_MANIFEST: true
|
||||
DOCKER_PROVIDERS: gcp
|
||||
staging:
|
||||
if: ${{ contains(github.event.label.name, 'staging:') || github.event.ref == 'refs/heads/main' }}
|
||||
uses: signoz/primus.workflows/.github/workflows/github-trigger.yaml@main
|
||||
secrets: inherit
|
||||
needs: [prepare, go-build]
|
||||
with:
|
||||
PRIMUS_REF: main
|
||||
GITHUB_ENVIRONMENT: staging
|
||||
GITHUB_SILENT: true
|
||||
GITHUB_REPOSITORY_NAME: charts-saas-v3-staging
|
||||
GITHUB_EVENT_NAME: releaser
|
||||
GITHUB_EVENT_PAYLOAD: "{\"deployment\": \"${{ needs.prepare.outputs.deployment }}\", \"signoz_version\": \"${{ needs.prepare.outputs.version }}\"}"
|
||||
122
.github/workflows/build.yaml
vendored
@@ -1,122 +0,0 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
enterprise:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: setup
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22"
|
||||
- name: setup-qemu
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: setup-buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
version: latest
|
||||
- name: docker-login
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: create-env-file
|
||||
run: |
|
||||
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: github-ref-info
|
||||
shell: bash
|
||||
run: |
|
||||
GH_REF=${{ github.ref }}
|
||||
if [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||
PREFIX="refs/tags/"
|
||||
echo "GH_IS_TAG=true" >> $GITHUB_ENV
|
||||
echo "GH_TAG=${GH_REF#$PREFIX}" >> $GITHUB_ENV
|
||||
else
|
||||
PREFIX="refs/heads/"
|
||||
echo "GH_IS_TAG=false" >> $GITHUB_ENV
|
||||
echo "GH_BRANCH_NAME=${GH_REF#$PREFIX}" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: set-version
|
||||
run: |
|
||||
if [ '${{ env.GH_IS_TAG }}' == 'true' ]; then
|
||||
echo "VERSION=${{ env.GH_TAG }}" >> $GITHUB_ENV
|
||||
elif [ '${{ env.GH_BRANCH_NAME }}' == 'main' ]; then
|
||||
echo "VERSION=latest" >> $GITHUB_ENV
|
||||
else
|
||||
echo "VERSION=${{ env.GH_BRANCH_NAME }}" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: cross-compilation-tools
|
||||
run: |
|
||||
set -ex
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu musl-tools
|
||||
- name: publish
|
||||
run: make docker-buildx-enterprise
|
||||
|
||||
community:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: setup-go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22"
|
||||
- name: setup-qemu
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: setup-buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
version: latest
|
||||
- name: docker-login
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: github-ref-info
|
||||
shell: bash
|
||||
run: |
|
||||
GH_REF=${{ github.ref }}
|
||||
if [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||
PREFIX="refs/tags/"
|
||||
echo "GH_IS_TAG=true" >> $GITHUB_ENV
|
||||
echo "GH_TAG=${GH_REF#$PREFIX}" >> $GITHUB_ENV
|
||||
else
|
||||
PREFIX="refs/heads/"
|
||||
echo "GH_IS_TAG=false" >> $GITHUB_ENV
|
||||
echo "GH_BRANCH_NAME=${GH_REF#$PREFIX}" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: set-version
|
||||
run: |
|
||||
if [ '${{ env.GH_IS_TAG }}' == 'true' ]; then
|
||||
echo "VERSION=${{ env.GH_TAG }}" >> $GITHUB_ENV
|
||||
elif [ '${{ env.GH_BRANCH_NAME }}' == 'main' ]; then
|
||||
echo "VERSION=latest" >> $GITHUB_ENV
|
||||
else
|
||||
echo "VERSION=${{ env.GH_BRANCH_NAME }}" >> $GITHUB_ENV
|
||||
fi
|
||||
- name: cross-compilation-tools
|
||||
run: |
|
||||
set -ex
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu musl-tools
|
||||
- name: publish
|
||||
run: make docker-buildx-community
|
||||
53
.github/workflows/integrationci.yaml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
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 \
|
||||
--basetemp=./tmp/ \
|
||||
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
@@ -1,9 +1,9 @@
|
||||
name: prereleaser
|
||||
|
||||
on:
|
||||
# schedule every wednesday 9:30 AM UTC (3pm IST)
|
||||
# schedule every wednesday 6:30 AM UTC (12:00 PM IST)
|
||||
schedule:
|
||||
- cron: '30 9 * * 3'
|
||||
- cron: '30 6 * * 3'
|
||||
|
||||
# allow manual triggering of the workflow by a maintainer
|
||||
workflow_dispatch:
|
||||
|
||||
16
.github/workflows/remove-label.yaml
vendored
@@ -1,16 +0,0 @@
|
||||
name: remove-label
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [synchronize]
|
||||
|
||||
jobs:
|
||||
remove:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Remove label testing-deploy from PR
|
||||
uses: buildsville/add-remove-label@v2.0.0
|
||||
with:
|
||||
label: testing-deploy
|
||||
type: remove
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
56
.github/workflows/staging-deployment.yaml
vendored
@@ -1,56 +0,0 @@
|
||||
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 OTELCOL_TAG="main"
|
||||
export PATH="/usr/local/go/bin/:$PATH" # needed for Golang to work
|
||||
export KAFKA_SPAN_EVAL="true"
|
||||
docker system prune --force
|
||||
docker pull signoz/signoz-otel-collector:main
|
||||
docker pull signoz/signoz-schema-migrator:main
|
||||
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
@@ -1,56 +0,0 @@
|
||||
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
|
||||
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
@@ -80,6 +80,153 @@ 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
|
||||
17
.versions/alpine
Normal file
@@ -0,0 +1,17 @@
|
||||
#### Auto generated by make docker-version-alpine. DO NOT EDIT! ####
|
||||
amd64=029a752048e32e843bd6defe3841186fb8d19a28dae8ec287f433bb9d6d1ad85
|
||||
unknown=5fea95373b9ec85974843f31446fa6a9df4492dddae4e1cb056193c34a20a5be
|
||||
arm=b4aef1a899e0271f06d948c9a8fa626ecdb2202d3a178bc14775dd559e23df8e
|
||||
unknown=a4d1e27e63a9d6353046eb25a2f0ec02945012b217f4364cd83a73fe6dfb0b15
|
||||
arm=4fdafe217d0922f3c3e2b4f64cf043f8403a4636685cd9c51fea2cbd1f419740
|
||||
unknown=7f21ac2018d95b2c51a5779c1d5ca6c327504adc3b0fdc747a6725d30b3f13c2
|
||||
arm64=ea3c5a9671f7b3f7eb47eab06f73bc6591df978b0d5955689a9e6f943aa368c0
|
||||
unknown=a8ba68c1a9e6eea8041b4b8f996c235163440808b9654a865976fdcbede0f433
|
||||
386=dea9f02e103e837849f984d5679305c758aba7fea1b95b7766218597f61a05ab
|
||||
unknown=3c6629bec05c8273a927d46b77428bf4a378dad911a0ae284887becdc149b734
|
||||
ppc64le=0880443bffa028dfbbc4094a32dd6b7ac25684e4c0a3d50da9e0acae355c5eaf
|
||||
unknown=bb48308f976b266e3ab39bbf9af84521959bd9c295d3c763690cf41f8df2a626
|
||||
riscv64=d76e6fbe348ff20c2931bb7f101e49379648e026de95dd37f96e00ce1909dcf7
|
||||
unknown=dd807544365f6dc187cbe6de0806adce2ea9de3e7124717d1d8e8b7a18b77b64
|
||||
s390x=b815fadf80495594eb6296a6af0bc647ae5f193e0044e07acec7e5b378c9ce2d
|
||||
unknown=74681be74a280a88abb53ff1e048eb1fb624b30d0066730df6d8afd02ba82e01
|
||||
37
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,6 +23,7 @@ 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)
|
||||
|
||||
@@ -55,6 +56,11 @@ devenv-clickhouse: ## Run clickhouse in devenv
|
||||
@cd .devenv/docker/clickhouse; \
|
||||
docker compose -f compose.yaml up -d
|
||||
|
||||
.PHONY: devenv-postgres
|
||||
devenv-postgres: ## Run postgres in devenv
|
||||
@cd .devenv/docker/postgres; \
|
||||
docker compose -f compose.yaml up -d
|
||||
|
||||
##############################################################
|
||||
# go commands
|
||||
##############################################################
|
||||
@@ -119,6 +125,18 @@ $(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
|
||||
##############################################################
|
||||
@@ -167,3 +185,20 @@ 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/
|
||||
@@ -103,6 +103,13 @@ telemetrystore:
|
||||
clickhouse:
|
||||
# The DSN to use for clickhouse.
|
||||
dsn: tcp://localhost:9000
|
||||
# The query settings for clickhouse.
|
||||
settings:
|
||||
max_execution_time: 0
|
||||
max_execution_time_leaf: 0
|
||||
timeout_before_checking_execution_speed: 0
|
||||
max_bytes_to_read: 0
|
||||
max_result_rows_for_ch_query: 0
|
||||
|
||||
##################### Prometheus #####################
|
||||
prometheus:
|
||||
|
||||
@@ -174,7 +174,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.77.0
|
||||
image: signoz/signoz:v0.80.0
|
||||
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.37
|
||||
image: signoz/signoz-otel-collector:v0.111.39
|
||||
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.37
|
||||
image: signoz/signoz-schema-migrator:v0.111.39
|
||||
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.77.0
|
||||
image: signoz/signoz:v0.80.0
|
||||
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.37
|
||||
image: signoz/signoz-otel-collector:v0.111.39
|
||||
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.37
|
||||
image: signoz/signoz-schema-migrator:v0.111.39
|
||||
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.77.0}
|
||||
image: signoz/signoz:${VERSION:-v0.80.0}
|
||||
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.37}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.39}
|
||||
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.37}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.39}
|
||||
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.37}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.39}
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
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.77.0}
|
||||
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.37}
|
||||
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.37}
|
||||
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.37}
|
||||
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.77.0}
|
||||
image: signoz/signoz:${VERSION:-v0.80.0}
|
||||
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.37}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.111.39}
|
||||
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.37}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.39}
|
||||
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.37}
|
||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-v0.111.39}
|
||||
container_name: schema-migrator-async
|
||||
command:
|
||||
- async
|
||||
|
||||
@@ -61,11 +61,17 @@ func (p *Pat) Wrap(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
role, err := authtypes.NewRole(user.Role)
|
||||
if err != nil {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
jwt := authtypes.Claims{
|
||||
UserID: user.ID,
|
||||
GroupID: user.GroupID,
|
||||
Email: user.Email,
|
||||
OrgID: user.OrgID,
|
||||
UserID: user.ID,
|
||||
Role: role,
|
||||
Email: user.Email,
|
||||
OrgID: user.OrgID,
|
||||
}
|
||||
|
||||
ctx = authtypes.NewContextWithClaims(ctx, jwt)
|
||||
|
||||
@@ -18,4 +18,4 @@ COPY frontend/build/ /etc/signoz/web/
|
||||
RUN chmod 755 /root /root/signoz
|
||||
|
||||
ENTRYPOINT ["./signoz"]
|
||||
CMD ["-config", "/root/config/prometheus.yml"]
|
||||
CMD ["-config", "/root/config/prometheus.yml"]
|
||||
36
ee/query-service/Dockerfile.integration
Normal file
@@ -0,0 +1,36 @@
|
||||
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"]
|
||||
22
ee/query-service/Dockerfile.multi-arch
Normal file
@@ -0,0 +1,22 @@
|
||||
ARG ALPINE_SHA="pass-a-valid-docker-sha-otherwise-this-will-fail"
|
||||
|
||||
FROM alpine@sha256:${ALPINE_SHA}
|
||||
LABEL maintainer="signoz"
|
||||
WORKDIR /root
|
||||
|
||||
ARG OS="linux"
|
||||
ARG ARCH
|
||||
|
||||
RUN apk update && \
|
||||
apk add ca-certificates && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
COPY ./target/${OS}-${ARCH}/signoz /root/signoz
|
||||
COPY ./conf/prometheus.yml /root/config/prometheus.yml
|
||||
COPY ./templates/email /root/templates
|
||||
COPY frontend/build/ /etc/signoz/web/
|
||||
|
||||
RUN chmod 755 /root /root/signoz
|
||||
|
||||
ENTRYPOINT ["./signoz"]
|
||||
CMD ["-config", "/root/config/prometheus.yml"]
|
||||
@@ -28,11 +28,10 @@ func NewDailyProvider(opts ...GenericProviderOption[*DailyProvider]) *DailyProvi
|
||||
}
|
||||
|
||||
dp.querierV2 = querierV2.NewQuerier(querierV2.QuerierOptions{
|
||||
Reader: dp.reader,
|
||||
Cache: dp.cache,
|
||||
KeyGenerator: queryBuilder.NewKeyGenerator(),
|
||||
FluxInterval: dp.fluxInterval,
|
||||
FeatureLookup: dp.ff,
|
||||
Reader: dp.reader,
|
||||
Cache: dp.cache,
|
||||
KeyGenerator: queryBuilder.NewKeyGenerator(),
|
||||
FluxInterval: dp.fluxInterval,
|
||||
})
|
||||
|
||||
return dp
|
||||
|
||||
@@ -28,11 +28,10 @@ func NewHourlyProvider(opts ...GenericProviderOption[*HourlyProvider]) *HourlyPr
|
||||
}
|
||||
|
||||
hp.querierV2 = querierV2.NewQuerier(querierV2.QuerierOptions{
|
||||
Reader: hp.reader,
|
||||
Cache: hp.cache,
|
||||
KeyGenerator: queryBuilder.NewKeyGenerator(),
|
||||
FluxInterval: hp.fluxInterval,
|
||||
FeatureLookup: hp.ff,
|
||||
Reader: hp.reader,
|
||||
Cache: hp.cache,
|
||||
KeyGenerator: queryBuilder.NewKeyGenerator(),
|
||||
FluxInterval: hp.fluxInterval,
|
||||
})
|
||||
|
||||
return hp
|
||||
|
||||
@@ -38,12 +38,6 @@ func WithKeyGenerator[T BaseProvider](keyGenerator cache.KeyGenerator) GenericPr
|
||||
}
|
||||
}
|
||||
|
||||
func WithFeatureLookup[T BaseProvider](ff interfaces.FeatureLookup) GenericProviderOption[T] {
|
||||
return func(p T) {
|
||||
p.GetBaseSeasonalProvider().ff = ff
|
||||
}
|
||||
}
|
||||
|
||||
func WithReader[T BaseProvider](reader interfaces.Reader) GenericProviderOption[T] {
|
||||
return func(p T) {
|
||||
p.GetBaseSeasonalProvider().reader = reader
|
||||
@@ -56,7 +50,6 @@ type BaseSeasonalProvider struct {
|
||||
fluxInterval time.Duration
|
||||
cache cache.Cache
|
||||
keyGenerator cache.KeyGenerator
|
||||
ff interfaces.FeatureLookup
|
||||
}
|
||||
|
||||
func (p *BaseSeasonalProvider) getQueryParams(req *GetAnomaliesRequest) *anomalyQueryParams {
|
||||
|
||||
@@ -27,11 +27,10 @@ func NewWeeklyProvider(opts ...GenericProviderOption[*WeeklyProvider]) *WeeklyPr
|
||||
}
|
||||
|
||||
wp.querierV2 = querierV2.NewQuerier(querierV2.QuerierOptions{
|
||||
Reader: wp.reader,
|
||||
Cache: wp.cache,
|
||||
KeyGenerator: queryBuilder.NewKeyGenerator(),
|
||||
FluxInterval: wp.fluxInterval,
|
||||
FeatureLookup: wp.ff,
|
||||
Reader: wp.reader,
|
||||
Cache: wp.cache,
|
||||
KeyGenerator: queryBuilder.NewKeyGenerator(),
|
||||
FluxInterval: wp.fluxInterval,
|
||||
})
|
||||
|
||||
return wp
|
||||
|
||||
@@ -11,6 +11,8 @@ 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/http/middleware"
|
||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
||||
@@ -27,7 +29,6 @@ import (
|
||||
|
||||
type APIHandlerOptions struct {
|
||||
DataConnector interfaces.DataConnector
|
||||
SkipConfig *basemodel.SkipConfig
|
||||
PreferSpanMetrics bool
|
||||
AppDao dao.ModelDao
|
||||
RulesManager *rules.Manager
|
||||
@@ -54,10 +55,8 @@ type APIHandler struct {
|
||||
|
||||
// NewAPIHandler returns an APIHandler
|
||||
func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler, error) {
|
||||
|
||||
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
|
||||
Reader: opts.DataConnector,
|
||||
SkipConfig: opts.SkipConfig,
|
||||
PreferSpanMetrics: opts.PreferSpanMetrics,
|
||||
AppDao: opts.AppDao,
|
||||
RuleManager: opts.RulesManager,
|
||||
@@ -70,6 +69,7 @@ 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,
|
||||
})
|
||||
|
||||
@@ -114,7 +114,7 @@ func (ah *APIHandler) CheckFeature(f string) bool {
|
||||
}
|
||||
|
||||
// RegisterRoutes registers routes for this handler on the given router
|
||||
func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddleware) {
|
||||
func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
// note: add ee override methods first
|
||||
|
||||
// routes available only in ee version
|
||||
@@ -157,7 +157,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
|
||||
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(ah.getInvite)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/register", am.OpenAccess(ah.registerUser)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/login", am.OpenAccess(ah.loginUser)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/traces/{traceId}", am.ViewAccess(ah.searchTraces)).Methods(http.MethodGet)
|
||||
|
||||
// PAT APIs
|
||||
router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.createPAT)).Methods(http.MethodPost)
|
||||
@@ -188,7 +187,7 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
|
||||
|
||||
}
|
||||
|
||||
func (ah *APIHandler) RegisterCloudIntegrationsRoutes(router *mux.Router, am *baseapp.AuthMiddleware) {
|
||||
func (ah *APIHandler) RegisterCloudIntegrationsRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||
|
||||
ah.APIHandler.RegisterCloudIntegrationsRoutes(router, am)
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
_, registerError := baseauth.Register(ctx, req, ah.Signoz.Alertmanager)
|
||||
_, registerError := baseauth.Register(ctx, req, ah.Signoz.Alertmanager, ah.Signoz.Modules.Organization)
|
||||
if !registerError.IsNil() {
|
||||
RespondError(w, apierr, nil)
|
||||
return
|
||||
@@ -151,9 +151,8 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
|
||||
func (ah *APIHandler) getInvite(w http.ResponseWriter, r *http.Request) {
|
||||
token := mux.Vars(r)["token"]
|
||||
sourceUrl := r.URL.Query().Get("ref")
|
||||
ctx := context.Background()
|
||||
|
||||
inviteObject, err := baseauth.GetInvite(context.Background(), token)
|
||||
inviteObject, err := baseauth.GetInvite(r.Context(), token, ah.Signoz.Modules.Organization)
|
||||
if err != nil {
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
return
|
||||
@@ -163,7 +162,7 @@ func (ah *APIHandler) getInvite(w http.ResponseWriter, r *http.Request) {
|
||||
InvitationResponseObject: inviteObject,
|
||||
}
|
||||
|
||||
precheck, apierr := ah.AppDao().PrecheckLogin(ctx, inviteObject.Email, sourceUrl)
|
||||
precheck, apierr := ah.AppDao().PrecheckLogin(r.Context(), inviteObject.Email, sourceUrl)
|
||||
resp.Precheck = precheck
|
||||
|
||||
if apierr != nil {
|
||||
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||
eeTypes "github.com/SigNoz/signoz/ee/types"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/auth"
|
||||
baseconstants "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/dao"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
"go.uber.org/zap"
|
||||
@@ -30,6 +30,12 @@ type CloudIntegrationConnectionParamsResponse struct {
|
||||
}
|
||||
|
||||
func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseWriter, r *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
cloudProvider := mux.Vars(r)["cloudProvider"]
|
||||
if cloudProvider != "aws" {
|
||||
RespondError(w, basemodel.BadRequest(fmt.Errorf(
|
||||
@@ -38,15 +44,7 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
|
||||
return
|
||||
}
|
||||
|
||||
currentUser, err := auth.GetUserFromReqContext(r.Context())
|
||||
if err != nil {
|
||||
RespondError(w, basemodel.UnauthorizedError(fmt.Errorf(
|
||||
"couldn't deduce current user: %w", err,
|
||||
)), nil)
|
||||
return
|
||||
}
|
||||
|
||||
apiKey, apiErr := ah.getOrCreateCloudIntegrationPAT(r.Context(), currentUser.OrgID, cloudProvider)
|
||||
apiKey, apiErr := ah.getOrCreateCloudIntegrationPAT(r.Context(), claims.OrgID, cloudProvider)
|
||||
if apiErr != nil {
|
||||
RespondError(w, basemodel.WrapApiError(
|
||||
apiErr, "couldn't provision PAT for cloud integration:",
|
||||
@@ -137,7 +135,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
|
||||
|
||||
newPAT := eeTypes.NewGettablePAT(
|
||||
integrationPATName,
|
||||
baseconstants.ViewerGroup,
|
||||
authtypes.RoleViewer.String(),
|
||||
integrationUser.ID,
|
||||
0,
|
||||
)
|
||||
@@ -153,9 +151,11 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
|
||||
func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
||||
ctx context.Context, orgId string, cloudProvider string,
|
||||
) (*types.User, *basemodel.ApiError) {
|
||||
cloudIntegrationUserId := fmt.Sprintf("%s-integration", cloudProvider)
|
||||
cloudIntegrationUser := fmt.Sprintf("%s-integration", cloudProvider)
|
||||
email := fmt.Sprintf("%s@signoz.io", cloudIntegrationUser)
|
||||
|
||||
integrationUserResult, apiErr := ah.AppDao().GetUser(ctx, cloudIntegrationUserId)
|
||||
// TODO(nitya): there should be orgId here
|
||||
integrationUserResult, apiErr := ah.AppDao().GetUserByEmail(ctx, email)
|
||||
if apiErr != nil {
|
||||
return nil, basemodel.WrapApiError(apiErr, "couldn't look for integration user")
|
||||
}
|
||||
@@ -170,20 +170,16 @@ func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
||||
)
|
||||
|
||||
newUser := &types.User{
|
||||
ID: cloudIntegrationUserId,
|
||||
Name: fmt.Sprintf("%s integration", cloudProvider),
|
||||
Email: fmt.Sprintf("%s@signoz.io", cloudIntegrationUserId),
|
||||
ID: uuid.New().String(),
|
||||
Name: cloudIntegrationUser,
|
||||
Email: email,
|
||||
TimeAuditable: types.TimeAuditable{
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
OrgID: orgId,
|
||||
}
|
||||
|
||||
viewerGroup, apiErr := dao.DB().GetGroupByName(ctx, baseconstants.ViewerGroup)
|
||||
if apiErr != nil {
|
||||
return nil, basemodel.WrapApiError(apiErr, "couldn't get viewer group for creating integration user")
|
||||
}
|
||||
newUser.GroupID = viewerGroup.ID
|
||||
newUser.Role = authtypes.RoleViewer.String()
|
||||
|
||||
passwordHash, err := auth.PasswordHash(uuid.NewString())
|
||||
if err != nil {
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/dashboards"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/auth"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@@ -36,18 +35,19 @@ func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := authtypes.ClaimsFromContext(r.Context())
|
||||
if !ok {
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(w, errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated"))
|
||||
return
|
||||
}
|
||||
|
||||
dashboard, err := dashboards.GetDashboard(r.Context(), claims.OrgID, uuid)
|
||||
if err != nil {
|
||||
render.Error(w, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to get dashboard"))
|
||||
return
|
||||
}
|
||||
|
||||
if !auth.IsAdminV2(claims) && (dashboard.CreatedBy != claims.Email) {
|
||||
if err := claims.IsAdmin(); err != nil && (dashboard.CreatedBy != claims.Email) {
|
||||
render.Error(w, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "You are not authorized to lock/unlock this dashboard"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/query-service/auth"
|
||||
baseconstants "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
errorsV2 "github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background()
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := model.CreatePATRequestBody{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
return
|
||||
}
|
||||
user, err := auth.GetUserFromReqContext(r.Context())
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{
|
||||
Typ: model.ErrorUnauthorized,
|
||||
Err: err,
|
||||
}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
pat := eeTypes.NewGettablePAT(
|
||||
req.Name,
|
||||
req.Role,
|
||||
user.ID,
|
||||
claims.UserID,
|
||||
req.ExpiresInDays,
|
||||
)
|
||||
err = validatePATRequest(pat)
|
||||
@@ -47,7 +47,7 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
zap.L().Info("Got Create PAT request", zap.Any("pat", pat))
|
||||
var apierr basemodel.BaseApiError
|
||||
if pat, apierr = ah.AppDao().CreatePAT(ctx, user.OrgID, pat); apierr != nil {
|
||||
if pat, apierr = ah.AppDao().CreatePAT(r.Context(), claims.OrgID, pat); apierr != nil {
|
||||
RespondError(w, apierr, nil)
|
||||
return
|
||||
}
|
||||
@@ -55,34 +55,59 @@ func (ah *APIHandler) createPAT(w http.ResponseWriter, r *http.Request) {
|
||||
ah.Respond(w, &pat)
|
||||
}
|
||||
|
||||
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")
|
||||
func validatePATRequest(req eeTypes.GettablePAT) error {
|
||||
_, err := authtypes.NewRole(req.Role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if req.ExpiresAt < 0 {
|
||||
return fmt.Errorf("valid expiresAt is required")
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
return fmt.Errorf("valid name is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background()
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := types.GettablePAT{}
|
||||
req := eeTypes.GettablePAT{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
RespondError(w, model.BadRequest(err), nil)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := auth.GetUserFromReqContext(r.Context())
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := valuer.NewUUID(idStr)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{
|
||||
Typ: model.ErrorUnauthorized,
|
||||
Err: err,
|
||||
}, nil)
|
||||
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
|
||||
return
|
||||
}
|
||||
|
||||
//get the pat
|
||||
existingPAT, paterr := ah.AppDao().GetPATByID(r.Context(), claims.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(r.Context(), 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
|
||||
}
|
||||
|
||||
@@ -92,12 +117,11 @@ func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UpdatedByUserID = user.ID
|
||||
id := mux.Vars(r)["id"]
|
||||
req.UpdatedByUserID = claims.UserID
|
||||
req.UpdatedAt = time.Now()
|
||||
zap.L().Info("Got Update PAT request", zap.Any("pat", req))
|
||||
var apierr basemodel.BaseApiError
|
||||
if apierr = ah.AppDao().UpdatePAT(ctx, user.OrgID, req, id); apierr != nil {
|
||||
if apierr = ah.AppDao().UpdatePAT(r.Context(), claims.OrgID, req, id); apierr != nil {
|
||||
RespondError(w, apierr, nil)
|
||||
return
|
||||
}
|
||||
@@ -106,38 +130,56 @@ func (ah *APIHandler) updatePAT(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (ah *APIHandler) getPATs(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background()
|
||||
user, err := auth.GetUserFromReqContext(r.Context())
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{
|
||||
Typ: model.ErrorUnauthorized,
|
||||
Err: err,
|
||||
}, nil)
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
zap.L().Info("Get PATs for user", zap.String("user_id", user.ID))
|
||||
pats, apierr := ah.AppDao().ListPATs(ctx, user.OrgID)
|
||||
|
||||
pats, apierr := ah.AppDao().ListPATs(r.Context(), claims.OrgID)
|
||||
if apierr != nil {
|
||||
RespondError(w, apierr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ah.Respond(w, pats)
|
||||
}
|
||||
|
||||
func (ah *APIHandler) revokePAT(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.Background()
|
||||
id := mux.Vars(r)["id"]
|
||||
user, err := auth.GetUserFromReqContext(r.Context())
|
||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{
|
||||
Typ: model.ErrorUnauthorized,
|
||||
Err: err,
|
||||
}, nil)
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
zap.L().Info("Revoke PAT with id", zap.String("id", id))
|
||||
if apierr := ah.AppDao().RevokePAT(ctx, user.OrgID, id, user.ID); apierr != nil {
|
||||
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
|
||||
}
|
||||
|
||||
//get the pat
|
||||
existingPAT, paterr := ah.AppDao().GetPATByID(r.Context(), claims.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(r.Context(), 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(r.Context(), claims.OrgID, id, claims.UserID); apierr != nil {
|
||||
RespondError(w, apierr, nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -88,28 +88,24 @@ func (aH *APIHandler) queryRangeV4(w http.ResponseWriter, r *http.Request) {
|
||||
anomaly.WithCache[*anomaly.WeeklyProvider](aH.opts.Cache),
|
||||
anomaly.WithKeyGenerator[*anomaly.WeeklyProvider](queryBuilder.NewKeyGenerator()),
|
||||
anomaly.WithReader[*anomaly.WeeklyProvider](aH.opts.DataConnector),
|
||||
anomaly.WithFeatureLookup[*anomaly.WeeklyProvider](aH.opts.FeatureFlags),
|
||||
)
|
||||
case anomaly.SeasonalityDaily:
|
||||
provider = anomaly.NewDailyProvider(
|
||||
anomaly.WithCache[*anomaly.DailyProvider](aH.opts.Cache),
|
||||
anomaly.WithKeyGenerator[*anomaly.DailyProvider](queryBuilder.NewKeyGenerator()),
|
||||
anomaly.WithReader[*anomaly.DailyProvider](aH.opts.DataConnector),
|
||||
anomaly.WithFeatureLookup[*anomaly.DailyProvider](aH.opts.FeatureFlags),
|
||||
)
|
||||
case anomaly.SeasonalityHourly:
|
||||
provider = anomaly.NewHourlyProvider(
|
||||
anomaly.WithCache[*anomaly.HourlyProvider](aH.opts.Cache),
|
||||
anomaly.WithKeyGenerator[*anomaly.HourlyProvider](queryBuilder.NewKeyGenerator()),
|
||||
anomaly.WithReader[*anomaly.HourlyProvider](aH.opts.DataConnector),
|
||||
anomaly.WithFeatureLookup[*anomaly.HourlyProvider](aH.opts.FeatureFlags),
|
||||
)
|
||||
default:
|
||||
provider = anomaly.NewDailyProvider(
|
||||
anomaly.WithCache[*anomaly.DailyProvider](aH.opts.Cache),
|
||||
anomaly.WithKeyGenerator[*anomaly.DailyProvider](queryBuilder.NewKeyGenerator()),
|
||||
anomaly.WithReader[*anomaly.DailyProvider](aH.opts.DataConnector),
|
||||
anomaly.WithFeatureLookup[*anomaly.DailyProvider](aH.opts.FeatureFlags),
|
||||
)
|
||||
}
|
||||
anomalies, err := provider.GetAnomalies(r.Context(), &anomaly.GetAnomaliesRequest{Params: queryRangeParams})
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/app/db"
|
||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (ah *APIHandler) searchTraces(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if !ah.CheckFeature(basemodel.SmartTraceDetail) {
|
||||
zap.L().Info("SmartTraceDetail feature is not enabled in this plan")
|
||||
ah.APIHandler.SearchTraces(w, r)
|
||||
return
|
||||
}
|
||||
searchTracesParams, err := baseapp.ParseSearchTracesParams(r)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading params")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := ah.opts.DataConnector.SearchTraces(r.Context(), searchTracesParams, db.SmartTraceAlgorithm)
|
||||
if ah.HandleError(w, err, http.StatusBadRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
ah.WriteJSON(w, r, result)
|
||||
|
||||
}
|
||||
@@ -5,36 +5,33 @@ import (
|
||||
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
basechr "github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
)
|
||||
|
||||
type ClickhouseReader struct {
|
||||
conn clickhouse.Conn
|
||||
appdb *sqlx.DB
|
||||
appdb sqlstore.SQLStore
|
||||
*basechr.ClickHouseReader
|
||||
}
|
||||
|
||||
func NewDataConnector(
|
||||
localDB *sqlx.DB,
|
||||
sqlDB sqlstore.SQLStore,
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
prometheus prometheus.Prometheus,
|
||||
lm interfaces.FeatureLookup,
|
||||
cluster string,
|
||||
useLogsNewSchema bool,
|
||||
useTraceNewSchema bool,
|
||||
fluxIntervalForTraceDetail time.Duration,
|
||||
cache cache.Cache,
|
||||
) *ClickhouseReader {
|
||||
chReader := basechr.NewReader(localDB, telemetryStore, prometheus, lm, cluster, useLogsNewSchema, useTraceNewSchema, fluxIntervalForTraceDetail, cache)
|
||||
chReader := basechr.NewReader(sqlDB, telemetryStore, prometheus, cluster, useLogsNewSchema, useTraceNewSchema, fluxIntervalForTraceDetail, cache)
|
||||
return &ClickhouseReader{
|
||||
conn: telemetryStore.ClickhouseDB(),
|
||||
appdb: localDB,
|
||||
appdb: sqlDB,
|
||||
ClickHouseReader: chReader,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -22,11 +21,9 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/auth"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/web"
|
||||
"github.com/rs/cors"
|
||||
@@ -44,30 +41,21 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/opamp"
|
||||
opAmpModel "github.com/SigNoz/signoz/pkg/query-service/app/opamp/model"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/preferences"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/cache"
|
||||
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/healthcheck"
|
||||
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
||||
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/telemetry"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const AppDbEngine = "sqlite"
|
||||
|
||||
type ServerOptions struct {
|
||||
Config signoz.Config
|
||||
SigNoz *signoz.SigNoz
|
||||
PromConfigPath string
|
||||
SkipTopLvlOpsPath string
|
||||
HTTPHostPort string
|
||||
PrivateHostPort string
|
||||
// alert specific params
|
||||
DisableRules bool
|
||||
RuleRepoURL string
|
||||
Config signoz.Config
|
||||
SigNoz *signoz.SigNoz
|
||||
HTTPHostPort string
|
||||
PrivateHostPort string
|
||||
PreferSpanMetrics bool
|
||||
CacheConfigPath string
|
||||
FluxInterval string
|
||||
@@ -116,10 +104,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := preferences.InitDB(serverOptions.SigNoz.SQLStore.SQLxDB()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := dashboards.InitDB(serverOptions.SigNoz.SQLStore); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -144,10 +128,9 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
}
|
||||
|
||||
reader := db.NewDataConnector(
|
||||
serverOptions.SigNoz.SQLStore.SQLxDB(),
|
||||
serverOptions.SigNoz.SQLStore,
|
||||
serverOptions.SigNoz.TelemetryStore,
|
||||
serverOptions.SigNoz.Prometheus,
|
||||
lm,
|
||||
serverOptions.Cluster,
|
||||
serverOptions.UseLogsNewSchema,
|
||||
serverOptions.UseTraceNewSchema,
|
||||
@@ -155,14 +138,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
serverOptions.SigNoz.Cache,
|
||||
)
|
||||
|
||||
skipConfig := &basemodel.SkipConfig{}
|
||||
if serverOptions.SkipTopLvlOpsPath != "" {
|
||||
// read skip config
|
||||
skipConfig, err = basemodel.ReadSkipConfig(serverOptions.SkipTopLvlOpsPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var c cache.Cache
|
||||
if serverOptions.CacheConfigPath != "" {
|
||||
cacheOpts, err := cache.LoadFromYAMLCacheConfigFile(serverOptions.CacheConfigPath)
|
||||
@@ -173,12 +148,9 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
}
|
||||
|
||||
rm, err := makeRulesManager(
|
||||
serverOptions.RuleRepoURL,
|
||||
serverOptions.SigNoz.SQLStore.SQLxDB(),
|
||||
reader,
|
||||
c,
|
||||
serverOptions.DisableRules,
|
||||
lm,
|
||||
serverOptions.UseLogsNewSchema,
|
||||
serverOptions.UseTraceNewSchema,
|
||||
serverOptions.SigNoz.Alertmanager,
|
||||
@@ -248,7 +220,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
|
||||
apiOpts := api.APIHandlerOptions{
|
||||
DataConnector: reader,
|
||||
SkipConfig: skipConfig,
|
||||
PreferSpanMetrics: serverOptions.PreferSpanMetrics,
|
||||
AppDao: modelDao,
|
||||
RulesManager: rm,
|
||||
@@ -341,24 +312,8 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
|
||||
}
|
||||
|
||||
func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*http.Server, error) {
|
||||
|
||||
r := baseapp.NewRouter()
|
||||
|
||||
// add auth middleware
|
||||
getUserFromRequest := func(ctx context.Context) (*types.GettableUser, error) {
|
||||
user, err := auth.GetUserFromReqContext(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user.User.OrgID == "" {
|
||||
return nil, basemodel.UnauthorizedError(errors.New("orgId is missing in the claims"))
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
am := baseapp.NewAuthMiddleware(getUserFromRequest)
|
||||
am := middleware.NewAuthZ(s.serverOptions.SigNoz.Instrumentation.Logger())
|
||||
|
||||
r.Use(middleware.NewAuth(zap.L(), s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
|
||||
r.Use(eemiddleware.NewPat(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}).Wrap)
|
||||
@@ -374,6 +329,7 @@ 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)
|
||||
@@ -435,14 +391,8 @@ func (s *Server) initListeners() error {
|
||||
}
|
||||
|
||||
// Start listening on http and private http port concurrently
|
||||
func (s *Server) Start() error {
|
||||
|
||||
// initiate rule manager first
|
||||
if !s.serverOptions.DisableRules {
|
||||
s.ruleManager.Start()
|
||||
} else {
|
||||
zap.L().Info("msg: Rules disabled as rules.disable is set to TRUE")
|
||||
}
|
||||
func (s *Server) Start(ctx context.Context) error {
|
||||
s.ruleManager.Start(ctx)
|
||||
|
||||
err := s.initListeners()
|
||||
if err != nil {
|
||||
@@ -523,7 +473,7 @@ func (s *Server) Stop() error {
|
||||
s.opampServer.Stop()
|
||||
|
||||
if s.ruleManager != nil {
|
||||
s.ruleManager.Stop()
|
||||
s.ruleManager.Stop(context.Background())
|
||||
}
|
||||
|
||||
// stop usage manager
|
||||
@@ -533,12 +483,9 @@ func (s *Server) Stop() error {
|
||||
}
|
||||
|
||||
func makeRulesManager(
|
||||
ruleRepoURL string,
|
||||
db *sqlx.DB,
|
||||
ch baseint.Reader,
|
||||
cache cache.Cache,
|
||||
disableRules bool,
|
||||
fm baseint.FeatureLookup,
|
||||
useLogsNewSchema bool,
|
||||
useTraceNewSchema bool,
|
||||
alertmanager alertmanager.Alertmanager,
|
||||
@@ -550,12 +497,9 @@ func makeRulesManager(
|
||||
managerOpts := &baserules.ManagerOptions{
|
||||
TelemetryStore: telemetryStore,
|
||||
Prometheus: prometheus,
|
||||
RepoURL: ruleRepoURL,
|
||||
DBConn: db,
|
||||
Context: context.Background(),
|
||||
Logger: zap.L(),
|
||||
DisableRules: disableRules,
|
||||
FeatureFlags: fm,
|
||||
Reader: ch,
|
||||
Cache: cache,
|
||||
EvalDelay: baseconst.GetEvalDelay(),
|
||||
|
||||
@@ -8,8 +8,8 @@ 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"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
@@ -36,10 +36,9 @@ type ModelDao interface {
|
||||
GetDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, basemodel.BaseApiError)
|
||||
|
||||
CreatePAT(ctx context.Context, orgID string, p types.GettablePAT) (types.GettablePAT, basemodel.BaseApiError)
|
||||
UpdatePAT(ctx context.Context, orgID string, p types.GettablePAT, id string) basemodel.BaseApiError
|
||||
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 string) (*types.GettablePAT, basemodel.BaseApiError)
|
||||
GetUserByPAT(ctx context.Context, orgID string, token string) (*ossTypes.GettableUser, basemodel.BaseApiError)
|
||||
GetPATByID(ctx context.Context, orgID string, id valuer.UUID) (*types.GettablePAT, basemodel.BaseApiError)
|
||||
ListPATs(ctx context.Context, orgID string) ([]types.GettablePAT, basemodel.BaseApiError)
|
||||
RevokePAT(ctx context.Context, orgID string, id string, userID string) basemodel.BaseApiError
|
||||
RevokePAT(ctx context.Context, orgID string, id valuer.UUID, userID string) basemodel.BaseApiError
|
||||
}
|
||||
|
||||
@@ -4,13 +4,11 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
||||
baseauth "github.com/SigNoz/signoz/pkg/query-service/auth"
|
||||
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/utils"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
@@ -37,14 +35,8 @@ func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (
|
||||
return nil, model.InternalErrorStr("failed to generate password hash")
|
||||
}
|
||||
|
||||
group, apiErr := m.GetGroupByName(ctx, baseconst.ViewerGroup)
|
||||
if apiErr != nil {
|
||||
zap.L().Error("GetGroupByName failed", zap.Error(apiErr))
|
||||
return nil, apiErr
|
||||
}
|
||||
|
||||
user := &types.User{
|
||||
ID: uuid.NewString(),
|
||||
ID: uuid.New().String(),
|
||||
Name: "",
|
||||
Email: email,
|
||||
Password: hash,
|
||||
@@ -52,11 +44,11 @@ func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (
|
||||
CreatedAt: time.Now(),
|
||||
},
|
||||
ProfilePictureURL: "", // Currently unused
|
||||
GroupID: group.ID,
|
||||
Role: authtypes.RoleViewer.String(),
|
||||
OrgID: domain.OrgID,
|
||||
}
|
||||
|
||||
user, apiErr = m.CreateUser(ctx, user, false)
|
||||
user, apiErr := m.CreateUser(ctx, user, false)
|
||||
if apiErr != nil {
|
||||
zap.L().Error("CreateUser failed", zap.Error(apiErr))
|
||||
return nil, apiErr
|
||||
@@ -116,7 +108,7 @@ func (m *modelDao) CanUsePassword(ctx context.Context, email string) (bool, base
|
||||
return false, baseapierr
|
||||
}
|
||||
|
||||
if userPayload.Role != baseconst.AdminGroup {
|
||||
if userPayload.Role != authtypes.RoleAdmin.String() {
|
||||
return false, model.BadRequest(fmt.Errorf("auth method not supported"))
|
||||
}
|
||||
|
||||
@@ -162,12 +154,7 @@ func (m *modelDao) PrecheckLogin(ctx context.Context, email, sourceUrl string) (
|
||||
// find domain from email
|
||||
orgDomain, apierr := m.GetDomainByEmail(ctx, email)
|
||||
if apierr != nil {
|
||||
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()))
|
||||
zap.L().Error("failed to get org domain from email", zap.String("email", email), zap.Error(apierr.ToError()))
|
||||
return resp, apierr
|
||||
}
|
||||
|
||||
|
||||
@@ -9,12 +9,14 @@ import (
|
||||
"github.com/SigNoz/signoz/ee/types"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
ossTypes "github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (m *modelDao) CreatePAT(ctx context.Context, orgID string, p types.GettablePAT) (types.GettablePAT, basemodel.BaseApiError) {
|
||||
p.StorablePersonalAccessToken.OrgID = orgID
|
||||
p.StorablePersonalAccessToken.ID = valuer.GenerateUUID()
|
||||
_, err := m.DB().NewInsert().
|
||||
Model(&p.StorablePersonalAccessToken).
|
||||
Exec(ctx)
|
||||
@@ -46,11 +48,11 @@ func (m *modelDao) CreatePAT(ctx context.Context, orgID string, p types.Gettable
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (m *modelDao) UpdatePAT(ctx context.Context, orgID string, p types.GettablePAT, id string) basemodel.BaseApiError {
|
||||
func (m *modelDao) UpdatePAT(ctx context.Context, orgID string, p types.GettablePAT, id valuer.UUID) basemodel.BaseApiError {
|
||||
_, err := m.DB().NewUpdate().
|
||||
Model(&p.StorablePersonalAccessToken).
|
||||
Column("role", "name", "updated_at", "updated_by_user_id").
|
||||
Where("id = ?", id).
|
||||
Where("id = ?", id.StringValue()).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("revoked = false").
|
||||
Exec(ctx)
|
||||
@@ -127,14 +129,14 @@ func (m *modelDao) ListPATs(ctx context.Context, orgID string) ([]types.Gettable
|
||||
return patsWithUsers, nil
|
||||
}
|
||||
|
||||
func (m *modelDao) RevokePAT(ctx context.Context, orgID string, id string, userID string) basemodel.BaseApiError {
|
||||
func (m *modelDao) RevokePAT(ctx context.Context, orgID string, id valuer.UUID, userID string) basemodel.BaseApiError {
|
||||
updatedAt := time.Now().Unix()
|
||||
_, err := m.DB().NewUpdate().
|
||||
Model(&types.StorablePersonalAccessToken{}).
|
||||
Set("revoked = ?", true).
|
||||
Set("updated_by_user_id = ?", userID).
|
||||
Set("updated_at = ?", updatedAt).
|
||||
Where("id = ?", id).
|
||||
Where("id = ?", id.StringValue()).
|
||||
Where("org_id = ?", orgID).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
@@ -169,12 +171,12 @@ func (m *modelDao) GetPAT(ctx context.Context, token string) (*types.GettablePAT
|
||||
return &patWithUser, nil
|
||||
}
|
||||
|
||||
func (m *modelDao) GetPATByID(ctx context.Context, orgID string, id string) (*types.GettablePAT, basemodel.BaseApiError) {
|
||||
func (m *modelDao) GetPATByID(ctx context.Context, orgID string, id valuer.UUID) (*types.GettablePAT, basemodel.BaseApiError) {
|
||||
pats := []types.StorablePersonalAccessToken{}
|
||||
|
||||
if err := m.DB().NewSelect().
|
||||
Model(&pats).
|
||||
Where("id = ?", id).
|
||||
Where("id = ?", id.StringValue()).
|
||||
Where("org_id = ?", orgID).
|
||||
Where("revoked = false").
|
||||
Scan(ctx); err != nil {
|
||||
@@ -194,27 +196,3 @@ func (m *modelDao) GetPATByID(ctx context.Context, orgID string, id string) (*ty
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -9,10 +9,9 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var C *Client
|
||||
|
||||
@@ -239,8 +239,8 @@ func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) {
|
||||
func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (licenseResponse *model.LicenseV3, errResponse *model.ApiError) {
|
||||
defer func() {
|
||||
if errResponse != nil {
|
||||
claims, ok := authtypes.ClaimsFromContext(ctx)
|
||||
if ok {
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED,
|
||||
map[string]interface{}{"err": errResponse.Err.Error()}, claims.Email, true, false)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/config"
|
||||
"github.com/SigNoz/signoz/pkg/config/envprovider"
|
||||
"github.com/SigNoz/signoz/pkg/config/fileprovider"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/auth"
|
||||
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
|
||||
@@ -53,19 +52,27 @@ func main() {
|
||||
|
||||
flag.BoolVar(&useLogsNewSchema, "use-logs-new-schema", false, "use logs_v2 schema for logs")
|
||||
flag.BoolVar(&useTraceNewSchema, "use-trace-new-schema", false, "use new schema for traces")
|
||||
// Deprecated
|
||||
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
|
||||
// Deprecated
|
||||
flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)")
|
||||
// Deprecated
|
||||
flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
|
||||
flag.BoolVar(&preferSpanMetrics, "prefer-span-metrics", false, "(prefer span metrics for service level metrics)")
|
||||
// Deprecated
|
||||
flag.IntVar(&maxIdleConns, "max-idle-conns", 50, "(number of connections to maintain in the pool.)")
|
||||
// Deprecated
|
||||
flag.IntVar(&maxOpenConns, "max-open-conns", 100, "(max connections for use at any time.)")
|
||||
// Deprecated
|
||||
flag.DurationVar(&dialTimeout, "dial-timeout", 5*time.Second, "(the maximum time to establish a connection.)")
|
||||
// Deprecated
|
||||
flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)")
|
||||
flag.StringVar(&cacheConfigPath, "experimental.cache-config", "", "(cache config to use)")
|
||||
flag.StringVar(&fluxInterval, "flux-interval", "5m", "(the interval to exclude data from being cached to avoid incorrect cache for data in motion)")
|
||||
flag.StringVar(&fluxIntervalForTraceDetail, "flux-interval-trace-detail", "2m", "(the interval to exclude data from being cached to avoid incorrect cache for trace data in motion)")
|
||||
flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')")
|
||||
flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)")
|
||||
// Deprecated
|
||||
flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses")
|
||||
flag.Parse()
|
||||
|
||||
@@ -122,12 +129,8 @@ func main() {
|
||||
Config: config,
|
||||
SigNoz: signoz,
|
||||
HTTPHostPort: baseconst.HTTPHostPort,
|
||||
PromConfigPath: promConfigPath,
|
||||
SkipTopLvlOpsPath: skipTopLvlOpsPath,
|
||||
PreferSpanMetrics: preferSpanMetrics,
|
||||
PrivateHostPort: baseconst.PrivateHostPort,
|
||||
DisableRules: disableRules,
|
||||
RuleRepoURL: ruleRepoURL,
|
||||
CacheConfigPath: cacheConfigPath,
|
||||
FluxInterval: fluxInterval,
|
||||
FluxIntervalForTraceDetail: fluxIntervalForTraceDetail,
|
||||
@@ -143,14 +146,10 @@ func main() {
|
||||
zap.L().Fatal("Failed to create server", zap.Error(err))
|
||||
}
|
||||
|
||||
if err := server.Start(); err != nil {
|
||||
if err := server.Start(context.Background()); err != nil {
|
||||
zap.L().Fatal("Could not start server", zap.Error(err))
|
||||
}
|
||||
|
||||
if err := auth.InitAuthCache(context.Background()); err != nil {
|
||||
zap.L().Fatal("Failed to initialize auth cache", zap.Error(err))
|
||||
}
|
||||
|
||||
signoz.Start(context.Background())
|
||||
|
||||
if err := signoz.Wait(context.Background()); err != nil {
|
||||
|
||||
@@ -157,8 +157,6 @@ func NewLicenseV3(data map[string]interface{}) (*LicenseV3, error) {
|
||||
}
|
||||
|
||||
switch planName {
|
||||
case PlanNameTeams:
|
||||
features = append(features, ProPlan...)
|
||||
case PlanNameEnterprise:
|
||||
features = append(features, EnterprisePlan...)
|
||||
case PlanNameBasic:
|
||||
|
||||
@@ -74,21 +74,21 @@ func TestNewLicenseV3(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Parse the entire license properly",
|
||||
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"TEAMS"},"valid_from": 1730899309,"valid_until": -1}`),
|
||||
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"ENTERPRISE"},"valid_from": 1730899309,"valid_until": -1}`),
|
||||
pass: true,
|
||||
expected: &LicenseV3{
|
||||
ID: "does-not-matter",
|
||||
Key: "does-not-matter-key",
|
||||
Data: map[string]interface{}{
|
||||
"plan": map[string]interface{}{
|
||||
"name": "TEAMS",
|
||||
"name": "ENTERPRISE",
|
||||
},
|
||||
"category": "FREE",
|
||||
"status": "ACTIVE",
|
||||
"valid_from": float64(1730899309),
|
||||
"valid_until": float64(-1),
|
||||
},
|
||||
PlanName: PlanNameTeams,
|
||||
PlanName: PlanNameEnterprise,
|
||||
ValidFrom: 1730899309,
|
||||
ValidUntil: -1,
|
||||
Status: "ACTIVE",
|
||||
@@ -98,14 +98,14 @@ func TestNewLicenseV3(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "Fallback to basic plan if license status is invalid",
|
||||
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"INVALID","plan":{"name":"TEAMS"},"valid_from": 1730899309,"valid_until": -1}`),
|
||||
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"INVALID","plan":{"name":"ENTERPRISE"},"valid_from": 1730899309,"valid_until": -1}`),
|
||||
pass: true,
|
||||
expected: &LicenseV3{
|
||||
ID: "does-not-matter",
|
||||
Key: "does-not-matter-key",
|
||||
Data: map[string]interface{}{
|
||||
"plan": map[string]interface{}{
|
||||
"name": "TEAMS",
|
||||
"name": "ENTERPRISE",
|
||||
},
|
||||
"category": "FREE",
|
||||
"status": "INVALID",
|
||||
@@ -122,21 +122,21 @@ func TestNewLicenseV3(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "fallback states for validFrom and validUntil",
|
||||
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"TEAMS"},"valid_from":1234.456,"valid_until":5678.567}`),
|
||||
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"ENTERPRISE"},"valid_from":1234.456,"valid_until":5678.567}`),
|
||||
pass: true,
|
||||
expected: &LicenseV3{
|
||||
ID: "does-not-matter",
|
||||
Key: "does-not-matter-key",
|
||||
Data: map[string]interface{}{
|
||||
"plan": map[string]interface{}{
|
||||
"name": "TEAMS",
|
||||
"name": "ENTERPRISE",
|
||||
},
|
||||
"valid_from": 1234.456,
|
||||
"valid_until": 5678.567,
|
||||
"category": "FREE",
|
||||
"status": "ACTIVE",
|
||||
},
|
||||
PlanName: PlanNameTeams,
|
||||
PlanName: PlanNameEnterprise,
|
||||
ValidFrom: 1234,
|
||||
ValidUntil: 5678,
|
||||
Status: "ACTIVE",
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
)
|
||||
|
||||
const SSO = "SSO"
|
||||
const Basic = "BASIC_PLAN"
|
||||
const Pro = "PRO_PLAN"
|
||||
const Enterprise = "ENTERPRISE_PLAN"
|
||||
|
||||
var (
|
||||
PlanNameEnterprise = "ENTERPRISE"
|
||||
PlanNameTeams = "TEAMS"
|
||||
PlanNameBasic = "BASIC"
|
||||
)
|
||||
|
||||
var (
|
||||
MapOldPlanKeyToNewPlanName map[string]string = map[string]string{PlanNameBasic: Basic, PlanNameTeams: Pro, PlanNameEnterprise: Enterprise}
|
||||
MapOldPlanKeyToNewPlanName map[string]string = map[string]string{PlanNameBasic: Basic, PlanNameEnterprise: Enterprise}
|
||||
)
|
||||
|
||||
var (
|
||||
LicenseStatusInvalid = "INVALID"
|
||||
)
|
||||
|
||||
const DisableUpsell = "DISABLE_UPSELL"
|
||||
const Onboarding = "ONBOARDING"
|
||||
const ChatSupport = "CHAT_SUPPORT"
|
||||
const Gateway = "GATEWAY"
|
||||
@@ -38,90 +34,6 @@ var BasicPlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.OSS,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: DisableUpsell,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.SmartTraceDetail,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.CustomMetricsFunction,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.QueryBuilderPanels,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.QueryBuilderAlerts,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelSlack,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelWebhook,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelPagerduty,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelOpsgenie,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelEmail,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelMsTeams,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.UseSpanMetrics,
|
||||
Active: false,
|
||||
@@ -150,142 +62,6 @@ var BasicPlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.HostsInfraMonitoring,
|
||||
Active: constants.EnableHostsInfraMonitoring(),
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.TraceFunnels,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
}
|
||||
|
||||
var ProPlan = basemodel.FeatureSet{
|
||||
basemodel.Feature{
|
||||
Name: SSO,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.OSS,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.SmartTraceDetail,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.CustomMetricsFunction,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.QueryBuilderPanels,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.QueryBuilderAlerts,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelSlack,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelWebhook,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelPagerduty,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelOpsgenie,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelEmail,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelMsTeams,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.UseSpanMetrics,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: Gateway,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: PremiumSupport,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AnomalyDetection,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.HostsInfraMonitoring,
|
||||
Active: constants.EnableHostsInfraMonitoring(),
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.TraceFunnels,
|
||||
Active: false,
|
||||
@@ -303,83 +79,6 @@ var EnterprisePlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.OSS,
|
||||
Active: false,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.SmartTraceDetail,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.CustomMetricsFunction,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.QueryBuilderPanels,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.QueryBuilderAlerts,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelSlack,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelWebhook,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelPagerduty,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelOpsgenie,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelEmail,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.AlertChannelMsTeams,
|
||||
Active: true,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.UseSpanMetrics,
|
||||
Active: false,
|
||||
@@ -422,13 +121,6 @@ var EnterprisePlan = basemodel.FeatureSet{
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.HostsInfraMonitoring,
|
||||
Active: constants.EnableHostsInfraMonitoring(),
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
},
|
||||
basemodel.Feature{
|
||||
Name: basemodel.TraceFunnels,
|
||||
Active: false,
|
||||
|
||||
@@ -15,6 +15,7 @@ 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"
|
||||
@@ -52,8 +53,7 @@ type AnomalyRule struct {
|
||||
|
||||
func NewAnomalyRule(
|
||||
id string,
|
||||
p *baserules.PostableRule,
|
||||
featureFlags interfaces.FeatureLookup,
|
||||
p *ruletypes.PostableRule,
|
||||
reader interfaces.Reader,
|
||||
cache cache.Cache,
|
||||
opts ...baserules.RuleOption,
|
||||
@@ -61,7 +61,7 @@ func NewAnomalyRule(
|
||||
|
||||
zap.L().Info("creating new AnomalyRule", zap.String("id", id), zap.Any("opts", opts))
|
||||
|
||||
if p.RuleCondition.CompareOp == baserules.ValueIsBelow {
|
||||
if p.RuleCondition.CompareOp == ruletypes.ValueIsBelow {
|
||||
target := -1 * *p.RuleCondition.Target
|
||||
p.RuleCondition.Target = &target
|
||||
}
|
||||
@@ -89,10 +89,9 @@ func NewAnomalyRule(
|
||||
zap.L().Info("using seasonality", zap.String("seasonality", t.seasonality.String()))
|
||||
|
||||
querierOptsV2 := querierV2.QuerierOptions{
|
||||
Reader: reader,
|
||||
Cache: cache,
|
||||
KeyGenerator: queryBuilder.NewKeyGenerator(),
|
||||
FeatureLookup: featureFlags,
|
||||
Reader: reader,
|
||||
Cache: cache,
|
||||
KeyGenerator: queryBuilder.NewKeyGenerator(),
|
||||
}
|
||||
|
||||
t.querierV2 = querierV2.NewQuerier(querierOptsV2)
|
||||
@@ -102,27 +101,24 @@ func NewAnomalyRule(
|
||||
anomaly.WithCache[*anomaly.HourlyProvider](cache),
|
||||
anomaly.WithKeyGenerator[*anomaly.HourlyProvider](queryBuilder.NewKeyGenerator()),
|
||||
anomaly.WithReader[*anomaly.HourlyProvider](reader),
|
||||
anomaly.WithFeatureLookup[*anomaly.HourlyProvider](featureFlags),
|
||||
)
|
||||
} else if t.seasonality == anomaly.SeasonalityDaily {
|
||||
t.provider = anomaly.NewDailyProvider(
|
||||
anomaly.WithCache[*anomaly.DailyProvider](cache),
|
||||
anomaly.WithKeyGenerator[*anomaly.DailyProvider](queryBuilder.NewKeyGenerator()),
|
||||
anomaly.WithReader[*anomaly.DailyProvider](reader),
|
||||
anomaly.WithFeatureLookup[*anomaly.DailyProvider](featureFlags),
|
||||
)
|
||||
} else if t.seasonality == anomaly.SeasonalityWeekly {
|
||||
t.provider = anomaly.NewWeeklyProvider(
|
||||
anomaly.WithCache[*anomaly.WeeklyProvider](cache),
|
||||
anomaly.WithKeyGenerator[*anomaly.WeeklyProvider](queryBuilder.NewKeyGenerator()),
|
||||
anomaly.WithReader[*anomaly.WeeklyProvider](reader),
|
||||
anomaly.WithFeatureLookup[*anomaly.WeeklyProvider](featureFlags),
|
||||
)
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) Type() baserules.RuleType {
|
||||
func (r *AnomalyRule) Type() ruletypes.RuleType {
|
||||
return RuleTypeAnomaly
|
||||
}
|
||||
|
||||
@@ -162,7 +158,7 @@ func (r *AnomalyRule) GetSelectedQuery() string {
|
||||
return r.Condition().GetSelectedQueryName()
|
||||
}
|
||||
|
||||
func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, ts time.Time) (baserules.Vector, error) {
|
||||
func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, ts time.Time) (ruletypes.Vector, error) {
|
||||
|
||||
params, err := r.prepareQueryRange(ts)
|
||||
if err != nil {
|
||||
@@ -189,7 +185,7 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, ts time.Time) (baser
|
||||
}
|
||||
}
|
||||
|
||||
var resultVector baserules.Vector
|
||||
var resultVector ruletypes.Vector
|
||||
|
||||
scoresJSON, _ := json.Marshal(queryResult.AnomalyScores)
|
||||
zap.L().Info("anomaly scores", zap.String("scores", string(scoresJSON)))
|
||||
@@ -218,7 +214,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]*baserules.Alert, len(res))
|
||||
var alerts = make(map[uint64]*ruletypes.Alert, len(res))
|
||||
|
||||
for _, smpl := range res {
|
||||
l := make(map[string]string, len(smpl.Metric))
|
||||
@@ -230,7 +226,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 := baserules.AlertTemplateData(l, value, threshold)
|
||||
tmplData := ruletypes.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}}"
|
||||
@@ -238,7 +234,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 := baserules.NewTemplateExpander(
|
||||
tmpl := ruletypes.NewTemplateExpander(
|
||||
ctx,
|
||||
defs+text,
|
||||
"__alert_"+r.Name(),
|
||||
@@ -283,7 +279,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
alerts[h] = &baserules.Alert{
|
||||
alerts[h] = &ruletypes.Alert{
|
||||
Labels: lbs,
|
||||
QueryResultLables: resultLabels,
|
||||
Annotations: annotations,
|
||||
@@ -324,7 +320,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) > baserules.ResolvedRetention) {
|
||||
if a.State == model.StatePending || (!a.ResolvedAt.IsZero() && ts.Sub(a.ResolvedAt) > ruletypes.ResolvedRetention) {
|
||||
delete(r.Active, fp)
|
||||
}
|
||||
if a.State != model.StateInactive {
|
||||
@@ -380,10 +376,10 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
||||
|
||||
func (r *AnomalyRule) String() string {
|
||||
|
||||
ar := baserules.PostableRule{
|
||||
ar := ruletypes.PostableRule{
|
||||
AlertName: r.Name(),
|
||||
RuleCondition: r.Condition(),
|
||||
EvalWindow: baserules.Duration(r.EvalWindow()),
|
||||
EvalWindow: ruletypes.Duration(r.EvalWindow()),
|
||||
Labels: r.Labels().Map(),
|
||||
Annotations: r.Annotations().Map(),
|
||||
PreferredChannels: r.PreferredChannels(),
|
||||
|
||||
@@ -8,6 +8,7 @@ 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"
|
||||
)
|
||||
@@ -18,12 +19,11 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
var task baserules.Task
|
||||
|
||||
ruleId := baserules.RuleIdFromTaskName(opts.TaskName)
|
||||
if opts.Rule.RuleType == baserules.RuleTypeThreshold {
|
||||
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
|
||||
// create a threshold rule
|
||||
tr, err := baserules.NewThresholdRule(
|
||||
ruleId,
|
||||
opts.Rule,
|
||||
opts.FF,
|
||||
opts.Reader,
|
||||
opts.UseLogsNewSchema,
|
||||
opts.UseTraceNewSchema,
|
||||
@@ -38,9 +38,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.RuleDB)
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
|
||||
} else if opts.Rule.RuleType == baserules.RuleTypeProm {
|
||||
} else if opts.Rule.RuleType == ruletypes.RuleTypeProm {
|
||||
|
||||
// create promql rule
|
||||
pr, err := baserules.NewPromRule(
|
||||
@@ -59,14 +59,13 @@ 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.RuleDB)
|
||||
task = newTask(baserules.TaskTypeProm, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
|
||||
} else if opts.Rule.RuleType == baserules.RuleTypeAnomaly {
|
||||
} else if opts.Rule.RuleType == ruletypes.RuleTypeAnomaly {
|
||||
// create anomaly rule
|
||||
ar, err := NewAnomalyRule(
|
||||
ruleId,
|
||||
opts.Rule,
|
||||
opts.FF,
|
||||
opts.Reader,
|
||||
opts.Cache,
|
||||
baserules.WithEvalDelay(opts.ManagerOpts.EvalDelay),
|
||||
@@ -79,10 +78,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.RuleDB)
|
||||
task = newTask(baserules.TaskTypeCh, opts.TaskName, time.Duration(opts.Rule.Frequency), rules, opts.ManagerOpts, opts.NotifyFunc, opts.MaintenanceStore, opts.OrgID)
|
||||
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, baserules.RuleTypeProm, baserules.RuleTypeThreshold)
|
||||
return nil, fmt.Errorf("unsupported rule type %s. Supported types: %s, %s", opts.Rule.RuleType, ruletypes.RuleTypeProm, ruletypes.RuleTypeThreshold)
|
||||
}
|
||||
|
||||
return task, nil
|
||||
@@ -107,12 +106,12 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
}
|
||||
|
||||
// append name to indicate this is test alert
|
||||
parsedRule.AlertName = fmt.Sprintf("%s%s", alertname, baserules.TestAlertPostFix)
|
||||
parsedRule.AlertName = fmt.Sprintf("%s%s", alertname, ruletypes.TestAlertPostFix)
|
||||
|
||||
var rule baserules.Rule
|
||||
var err error
|
||||
|
||||
if parsedRule.RuleType == baserules.RuleTypeThreshold {
|
||||
if parsedRule.RuleType == ruletypes.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)
|
||||
@@ -123,7 +122,6 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
rule, err = baserules.NewThresholdRule(
|
||||
alertname,
|
||||
parsedRule,
|
||||
opts.FF,
|
||||
opts.Reader,
|
||||
opts.UseLogsNewSchema,
|
||||
opts.UseTraceNewSchema,
|
||||
@@ -137,7 +135,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
return 0, basemodel.BadRequest(err)
|
||||
}
|
||||
|
||||
} else if parsedRule.RuleType == baserules.RuleTypeProm {
|
||||
} else if parsedRule.RuleType == ruletypes.RuleTypeProm {
|
||||
|
||||
// create promql rule
|
||||
rule, err = baserules.NewPromRule(
|
||||
@@ -155,12 +153,11 @@ 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 == baserules.RuleTypeAnomaly {
|
||||
} else if parsedRule.RuleType == ruletypes.RuleTypeAnomaly {
|
||||
// create anomaly rule
|
||||
rule, err = NewAnomalyRule(
|
||||
alertname,
|
||||
parsedRule,
|
||||
opts.FF,
|
||||
opts.Reader,
|
||||
opts.Cache,
|
||||
baserules.WithSendAlways(),
|
||||
@@ -194,9 +191,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, ruleDB baserules.RuleDB) baserules.Task {
|
||||
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 {
|
||||
if taskType == baserules.TaskTypeCh {
|
||||
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify, ruleDB)
|
||||
return baserules.NewRuleTask(name, "", frequency, rules, opts, notify, maintenanceStore, orgID)
|
||||
}
|
||||
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify, ruleDB)
|
||||
return baserules.NewPromRuleTask(name, "", frequency, rules, opts, notify, maintenanceStore, orgID)
|
||||
}
|
||||
|
||||
@@ -138,15 +138,6 @@ func (lm *Manager) UploadUsage() {
|
||||
|
||||
zap.L().Info("uploading usage data")
|
||||
|
||||
orgName := ""
|
||||
orgNames, orgError := lm.modelDao.GetOrgs(ctx)
|
||||
if orgError != nil {
|
||||
zap.L().Error("failed to get org data: %v", zap.Error(orgError))
|
||||
}
|
||||
if len(orgNames) == 1 {
|
||||
orgName = orgNames[0].Name
|
||||
}
|
||||
|
||||
usagesPayload := []model.Usage{}
|
||||
for _, usage := range usages {
|
||||
usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data))
|
||||
@@ -166,7 +157,7 @@ func (lm *Manager) UploadUsage() {
|
||||
usageData.ExporterID = usage.ExporterID
|
||||
usageData.Type = usage.Type
|
||||
usageData.Tenant = "default"
|
||||
usageData.OrgName = orgName
|
||||
usageData.OrgName = "default"
|
||||
usageData.TenantId = lm.tenantID
|
||||
usagesPayload = append(usagesPayload, usageData)
|
||||
}
|
||||
|
||||
@@ -4,14 +4,33 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type dialect struct {
|
||||
}
|
||||
var (
|
||||
Identity = "id"
|
||||
Integer = "bigint"
|
||||
Text = "text"
|
||||
)
|
||||
|
||||
func (dialect *dialect) MigrateIntToTimestamp(ctx context.Context, bun bun.IDB, table string, column string) error {
|
||||
var (
|
||||
Org = "org"
|
||||
User = "user"
|
||||
CloudIntegration = "cloud_integration"
|
||||
)
|
||||
|
||||
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`
|
||||
)
|
||||
|
||||
type dialect struct{}
|
||||
|
||||
func (dialect *dialect) IntToTimestamp(ctx context.Context, bun bun.IDB, table string, column string) error {
|
||||
columnType, err := dialect.GetColumnType(ctx, bun, table, column)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -58,7 +77,15 @@ func (dialect *dialect) MigrateIntToTimestamp(ctx context.Context, bun bun.IDB,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dialect *dialect) MigrateIntToBoolean(ctx context.Context, bun bun.IDB, table string, column string) error {
|
||||
func (dialect *dialect) IntToBoolean(ctx context.Context, bun bun.IDB, table string, column string) error {
|
||||
columnExists, err := dialect.ColumnExists(ctx, bun, table, column)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !columnExists {
|
||||
return nil
|
||||
}
|
||||
|
||||
columnType, err := dialect.GetColumnType(ctx, bun, table, column)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -131,6 +158,26 @@ func (dialect *dialect) ColumnExists(ctx context.Context, bun bun.IDB, table str
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (dialect *dialect) AddColumn(ctx context.Context, bun bun.IDB, table string, column string, columnExpr string) error {
|
||||
exists, err := dialect.ColumnExists(ctx, bun, table, column)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
_, err = bun.
|
||||
NewAddColumn().
|
||||
Table(table).
|
||||
ColumnExpr(column + " " + columnExpr).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dialect *dialect) RenameColumn(ctx context.Context, bun bun.IDB, table string, oldColumnName string, newColumnName string) (bool, error) {
|
||||
oldColumnExists, err := dialect.ColumnExists(ctx, bun, table, oldColumnName)
|
||||
if err != nil {
|
||||
@@ -142,10 +189,14 @@ func (dialect *dialect) RenameColumn(ctx context.Context, bun bun.IDB, table str
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !oldColumnExists && newColumnExists {
|
||||
if newColumnExists {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if !oldColumnExists {
|
||||
return false, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "old column: %s doesn't exist", oldColumnName)
|
||||
}
|
||||
|
||||
_, err = bun.
|
||||
ExecContext(ctx, "ALTER TABLE "+table+" RENAME COLUMN "+oldColumnName+" TO "+newColumnName)
|
||||
if err != nil {
|
||||
@@ -154,6 +205,26 @@ func (dialect *dialect) RenameColumn(ctx context.Context, bun bun.IDB, table str
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (dialect *dialect) DropColumn(ctx context.Context, bun bun.IDB, table string, column string) error {
|
||||
exists, err := dialect.ColumnExists(ctx, bun, table, column)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
_, err = bun.
|
||||
NewDropColumn().
|
||||
Table(table).
|
||||
Column(column).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dialect *dialect) TableExists(ctx context.Context, bun bun.IDB, table interface{}) (bool, error) {
|
||||
|
||||
count := 0
|
||||
@@ -175,7 +246,10 @@ func (dialect *dialect) TableExists(ctx context.Context, bun bun.IDB, table inte
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (dialect *dialect) RenameTableAndModifyModel(ctx context.Context, bun bun.IDB, oldModel interface{}, newModel interface{}, cb func(context.Context) error) error {
|
||||
func (dialect *dialect) RenameTableAndModifyModel(ctx context.Context, bun bun.IDB, oldModel interface{}, newModel interface{}, references []string, cb func(context.Context) error) error {
|
||||
if len(references) == 0 {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "cannot run migration without reference")
|
||||
}
|
||||
exists, err := dialect.TableExists(ctx, bun, newModel)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -184,12 +258,27 @@ func (dialect *dialect) RenameTableAndModifyModel(ctx context.Context, bun bun.I
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = bun.
|
||||
var fkReferences []string
|
||||
for _, reference := range references {
|
||||
if reference == Org && !slices.Contains(fkReferences, OrgReference) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
createTable := bun.
|
||||
NewCreateTable().
|
||||
IfNotExists().
|
||||
Model(newModel).
|
||||
Exec(ctx)
|
||||
Model(newModel)
|
||||
|
||||
for _, fk := range fkReferences {
|
||||
createTable = createTable.ForeignKey(fk)
|
||||
}
|
||||
|
||||
_, err = createTable.Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -218,3 +307,138 @@ func (dialect *dialect) AddNotNullDefaultToColumn(ctx context.Context, bun bun.I
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dialect *dialect) UpdatePrimaryKey(ctx context.Context, bun bun.IDB, oldModel interface{}, newModel interface{}, reference string, cb func(context.Context) error) error {
|
||||
if reference == "" {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "cannot run migration without reference")
|
||||
}
|
||||
oldTableName := bun.Dialect().Tables().Get(reflect.TypeOf(oldModel)).Name
|
||||
newTableName := bun.Dialect().Tables().Get(reflect.TypeOf(newModel)).Name
|
||||
|
||||
columnType, err := dialect.GetColumnType(ctx, bun, oldTableName, Identity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if columnType == Text {
|
||||
return nil
|
||||
}
|
||||
|
||||
fkReference := ""
|
||||
if reference == Org {
|
||||
fkReference = OrgReference
|
||||
} else if reference == User {
|
||||
fkReference = UserReference
|
||||
}
|
||||
|
||||
_, err = bun.
|
||||
NewCreateTable().
|
||||
IfNotExists().
|
||||
Model(newModel).
|
||||
ForeignKey(fkReference).
|
||||
Exec(ctx)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cb(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = bun.
|
||||
NewDropTable().
|
||||
IfExists().
|
||||
Model(oldModel).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = bun.
|
||||
ExecContext(ctx, fmt.Sprintf("ALTER TABLE %s RENAME TO %s", newTableName, oldTableName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dialect *dialect) AddPrimaryKey(ctx context.Context, bun bun.IDB, oldModel interface{}, newModel interface{}, reference string, cb func(context.Context) error) error {
|
||||
if reference == "" {
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "cannot run migration without reference")
|
||||
}
|
||||
oldTableName := bun.Dialect().Tables().Get(reflect.TypeOf(oldModel)).Name
|
||||
newTableName := bun.Dialect().Tables().Get(reflect.TypeOf(newModel)).Name
|
||||
|
||||
identityExists, err := dialect.ColumnExists(ctx, bun, oldTableName, Identity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if identityExists {
|
||||
return nil
|
||||
}
|
||||
|
||||
fkReference := ""
|
||||
if reference == Org {
|
||||
fkReference = OrgReference
|
||||
} else if reference == User {
|
||||
fkReference = UserReference
|
||||
}
|
||||
|
||||
_, err = bun.
|
||||
NewCreateTable().
|
||||
IfNotExists().
|
||||
Model(newModel).
|
||||
ForeignKey(fkReference).
|
||||
Exec(ctx)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cb(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = bun.
|
||||
NewDropTable().
|
||||
IfExists().
|
||||
Model(oldModel).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = bun.
|
||||
ExecContext(ctx, fmt.Sprintf("ALTER TABLE %s RENAME TO %s", newTableName, oldTableName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dialect *dialect) DropColumnWithForeignKeyConstraint(ctx context.Context, bunIDB bun.IDB, model interface{}, column string) error {
|
||||
existingTable := bunIDB.Dialect().Tables().Get(reflect.TypeOf(model))
|
||||
columnExists, err := dialect.ColumnExists(ctx, bunIDB, existingTable.Name, column)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !columnExists {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = bunIDB.
|
||||
NewDropColumn().
|
||||
Model(model).
|
||||
Column(column).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/jmoiron/sqlx"
|
||||
@@ -87,3 +89,20 @@ func (provider *provider) BunDBCtx(ctx context.Context) bun.IDB {
|
||||
func (provider *provider) RunInTxCtx(ctx context.Context, opts *sql.TxOptions, cb func(ctx context.Context) error) error {
|
||||
return provider.bundb.RunInTxCtx(ctx, opts, cb)
|
||||
}
|
||||
|
||||
func (provider *provider) WrapNotFoundErrf(err error, code errors.Code, format string, args ...any) error {
|
||||
if err == sql.ErrNoRows {
|
||||
return errors.Wrapf(err, errors.TypeNotFound, code, format, args...)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (provider *provider) WrapAlreadyExistsErrf(err error, code errors.Code, format string, args ...any) error {
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
|
||||
return errors.Wrapf(err, errors.TypeAlreadyExists, code, format, args...)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
@@ -28,11 +29,10 @@ func NewGettablePAT(name, role, userID string, expiresAt int64) GettablePAT {
|
||||
}
|
||||
|
||||
type StorablePersonalAccessToken struct {
|
||||
bun.BaseModel `bun:"table:personal_access_tokens"`
|
||||
|
||||
bun.BaseModel `bun:"table:personal_access_token"`
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
OrgID string `json:"orgId" bun:"org_id,type:text,notnull"`
|
||||
ID int `json:"id" bun:"id,pk,autoincrement"`
|
||||
Role string `json:"role" bun:"role,type:text,notnull,default:'ADMIN'"`
|
||||
UserID string `json:"userId" bun:"user_id,type:text,notnull"`
|
||||
Token string `json:"token" bun:"token,type:text,notnull,unique"`
|
||||
@@ -69,5 +69,8 @@ func NewStorablePersonalAccessToken(name, role, userID string, expiresAt int64)
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
},
|
||||
Identifiable: types.Identifiable{
|
||||
ID: valuer.GenerateUUID(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
1
frontend/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
|
||||
# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
.qodo
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"@radix-ui/react-tooltip": "1.0.7",
|
||||
"@sentry/react": "8.41.0",
|
||||
"@sentry/webpack-plugin": "2.22.6",
|
||||
"@signozhq/button": "0.0.0",
|
||||
"@signozhq/design-tokens": "1.1.4",
|
||||
"@tanstack/react-table": "8.20.6",
|
||||
"@tanstack/react-virtual": "3.11.2",
|
||||
@@ -55,7 +56,7 @@
|
||||
"ansi-to-html": "0.7.2",
|
||||
"antd": "5.11.0",
|
||||
"antd-table-saveas-excel": "2.2.1",
|
||||
"axios": "1.7.7",
|
||||
"axios": "1.8.2",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^29.6.4",
|
||||
"babel-loader": "9.1.3",
|
||||
@@ -198,7 +199,7 @@
|
||||
"autoprefixer": "10.4.19",
|
||||
"babel-plugin-styled-components": "^1.12.0",
|
||||
"compression-webpack-plugin": "9.0.0",
|
||||
"copy-webpack-plugin": "^8.1.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"critters-webpack-plugin": "^3.0.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
@@ -255,6 +256,7 @@
|
||||
"body-parser": "1.20.3",
|
||||
"http-proxy-middleware": "3.0.3",
|
||||
"cross-spawn": "7.0.5",
|
||||
"cookie": "^0.7.1"
|
||||
"cookie": "^0.7.1",
|
||||
"serialize-javascript": "6.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
1
frontend/public/Logos/amazon-msk.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80"><defs><linearGradient id="a" x1="0%" x2="100%" y1="100%" y2="0%"><stop offset="0%" stop-color="#4D27A8"/><stop offset="100%" stop-color="#A166FF"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><path fill="url(#a)" d="M0 0h80v80H0z"/><path fill="#FFF" d="M36.008 36.977v-8.99h1.996v3.995l2.694-3.996h2.21l-3.123 4.204 3.38 4.787h-2.368l-2.793-4.345v4.345zM66 49.117l-.669.579c-1.347 1.18-3.16 1.77-4.973 1.77-1.815 0-3.63-.59-4.98-1.773l-1.333-1.155c-1.956-1.714-5.38-1.714-7.332-.004l-1.341 1.162c-2.693 2.36-7.255 2.362-9.953-.003l-1.333-1.155c-1.956-1.714-5.38-1.714-7.332-.004l-1.341 1.162c-2.694 2.36-7.255 2.362-9.953-.003l-.665-.576 1.307-1.511.668.58c1.957 1.714 5.381 1.714 7.332.003l1.342-1.162c2.693-2.36 7.255-2.36 9.952.004l1.334 1.154c1.957 1.715 5.38 1.715 7.332.004l1.341-1.162c2.693-2.36 7.255-2.36 9.953.004l1.333 1.154c1.956 1.715 5.38 1.715 7.332.004l.673-.583zM63.898 61.14l1.307 1.51-.669.58c-1.346 1.18-3.159 1.769-4.972 1.769-1.815 0-3.63-.59-4.98-1.773L53.25 62.07c-1.956-1.712-5.38-1.712-7.332-.004l-1.341 1.163c-2.692 2.36-7.254 2.361-9.953-.004l-1.333-1.155c-1.955-1.712-5.38-1.712-7.332-.004l-1.342 1.163c-2.692 2.36-7.254 2.361-9.952-.004L14 62.65l1.306-1.51.669.58c1.957 1.712 5.381 1.713 7.332.002l1.341-1.161c2.695-2.36 7.255-2.361 9.953.002l1.333 1.156c1.957 1.713 5.381 1.714 7.332.003l1.342-1.161c2.694-2.36 7.254-2.361 9.952.002l1.334 1.156c1.957 1.713 5.38 1.714 7.332.003zm0-6.333 1.307 1.509-.669.58c-2.693 2.36-7.254 2.362-9.953-.004l-1.333-1.154c-1.956-1.715-5.38-1.714-7.332-.004l-1.341 1.162c-1.347 1.18-3.16 1.77-4.972 1.77-1.815 0-3.63-.59-4.981-1.774l-1.333-1.154c-1.955-1.715-5.38-1.714-7.332-.004l-1.342 1.162c-2.691 2.36-7.253 2.36-9.952-.004L14 56.316l1.306-1.51.669.58c1.957 1.714 5.381 1.713 7.332.003l1.341-1.162c2.695-2.36 7.257-2.361 9.953.003l1.333 1.155c1.957 1.715 5.381 1.714 7.332.004l1.342-1.162c2.694-2.36 7.256-2.361 9.952.003l1.334 1.155c1.957 1.715 5.38 1.714 7.332.004zM24.033 37.976a1.999 1.999 0 0 1 0 3.996 1.999 1.999 0 0 1 0-3.996m14.969-20.978a1.999 1.999 0 0 1 0 3.996 1.999 1.999 0 0 1 0-3.996m16.965 22.976a2 2 0 0 1-1.995 1.998 1.999 1.999 0 0 1 0-3.996c1.1 0 1.995.896 1.995 1.998M39.002 22.992c.665 0 1.283-.18 1.835-.469l10.168 14.804a3.95 3.95 0 0 0-.883 1.648h-22.24A3.95 3.95 0 0 0 27 37.327l10.167-14.804c.552.29 1.17.469 1.835.469M24.032 43.97c1.855 0 3.405-1.279 3.85-2.997h22.24c.445 1.718 1.995 2.997 3.85 2.997a4 4 0 0 0 3.991-3.996 4 4 0 0 0-3.991-3.996c-.46 0-.896.094-1.309.238L42.337 21.18a3.96 3.96 0 0 0 .657-2.185A4 4 0 0 0 39.002 15a4 4 0 0 0-3.992 3.996c0 .807.244 1.556.657 2.185L25.34 36.216a4 4 0 0 0-1.308-.238 4 4 0 0 0-3.992 3.996 4 4 0 0 0 3.992 3.996"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
18
frontend/public/Logos/api-gateway.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="80px" height="80px" viewBox="0 0 80 80" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Icon-Architecture/64/Arch_ Amazon-API-Gateway_64</title>
|
||||
<defs>
|
||||
<linearGradient x1="0%" y1="100%" x2="100%" y2="0%" id="linearGradient-1">
|
||||
<stop stop-color="#B0084D" offset="0%"></stop>
|
||||
<stop stop-color="#FF4F8B" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Icon-Architecture/64/Arch_-Amazon-API-Gateway_64" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Rectangle" fill="url(#linearGradient-1)">
|
||||
<rect x="0" y="0" width="80" height="80"></rect>
|
||||
</g>
|
||||
<g id="Icon-Service/64/Amazon-API-Gateway_64" transform="translate(8.000000, 8.000000)" fill="#FFFFFF">
|
||||
<path d="M26.065,47.6323642 L29,47.6323642 L29,45.6152989 L26.065,45.6152989 L26.065,47.6323642 Z M31,47.6323642 L34,47.6323642 L34,45.6152989 L31,45.6152989 L31,47.6323642 Z M19,6.62946112 L6,13.082053 L6,53.1934132 L19,57.589607 L19,6.62946112 Z M21,19.3934502 L21,45.6152989 L24,45.6152989 L24,47.6323642 L21,47.6323642 L21,58.9914674 C21,59.3162149 20.845,59.6207917 20.584,59.8103959 C20.412,59.9354539 20.207,60 20,60 C19.894,60 19.786,59.9828549 19.682,59.9475563 L4.682,54.8756456 C4.274,54.7374767 4,54.3522172 4,53.9195567 L4,12.4537371 C4,12.0704947 4.217,11.7185168 4.559,11.5490833 L19.559,4.10409539 C19.868,3.94978989 20.235,3.96794348 20.528,4.15250495 C20.821,4.33605789 21,4.65979687 21,5.00874917 L21,17.376385 L24,17.376385 L24,19.3934502 L21,19.3934502 Z M36,47.6323642 L39,47.6323642 L39,45.6152989 L36,45.6152989 L36,47.6323642 Z M36.065,19.3934502 L39,19.3934502 L39,17.376385 L36.065,17.376385 L36.065,19.3934502 Z M31.065,19.3934502 L34,19.3934502 L34,17.376385 L31.065,17.376385 L31.065,19.3934502 Z M26.065,19.3934502 L29,19.3934502 L29,17.376385 L26.065,17.376385 L26.065,19.3934502 Z M58,13.082053 L45,6.62946112 L45,57.589607 L58,53.1934132 L58,13.082053 Z M60,53.9195567 C60,54.3522172 59.726,54.7374767 59.318,54.8756456 L44.318,59.9475563 C44.214,59.9828549 44.106,60 44,60 C43.793,60 43.588,59.9354539 43.416,59.8103959 C43.155,59.6207917 43,59.3162149 43,58.9914674 L43,47.6323642 L41.065,47.6323642 L41.065,45.6152989 L43,45.6152989 L43,19.3934502 L41.065,19.3934502 L41.065,17.376385 L43,17.376385 L43,5.00874917 C43,4.65979687 43.179,4.33605789 43.472,4.15250495 C43.765,3.96794348 44.131,3.94978989 44.441,4.10409539 L59.441,11.5490833 C59.783,11.7185168 60,12.0704947 60,12.4537371 L60,53.9195567 Z M34.934,25.8067093 L33.066,25.0825829 L28.066,38.1935072 L29.934,38.9176337 L34.934,25.8067093 Z M41.707,32.2088745 C42.098,31.8145383 42.098,31.1761371 41.707,30.7828094 L37.707,26.7486788 L36.293,28.174744 L39.586,31.4958419 L36.293,34.8169399 L37.707,36.2430051 L41.707,32.2088745 Z M26.293,36.2430051 L22.293,32.2088745 C21.902,31.8145383 21.902,31.1761371 22.293,30.7828094 L26.293,26.7486788 L27.707,28.174744 L24.414,31.4958419 L27.707,34.8169399 L26.293,36.2430051 Z" id="Amazon-API-Gateway_Icon_64_Squid"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
1
frontend/public/Logos/aws-alb.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 85 85" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#A" x="2.5" y="2.5"/><symbol id="A" overflow="visible"><g stroke="none"><path d="M0 41.579C0 20.293 17.84 3.157 40 3.157s40 17.136 40 38.422S62.16 80 40 80 0 62.864 0 41.579z" fill="#9d5025"/><path d="M0 38.422C0 17.136 17.84 0 40 0s40 17.136 40 38.422-17.84 38.422-40 38.422S0 59.707 0 38.422z" fill="#f58536"/><path d="M51.672 7.387v13.952H28.327V7.387zm18.061 40.378v11.364h-11.83V47.765zm-14.958 0v11.364h-11.83V47.765zm-18.206 0v11.364h-11.83V47.765zm-14.959 0v11.364H9.78V47.765z"/><path d="M14.63 37.929h2.13v11.149h-2.13z"/><path d="M14.63 37.929h17.088v2.045H14.63z"/><path d="M29.589 37.929h2.13v11.149H29.59zm18.206 0h2.13v11.149h-2.13z"/><path d="M47.795 37.929h17.088v2.045H47.795z"/><path d="M62.754 37.929h2.13v11.149h-2.129zm-40.631-7.954h2.13v8.977h-2.13zM38.935 19.28h2.13v10.859h-2.129z"/><path d="M22.123 29.116h35.32v2.045h-35.32z"/><path d="M55.314 29.975h2.13v8.977h-2.129z"/></g></symbol></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
frontend/public/Logos/celery.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" id="图层_1" x="0" y="0" version="1.1" viewBox="0 0 519.27 214.14"><style>.st2{fill:#a9cc54}</style><path d="M493.54 171.25c-35.63 0-64.52-28.89-64.52-64.52s28.89-64.52 64.52-64.52c3.85 0 7.62.36 11.29 1L493.54 5.45H104.46v.02C49.24 6.51 4.78 51.59 4.78 107.07c0 55.47 44.45 100.56 99.68 101.59v.02h389.07l11.29-38.44c-3.66.65-7.43 1.01-11.28 1.01" style="fill:#b6de64"/><defs><path id="SVGID_1_" d="M493.54 171.25c-35.63 0-64.52-28.89-64.52-64.52s28.89-64.52 64.52-64.52c3.85 0 7.62.36 11.29 1V5.45H104.46v.02C49.24 6.51 4.78 51.59 4.78 107.07c0 55.47 44.45 100.56 99.68 101.59v.02h389.07l11.29-38.44c-3.66.65-7.43 1.01-11.28 1.01"/></defs><clipPath id="SVGID_2_"><use xlink:href="#SVGID_1_" style="overflow:visible"/></clipPath><g style="clip-path:url(#SVGID_2_)"><path d="M-23.16 55.58H428.5v51.1H-23.16zM-23.16 157.59h530.98v72.35H-23.16z" class="st2"/></g><path d="M493.54 184.23c-43.94 0-79.56-33.62-79.56-77.56s35.62-76.56 79.56-76.56" style="fill:none;stroke:#ddf4a4;stroke-width:49;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
31
frontend/public/Logos/clickhouse.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns:x="ns_extend;" xmlns:i="ns_ai;" xmlns:graph="ns_graphs;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 50.6 50.6" style="enable-background:new 0 0 50.6 50.6;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<metadata>
|
||||
<sfw xmlns="ns_sfw;">
|
||||
<slices>
|
||||
</slices>
|
||||
<sliceSourceBounds bottomLeftOrigin="true" height="50.6" width="50.6" x="0" y="0">
|
||||
</sliceSourceBounds>
|
||||
</sfw>
|
||||
</metadata>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M0.6,0H5c0.3,0,0.6,0.3,0.6,0.6V50c0,0.3-0.3,0.6-0.6,0.6H0.6C0.3,50.6,0,50.4,0,50V0.6C0,0.3,0.3,0,0.6,0z">
|
||||
</path>
|
||||
<path class="st0" d="M11.8,0h4.4c0.3,0,0.6,0.3,0.6,0.6V50c0,0.3-0.3,0.6-0.6,0.6h-4.4c-0.3,0-0.6-0.3-0.6-0.6V0.6
|
||||
C11.3,0.3,11.5,0,11.8,0z">
|
||||
</path>
|
||||
<path class="st0" d="M23.1,0h4.4c0.3,0,0.6,0.3,0.6,0.6V50c0,0.3-0.3,0.6-0.6,0.6h-4.4c-0.3,0-0.6-0.3-0.6-0.6V0.6
|
||||
C22.5,0.3,22.8,0,23.1,0z">
|
||||
</path>
|
||||
<path class="st0" d="M34.3,0h4.4c0.3,0,0.6,0.3,0.6,0.6V50c0,0.3-0.3,0.6-0.6,0.6h-4.4c-0.3,0-0.6-0.3-0.6-0.6V0.6
|
||||
C33.7,0.3,34,0,34.3,0z">
|
||||
</path>
|
||||
<path class="st0" d="M45.6,19.7H50c0.3,0,0.6,0.3,0.6,0.6v10.1c0,0.3-0.3,0.6-0.6,0.6h-4.4c-0.3,0-0.6-0.3-0.6-0.6V20.3
|
||||
C45,20,45.3,19.7,45.6,19.7z">
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
30
frontend/public/Logos/confluent-kafka.svg
Normal file
@@ -0,0 +1,30 @@
|
||||
<svg version="1.1" id="Layer_1" xmlns:x="ns_extend;" xmlns:i="ns_ai;" xmlns:graph="ns_graphs;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 43 43" style="enable-background:new 0 0 43 43;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
|
||||
</style>
|
||||
<metadata>
|
||||
<sfw xmlns="ns_sfw;">
|
||||
<slices>
|
||||
</slices>
|
||||
<sliceSourceBounds bottomLeftOrigin="true" height="43" width="43" x="68.7" y="-221.7">
|
||||
</sliceSourceBounds>
|
||||
</sfw>
|
||||
</metadata>
|
||||
<g>
|
||||
<path class="st0" d="M30.8,22.7c1.1-0.1,2.2-0.1,3.3-0.2v-0.8c-1.1-0.1-2.2-0.1-3.3-0.2l-3.3-0.1c-1.9-0.1-3.9-0.1-5.8-0.1
|
||||
c0-1.9,0-3.9-0.1-5.8l-0.1-3.3c-0.1-1.1-0.1-2.2-0.2-3.3h-0.9c-0.1,1.1-0.1,2.2-0.2,3.3l-0.1,3.3c0,0.9,0,1.8,0,2.7
|
||||
c-0.4-0.8-0.7-1.7-1.1-2.5l-1.4-3c-0.5-1-0.9-2-1.4-3L15.4,10c0.4,1.1,0.7,2.1,1.1,3.1l1.2,3.1c0.3,0.9,0.7,1.7,1,2.6
|
||||
c-0.7-0.6-1.3-1.3-2-1.9l-2.4-2.3c-0.8-0.7-1.6-1.5-2.5-2.2l-0.7,0.7c0.7,0.8,1.5,1.6,2.2,2.5l2.3,2.4c0.6,0.7,1.3,1.3,1.9,2
|
||||
c-0.8-0.3-1.7-0.7-2.6-1l-3.1-1.2c-1-0.4-2.1-0.8-3.1-1.1l-0.4,0.9c1,0.5,2,0.9,3,1.4l3,1.4c0.8,0.4,1.7,0.7,2.5,1.1
|
||||
c-0.9,0-1.8,0-2.7,0l-3.3,0.1c-1.1,0.1-2.2,0.1-3.3,0.2v0.9c1.1,0.1,2.2,0.1,3.3,0.2l3.3,0.1c2,0.1,3.9,0.1,5.8,0.1
|
||||
c0,1.9,0,3.9,0.1,5.8l0.1,3.3c0.1,1.1,0.1,2.2,0.2,3.3h0.8c0.1-1.1,0.1-2.2,0.2-3.3l0.1-3.3c0-0.9,0-1.9,0.1-2.8
|
||||
c0.4,0.9,0.7,1.7,1.1,2.6l1.4,3c0.5,1,0.9,2,1.4,3l0.8-0.3c-0.3-1.1-0.7-2.1-1.1-3.1L24.1,28c-0.3-0.9-0.7-1.7-1-2.6
|
||||
c0.7,0.7,1.3,1.3,2,1.9l2.4,2.3c0.8,0.7,1.6,1.5,2.5,2.2l0.6-0.6c-0.7-0.8-1.5-1.6-2.2-2.5l-2.3-2.4c-0.6-0.7-1.3-1.4-1.9-2
|
||||
c0.9,0.3,1.7,0.7,2.6,1l3.1,1.2c1,0.4,2.1,0.8,3.1,1.1l0.3-0.8c-1-0.5-2-1-3-1.4l-3-1.4c-0.9-0.4-1.7-0.8-2.6-1.1
|
||||
c0.9,0,1.9,0,2.8-0.1C27.5,22.8,30.8,22.7,30.8,22.7z">
|
||||
</path>
|
||||
<path class="st0" d="M21.5,43C9.6,43,0,33.4,0,21.5S9.6,0,21.5,0S43,9.6,43,21.5S33.4,43,21.5,43 M21.5,2C10.8,2,2,10.8,2,21.5
|
||||
S10.8,41,21.5,41S41,32.2,41,21.5S32.2,2,21.5,2">
|
||||
</path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
18
frontend/public/Logos/elasticache.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="80px" height="80px" viewBox="0 0 80 80" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 64 (93537) - https://sketch.com -->
|
||||
<title>Icon-Architecture/64/Arch_Amazon-ElastiCache_64</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<linearGradient x1="0%" y1="100%" x2="100%" y2="0%" id="linearGradient-1">
|
||||
<stop stop-color="#2E27AD" offset="0%"></stop>
|
||||
<stop stop-color="#527FFF" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Icon-Architecture/64/Arch_Amazon-ElastiCache_64" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Icon-Architecture-BG/64/Database" fill="url(#linearGradient-1)">
|
||||
<rect id="Rectangle" x="0" y="0" width="80" height="80"></rect>
|
||||
</g>
|
||||
<path d="M51,61.5554864 L51,55.7029342 C48.466,57.3400887 43.904,58.1131616 39.556,58.1131616 C34.816,58.1131616 31.121,57.2860836 29,55.8679498 L29,61.5554864 C29,63.2486461 32.948,64.9998113 39.556,64.9998113 C46.3,64.9998113 51,63.1846401 51,61.5554864 L51,61.5554864 Z M39.556,49.2203227 C34.816,49.2203227 31.121,48.3942447 29,46.976111 L29,52.6866497 C29.031,54.3738088 32.973,56.1129729 39.556,56.1129729 C46.279,56.1129729 50.969,54.3088027 51,52.6826493 L51,46.8100953 C48.466,48.4482498 43.904,49.2203227 39.556,49.2203227 L39.556,49.2203227 Z M51,43.7908105 L51,37.0291726 C48.466,38.666327 43.904,39.4393999 39.556,39.4393999 C34.816,39.4393999 31.121,38.613322 29,37.1951882 L29,43.7948108 C29.031,45.48197 32.973,47.220134 39.556,47.220134 C46.279,47.220134 50.969,45.4159638 51,43.7908105 L51,43.7908105 Z M28.997,33.9928861 C28.997,33.9958864 28.998,33.9988867 28.998,34.001887 L29,34.001887 L29,34.012888 C29.031,35.7000472 32.973,37.4392112 39.556,37.4392112 C46.898,37.4392112 50.969,35.4170205 51,34.0098877 L51,34.001887 L51.002,34.001887 C51.002,33.9988867 51.003,33.9958864 51.003,33.9928861 C51.003,32.5847533 46.927,30.546561 39.556,30.546561 C32.946,30.546561 28.997,32.2987263 28.997,33.9928861 L28.997,33.9928861 Z M53,34.0178885 L53,43.7738088 L53.003,43.7738088 C53.003,43.7828097 53,43.7898104 53,43.7988112 L53,52.6666478 L53.003,52.6666478 C53.003,52.6756486 53,52.6826493 53,52.6916502 L53,61.5554864 C53,65.2968393 46.031,67 39.556,67 C31.929,67 27,64.8627984 27,61.5554864 L27,52.6976507 C27,52.6866497 26.997,52.6776488 26.997,52.6666478 L27,52.6666478 L27,43.8048118 C27,43.7948108 26.997,43.7848099 26.997,43.7738088 L27,43.7738088 L27,34.023889 C27,34.012888 26.997,34.0038872 26.997,33.9928861 C26.997,30.684574 31.927,28.5463723 39.556,28.5463723 C46.032,28.5463723 53.003,30.2505331 53.003,33.9928861 C53.003,34.001887 53,34.0088876 53,34.0178885 L53,34.0178885 Z M67,21.1206718 C67.553,21.1206718 68,20.6726295 68,20.1205774 L68,15.0000943 C68,14.4470422 67.553,14 67,14 L13,14 C12.447,14 12,14.4470422 12,15.0000943 L12,20.1205774 C12,20.6726295 12.447,21.1206718 13,21.1206718 C14.221,21.1206718 15.214,22.1077649 15.214,23.3208793 C15.214,24.5339938 14.221,25.5210869 13,25.5210869 C12.447,25.5210869 12,25.9691292 12,26.5211812 L12,47.0031135 C12,47.5551656 12.447,48.0032078 13,48.0032078 L23,48.0032078 L23,46.0030192 L18,46.0030192 L18,43.0027361 L23,43.0027361 L23,41.0025474 L17,41.0025474 C16.447,41.0025474 16,41.4495896 16,42.0026418 L16,46.0030192 L14,46.0030192 L14,27.4012643 C15.843,26.9522219 17.214,25.2930654 17.214,23.3208793 C17.214,21.3476932 15.843,19.6885367 14,19.2394943 L14,16.0001887 L66,16.0001887 L66,19.2394943 C64.157,19.6885367 62.786,21.3476932 62.786,23.3208793 C62.786,25.2930654 64.157,26.9522219 66,27.4012643 L66,46.0030192 L64,46.0030192 L64,42.0026418 C64,41.4495896 63.553,41.0025474 63,41.0025474 L57,41.0025474 L57,43.0027361 L62,43.0027361 L62,46.0030192 L57,46.0030192 L57,48.0032078 L67,48.0032078 C67.553,48.0032078 68,47.5551656 68,47.0031135 L68,26.5211812 C68,25.9691292 67.553,25.5210869 67,25.5210869 C65.779,25.5210869 64.786,24.5339938 64.786,23.3208793 C64.786,22.1077649 65.779,21.1206718 67,21.1206718 L67,21.1206718 Z M28,28.0013209 L28,20.0005661 C28,19.4475139 27.553,19.0004717 27,19.0004717 L21,19.0004717 C20.447,19.0004717 20,19.4475139 20,20.0005661 L20,37.00217 C20,37.5542221 20.447,38.0022644 21,38.0022644 L24,38.0022644 L24,36.0020757 L22,36.0020757 L22,21.0006604 L26,21.0006604 L26,28.0013209 L28,28.0013209 Z M58,36.0020757 L57,36.0020757 L57,38.0022644 L59,38.0022644 C59.553,38.0022644 60,37.5542221 60,37.00217 L60,20.0005661 C60,19.4475139 59.553,19.0004717 59,19.0004717 L53,19.0004717 C52.447,19.0004717 52,19.4475139 52,20.0005661 L52,28.0013209 L54,28.0013209 L54,21.0006604 L58,21.0006604 L58,36.0020757 Z M50,27.0012265 L50,20.0005661 C50,19.4475139 49.553,19.0004717 49,19.0004717 L42,19.0004717 C41.447,19.0004717 41,19.4475139 41,20.0005661 L41,26.0011322 L43,26.0011322 L43,21.0006604 L48,21.0006604 L48,27.0012265 L50,27.0012265 Z M37,26.0011322 L37,21.0006604 L32,21.0006604 L32,27.0012265 L30,27.0012265 L30,20.0005661 C30,19.4475139 30.447,19.0004717 31,19.0004717 L38,19.0004717 C38.553,19.0004717 39,19.4475139 39,20.0005661 L39,26.0011322 L37,26.0011322 Z" id="Amazon-ElastiCache_Icon_64_Squid" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
1
frontend/public/Logos/kafka.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="154" height="250" preserveAspectRatio="xMidYMid" viewBox="0 0 256 416"><path d="M201.816 230.216c-16.186 0-30.697 7.171-40.634 18.461l-25.463-18.026c2.703-7.442 4.255-15.433 4.255-23.797 0-8.219-1.498-16.076-4.112-23.408l25.406-17.835c9.936 11.233 24.409 18.365 40.548 18.365 29.875 0 54.184-24.305 54.184-54.184s-24.309-54.184-54.184-54.184-54.184 24.305-54.184 54.184c0 5.348.808 10.505 2.258 15.389l-25.423 17.844c-10.62-13.175-25.911-22.374-43.333-25.182v-30.64c24.544-5.155 43.037-26.962 43.037-53.019C124.171 24.305 99.862 0 69.987 0S15.803 24.305 15.803 54.184c0 25.708 18.014 47.246 42.067 52.769v31.038C25.044 143.753 0 172.401 0 206.854c0 34.621 25.292 63.374 58.355 68.94v32.774c-24.299 5.341-42.552 27.011-42.552 52.894 0 29.879 24.309 54.184 54.184 54.184s54.184-24.305 54.184-54.184c0-25.883-18.253-47.553-42.552-52.894v-32.775a69.97 69.97 0 0 0 42.6-24.776l25.633 18.143c-1.423 4.84-2.22 9.946-2.22 15.24 0 29.879 24.309 54.184 54.184 54.184S256 314.279 256 284.4s-24.309-54.184-54.184-54.184m0-126.695c14.487 0 26.27 11.788 26.27 26.271s-11.783 26.27-26.27 26.27-26.27-11.787-26.27-26.27 11.783-26.271 26.27-26.271m-158.1-49.337c0-14.483 11.784-26.27 26.271-26.27s26.27 11.787 26.27 26.27-11.783 26.27-26.27 26.27-26.271-11.787-26.271-26.27m52.541 307.278c0 14.483-11.783 26.27-26.27 26.27s-26.271-11.787-26.271-26.27 11.784-26.27 26.271-26.27 26.27 11.787 26.27 26.27m-26.272-117.97c-20.205 0-36.642-16.434-36.642-36.638 0-20.205 16.437-36.642 36.642-36.642 20.204 0 36.641 16.437 36.641 36.642 0 20.204-16.437 36.638-36.641 36.638m131.831 67.179c-14.487 0-26.27-11.788-26.27-26.271s11.783-26.27 26.27-26.27 26.27 11.787 26.27 26.27-11.783 26.271-26.27 26.271" style="fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
5
frontend/public/Logos/mongo.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="512" cy="512" r="512" style="fill:#13aa52"/>
|
||||
<path d="M648.86 449.44c-32.34-142.73-108.77-189.66-117-207.59-9-12.65-18.12-35.15-18.12-35.15-.15-.38-.39-1.05-.67-1.7-.93 12.65-1.41 17.53-13.37 30.29-18.52 14.48-113.54 94.21-121.27 256.37-7.21 151.24 109.25 241.36 125 252.85l1.79 1.27v-.11c.1.76 5 36 8.44 73.34H526a726.68 726.68 0 0 1 13-78.53l1-.65a204.48 204.48 0 0 0 20.11-16.45l.72-.65c33.48-30.93 93.67-102.47 93.08-216.53a347.07 347.07 0 0 0-5.05-56.76zM512.35 659.12s0-212.12 7-212.08c5.46 0 12.53 273.61 12.53 273.61-9.72-1.17-19.53-45.03-19.53-61.53z" style="fill:#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 818 B |
2
frontend/public/Logos/nginx.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><title>file_type_nginx</title><path d="M15.948,2h.065a10.418,10.418,0,0,1,.972.528Q22.414,5.65,27.843,8.774a.792.792,0,0,1,.414.788c-.008,4.389,0,8.777-.005,13.164a.813.813,0,0,1-.356.507q-5.773,3.324-11.547,6.644a.587.587,0,0,1-.657.037Q9.912,26.6,4.143,23.274a.7.7,0,0,1-.4-.666q0-6.582,0-13.163a.693.693,0,0,1,.387-.67Q9.552,5.657,14.974,2.535c.322-.184.638-.379.974-.535" style="fill:#019639"/><path d="M8.767,10.538q0,5.429,0,10.859a1.509,1.509,0,0,0,.427,1.087,1.647,1.647,0,0,0,2.06.206,1.564,1.564,0,0,0,.685-1.293c0-2.62-.005-5.24,0-7.86q3.583,4.29,7.181,8.568a2.833,2.833,0,0,0,2.6.782,1.561,1.561,0,0,0,1.251-1.371q.008-5.541,0-11.081a1.582,1.582,0,0,0-3.152,0c0,2.662-.016,5.321,0,7.982-2.346-2.766-4.663-5.556-7-8.332A2.817,2.817,0,0,0,10.17,9.033,1.579,1.579,0,0,0,8.767,10.538Z" style="fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
frontend/public/Logos/nuxtjs.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none"><path fill="#00DC82" d="M281.44 397.667h156.88c5.006 0 9.798-1.759 14.133-4.24 4.336-2.481 8.805-5.596 11.307-9.894 2.502-4.297 4.242-9.173 4.24-14.134s-1.734-9.836-4.24-14.131l-106-182.321c-2.502-4.297-5.559-7.413-9.893-9.894s-10.542-4.24-15.547-4.24-9.799 1.76-14.133 4.24c-4.335 2.481-7.392 5.597-9.894 9.894l-26.853 46.64-53.707-90.457c-2.504-4.296-5.557-8.823-9.893-11.303S208.713 115 203.707 115s-9.798.346-14.134 2.827c-4.335 2.48-8.802 7.007-11.306 11.303L46.827 355.268c-2.506 4.295-2.825 9.169-2.827 14.131s.325 9.836 2.827 14.134c2.502 4.297 6.97 7.413 11.306 9.894s9.127 4.24 14.134 4.24H171.2c39.201 0 67.734-17.585 87.627-50.88L306.88 263.4l25.44-43.813 77.733 132.853H306.88zM169.787 352.44h-69.254l103.174-178.08L256 263.4l-34.639 60.384c-13.21 21.603-28.272 28.656-51.574 28.656"/></svg>
|
||||
|
After Width: | Height: | Size: 881 B |
2
frontend/public/Logos/postgresql.svg
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
21
frontend/public/Logos/quickstart.svg
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 512 512" xml:space="preserve">
|
||||
<polygon style="fill:#FFD500;" points="382.395,228.568 291.215,228.568 330.762,10.199 129.603,283.43 220.785,283.43
|
||||
181.238,501.799 "/>
|
||||
<g>
|
||||
<path style="fill:#3D3D3D;" d="M181.234,512c-1.355,0-2.726-0.271-4.033-0.833c-4.357-1.878-6.845-6.514-5.999-11.184
|
||||
l37.371-206.353h-78.969c-3.846,0-7.367-2.164-9.103-5.597c-1.735-3.433-1.391-7.55,0.889-10.648L322.548,4.153
|
||||
c2.814-3.822,7.891-5.196,12.25-3.32c4.357,1.878,6.845,6.514,5.999,11.184L303.427,218.37h78.969c3.846,0,7.367,2.164,9.103,5.597
|
||||
c1.735,3.433,1.391,7.55-0.889,10.648L189.451,507.846C187.481,510.523,184.399,512,181.234,512z M149.777,273.231h71.007
|
||||
c3.023,0,5.89,1.341,7.828,3.662c1.938,2.32,2.747,5.38,2.208,8.355l-31.704,175.065l163.105-221.545h-71.007
|
||||
c-3.023,0-5.89-1.341-7.828-3.661c-1.938-2.32-2.747-5.38-2.208-8.355l31.704-175.065L149.777,273.231z"/>
|
||||
<path style="fill:#3D3D3D;" d="M267.666,171.348c-0.604,0-1.215-0.054-1.829-0.165c-5.543-1.004-9.223-6.31-8.22-11.853l0.923-5.1
|
||||
c1.003-5.543,6.323-9.225,11.852-8.219c5.543,1.004,9.223,6.31,8.22,11.853l-0.923,5.1
|
||||
C276.797,167.892,272.503,171.348,267.666,171.348z"/>
|
||||
<path style="fill:#3D3D3D;" d="M255.455,238.77c-0.604,0-1.215-0.054-1.83-0.165c-5.543-1.004-9.222-6.31-8.218-11.853
|
||||
l7.037-38.864c1.004-5.543,6.317-9.225,11.854-8.219c5.543,1.004,9.222,6.31,8.219,11.853l-7.037,38.864
|
||||
C264.587,235.314,260.293,238.77,255.455,238.77z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
frontend/public/Logos/reactjs.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_2" width="3618.6" height="3618.6"><style>.st2{fill:none;stroke:#00d8ff;stroke-width:144.7456;stroke-miterlimit:10}</style><path d="M0 0h3618.6v3618.6H0z" style="fill:#222"/><circle cx="1806.5" cy="1807.1" r="302.6" style="fill:#00d8ff"/><path d="M1806.5 1191.9c406.2 0 783.6 58.3 1068.1 156.2 342.8 118 553.6 296.9 553.6 458.9 0 168.8-223.4 358.9-591.5 480.8-278.3 92.2-644.6 140.4-1030.2 140.4-395.4 0-769.7-45.2-1051.2-141.4-356.1-121.7-570.6-314.2-570.6-479.8 0-160.7 201.3-338.2 539.3-456 285.6-99.5 672.3-159.1 1082.5-159.1z" class="st2"/><path d="M1271 1501.3c202.9-351.9 442-649.7 669-847.2 273.5-238 533.8-331.2 674.1-250.3 146.2 84.3 199.3 372.8 121 752.7-59.2 287.2-200.4 628.5-393.1 962.6-197.5 342.5-423.7 644.2-647.6 840-283.3 247.7-557.3 337.3-700.7 254.6-139.2-80.3-192.4-343.3-125.7-695 56.4-297.4 198-662.1 403-1017.4z" class="st2"/><path d="M1271.5 2119.8c-203.5-351.6-342.1-707.4-399.9-1002.7-69.6-355.8-20.4-627.9 119.8-709 146.1-84.6 422.5 13.5 712.5 271 219.3 194.7 444.4 487.5 637.6 821.3 198.1 342.2 346.6 688.8 404.3 980.5 73.1 369.2 13.9 651.3-129.4 734.2-139.1 80.5-393.5-4.7-664.9-238.2-229.2-197.3-474.5-502.1-680-857.1z" class="st2"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
2
frontend/public/Logos/redis.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 -18 256 256" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMinYMin meet"><path d="M245.97 168.943c-13.662 7.121-84.434 36.22-99.501 44.075-15.067 7.856-23.437 7.78-35.34 2.09-11.902-5.69-87.216-36.112-100.783-42.597C3.566 169.271 0 166.535 0 163.951v-25.876s98.05-21.345 113.879-27.024c15.828-5.679 21.32-5.884 34.79-.95 13.472 4.936 94.018 19.468 107.331 24.344l-.006 25.51c.002 2.558-3.07 5.364-10.024 8.988" fill="#912626"/><path d="M245.965 143.22c-13.661 7.118-84.431 36.218-99.498 44.072-15.066 7.857-23.436 7.78-35.338 2.09-11.903-5.686-87.214-36.113-100.78-42.594-13.566-6.485-13.85-10.948-.524-16.166 13.326-5.22 88.224-34.605 104.055-40.284 15.828-5.677 21.319-5.884 34.789-.948 13.471 4.934 83.819 32.935 97.13 37.81 13.316 4.881 13.827 8.9.166 16.02" fill="#C6302B"/><path d="M245.97 127.074c-13.662 7.122-84.434 36.22-99.501 44.078-15.067 7.853-23.437 7.777-35.34 2.087-11.903-5.687-87.216-36.112-100.783-42.597C3.566 127.402 0 124.67 0 122.085V96.206s98.05-21.344 113.879-27.023c15.828-5.679 21.32-5.885 34.79-.95C162.142 73.168 242.688 87.697 256 92.574l-.006 25.513c.002 2.557-3.07 5.363-10.024 8.987" fill="#912626"/><path d="M245.965 101.351c-13.661 7.12-84.431 36.218-99.498 44.075-15.066 7.854-23.436 7.777-35.338 2.087-11.903-5.686-87.214-36.112-100.78-42.594-13.566-6.483-13.85-10.947-.524-16.167C23.151 83.535 98.05 54.148 113.88 48.47c15.828-5.678 21.319-5.884 34.789-.949 13.471 4.934 83.819 32.933 97.13 37.81 13.316 4.88 13.827 8.9.166 16.02" fill="#C6302B"/><path d="M245.97 83.653c-13.662 7.12-84.434 36.22-99.501 44.078-15.067 7.854-23.437 7.777-35.34 2.087-11.903-5.687-87.216-36.113-100.783-42.595C3.566 83.98 0 81.247 0 78.665v-25.88s98.05-21.343 113.879-27.021c15.828-5.68 21.32-5.884 34.79-.95C162.142 29.749 242.688 44.278 256 49.155l-.006 25.512c.002 2.555-3.07 5.361-10.024 8.986" fill="#912626"/><path d="M245.965 57.93c-13.661 7.12-84.431 36.22-99.498 44.074-15.066 7.854-23.436 7.777-35.338 2.09C99.227 98.404 23.915 67.98 10.35 61.497-3.217 55.015-3.5 50.55 9.825 45.331 23.151 40.113 98.05 10.73 113.88 5.05c15.828-5.679 21.319-5.883 34.789-.948 13.471 4.935 83.819 32.934 97.13 37.811 13.316 4.876 13.827 8.897.166 16.017" fill="#C6302B"/><path d="M159.283 32.757l-22.01 2.285-4.927 11.856-7.958-13.23-25.415-2.284 18.964-6.839-5.69-10.498 17.755 6.944 16.738-5.48-4.524 10.855 17.067 6.391M131.032 90.275L89.955 73.238l58.86-9.035-17.783 26.072M74.082 39.347c17.375 0 31.46 5.46 31.46 12.194 0 6.736-14.085 12.195-31.46 12.195s-31.46-5.46-31.46-12.195c0-6.734 14.085-12.194 31.46-12.194" fill="#FFF"/><path d="M185.295 35.998l34.836 13.766-34.806 13.753-.03-27.52" fill="#621B1C"/><path d="M146.755 51.243l38.54-15.245.03 27.519-3.779 1.478-34.791-13.752" fill="#9A2928"/></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
1
frontend/public/Logos/strimzi.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" width="797.333" height="677.986" viewBox="1.333 9.018 797.333 677.986"><defs><path id="d" fill="#192c47" fill-opacity="1" fill-rule="nonzero" stroke="none" d="m0 0 17.613 99.761h32.831L32.831 0Z"/></defs><defs><clipPath id="a" clipPathUnits="userSpaceOnUse"><path d="M303.856 398.29c-5.979-7.665-76.496-24.775-132.823-50.154v-72.025L300 201.652l128.967 74.459v14.961c-21.435 4.168-48.369 8.136-81.695 11.016-113.441 9.803-8.402 32.212 39.215 72.386 47.618 40.175-158.259 52.26-158.259 52.26s84.031-17.671 75.628-28.444"/></clipPath><clipPath id="c" clipPathUnits="userSpaceOnUse"><path d="M0 524.999h600V0H0Z"/></clipPath><linearGradient id="b" x1="0" x2="1" y1="0" y2="0" gradientTransform="scale(445.57887 -445.57887)rotate(64.94 1.077 -.149)" gradientUnits="userSpaceOnUse" spreadMethod="pad"><stop offset="0" stop-color="#fff"/><stop offset="1" stop-color="#54bad8"/></linearGradient></defs><path fill="#192c47" d="M400 9.018 206.397 120.794V344.35L400 456.125l193.604-111.776V120.794z"/><g clip-path="url(#a)" transform="matrix(1.33333 0 0 -1.33333 0 699.999)"><path fill="url(#b)" d="M303.856 398.29c-5.979-7.665-76.496-24.775-132.823-50.154v-72.025L300 201.652l128.967 74.459v14.961c-21.435 4.168-48.369 8.136-81.695 11.016-113.441 9.803-8.402 32.212 39.215 72.386 47.618 40.175-158.259 52.26-158.259 52.26s84.031-17.671 75.628-28.444"/></g><g clip-path="url(#c)" transform="matrix(1.33333 0 0 -1.33333 0 699.999)"><path fill="#192c47" d="M74.835 82.594c-1.6 1.878-3.663 3.404-6.2 4.579q-3.804 1.761-8.877 1.762-3.946 0-7.116-1.691t-3.17-5.214q-.001-2.82 2.666-4.368 2.668-1.551 7.863-3.24 6.456-2.116 11.509-4.791 5.054-2.68 8.565-6.2 3.509-3.525 5.405-8.032 1.896-4.511 1.896-10.145-.001-9.583-4.21-16.345-4.21-6.765-10.735-10.991-6.526-4.227-14.39-6.2T42.709 9.747q-5.918.001-11.837.986A73 73 0 0 0 19.53 13.55a60.4 60.4 0 0 0-10.216 4.51Q4.524 20.736 1 24.118L23.263 44.69q1.69-1.975 4.087-3.523a29.5 29.5 0 0 1 5.002-2.607 36 36 0 0 1 5.284-1.691q2.676-.633 4.93-.634 4.933 0 7.893 1.62t2.959 4.72q-.002 2.537-2.466 4.51-2.467 1.972-9.23 4.086-5.357 1.691-10.075 3.945-4.722 2.254-8.172 5.426-3.454 3.17-5.425 7.609-1.974 4.438-1.972 10.779 0 7.747 3.17 14.372 3.17 6.622 9.018 11.484 5.846 4.86 13.949 7.609 8.102 2.746 17.825 2.747 5.354 0 10.568-.916 5.213-.918 9.864-2.536 4.649-1.622 8.594-3.875 3.945-2.255 6.623-5.073zM155.15 84.848l-12.68-72.284h-32.691l12.682 72.284H97.239l4.932 27.477h82.289l-4.933-27.477ZM221.517 69.772q6.479 0 10.145 2.184 3.662 2.183 3.664 6.975 0 4.366-3.03 5.988-3.032 1.62-8.243 1.62h-6.058l-2.96-16.767Zm7.186-57.208-13.668 36.495h-3.945l-6.2-36.495h-31.422l17.613 99.76h41.004q8.17 0 14.795-1.83 6.621-1.834 11.272-5.355 4.649-3.525 7.116-8.665 2.465-5.146 2.466-11.766-.001-12.4-5.847-20.431-5.849-8.032-15.994-10.991l19.446-40.722z"/><use xlink:href="#d" transform="translate(269.847 12.564)"/><path fill="#192c47" d="m409.483 12.564 11.132 57.77h-.845l-29.45-57.77h-22.968l-8.877 57.77h-.704l-8.596-57.77H318.74l17.613 99.76h41.567l8.736-54.67h.705l25.927 54.67H458.8l-17.613-99.76ZM453.867 12.564l4.79 27.336 46.5 45.37h-36.918l4.79 27.055h79.33l-4.227-23.672-49.034-48.331h41.707l-4.932-27.758Z"/><use xlink:href="#d" transform="translate(548.556 12.564)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -64,7 +64,7 @@ function App(): JSX.Element {
|
||||
// wait for the required data to be loaded before doing init for anything!
|
||||
if (!isFetchingActiveLicenseV3 && activeLicenseV3 && org) {
|
||||
const orgName =
|
||||
org && Array.isArray(org) && org.length > 0 ? org[0].name : '';
|
||||
org && Array.isArray(org) && org.length > 0 ? org[0].displayName : '';
|
||||
|
||||
const { name, email, role } = user;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/save';
|
||||
|
||||
@@ -7,7 +8,7 @@ import put from './put';
|
||||
const save = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
if (props.id && props.id > 0) {
|
||||
if (props.id && !isEmpty(props.id)) {
|
||||
return put({ ...props });
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,12 @@ const logEvent = async (
|
||||
rateLimited?: boolean,
|
||||
): Promise<SuccessResponse<EventSuccessPayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
// add tenant_url to attributes
|
||||
const { hostname } = window.location;
|
||||
const updatedAttributes = { ...attributes, tenant_url: hostname };
|
||||
const response = await axios.post('/event', {
|
||||
eventName,
|
||||
attributes,
|
||||
attributes: updatedAttributes,
|
||||
eventType: eventType || 'track',
|
||||
rateLimited: rateLimited || false, // TODO: Update this once we have a proper way to handle rate limiting
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface MetricDetails {
|
||||
timeSeriesTotal: number;
|
||||
timeSeriesActive: number;
|
||||
lastReceived: string;
|
||||
attributes: MetricDetailsAttribute[];
|
||||
attributes: MetricDetailsAttribute[] | null;
|
||||
metadata?: {
|
||||
metric_type: MetricType;
|
||||
description: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios from 'api';
|
||||
import { ApiV2Instance as axios } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
@@ -8,14 +8,12 @@ const editOrg = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`/org/${props.orgId}`, {
|
||||
name: props.name,
|
||||
isAnonymous: props.isAnonymous,
|
||||
hasOptedUpdates: props.hasOptedUpdates,
|
||||
const response = await axios.put(`/orgs/me`, {
|
||||
displayName: props.displayName,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
statusCode: 204,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
|
||||
@@ -45,6 +45,7 @@ export default function ChatSupportGateway(): JSX.Element {
|
||||
},
|
||||
);
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const handleAddCreditCard = (): void => {
|
||||
logEvent('Add Credit card modal: Clicked', {
|
||||
source: `intercom icon`,
|
||||
@@ -52,7 +53,7 @@ export default function ChatSupportGateway(): JSX.Element {
|
||||
});
|
||||
|
||||
updateCreditCard({
|
||||
url: window.location.href,
|
||||
url: window.location.origin,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ function LaunchChatSupport({
|
||||
});
|
||||
|
||||
updateCreditCard({
|
||||
url: window.location.href,
|
||||
url: window.location.origin,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
13
frontend/src/components/NewSelect/CustomMultiSelect.scss
Normal file
@@ -0,0 +1,13 @@
|
||||
.custom-multiselect-dropdown {
|
||||
.divider {
|
||||
height: 1px;
|
||||
background-color: #e8e8e8;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.all-option {
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
1765
frontend/src/components/NewSelect/CustomMultiSelect.tsx
Normal file
606
frontend/src/components/NewSelect/CustomSelect.tsx
Normal file
@@ -0,0 +1,606 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable react/function-component-definition */
|
||||
import './styles.scss';
|
||||
|
||||
import {
|
||||
CloseOutlined,
|
||||
DownOutlined,
|
||||
LoadingOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Select } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { capitalize, isEmpty } from 'lodash-es';
|
||||
import { ArrowDown, ArrowUp } from 'lucide-react';
|
||||
import type { BaseSelectRef } from 'rc-select';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { CustomSelectProps, OptionData } from './types';
|
||||
import {
|
||||
filterOptionsBySearch,
|
||||
prioritizeOrAddOptionForSingleSelect,
|
||||
SPACEKEY,
|
||||
} from './utils';
|
||||
|
||||
/**
|
||||
* CustomSelect Component
|
||||
*
|
||||
*/
|
||||
const CustomSelect: React.FC<CustomSelectProps> = ({
|
||||
placeholder = 'Search...',
|
||||
className,
|
||||
loading = false,
|
||||
onSearch,
|
||||
options = [],
|
||||
value,
|
||||
onChange,
|
||||
defaultActiveFirstOption = true,
|
||||
noDataMessage,
|
||||
onClear,
|
||||
getPopupContainer,
|
||||
dropdownRender,
|
||||
highlightSearch = true,
|
||||
placement = 'bottomLeft',
|
||||
popupMatchSelectWidth = true,
|
||||
popupClassName,
|
||||
errorMessage,
|
||||
allowClear = false,
|
||||
onRetry,
|
||||
...rest
|
||||
}) => {
|
||||
// ===== State & Refs =====
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [activeOptionIndex, setActiveOptionIndex] = useState<number>(-1);
|
||||
|
||||
// Refs for element access and scroll behavior
|
||||
const selectRef = useRef<BaseSelectRef>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const optionRefs = useRef<Record<number, HTMLDivElement | null>>({});
|
||||
|
||||
// ===== Option Filtering & Processing Utilities =====
|
||||
|
||||
/**
|
||||
* Checks if a label exists in the provided options
|
||||
*/
|
||||
const isLabelPresent = useCallback(
|
||||
(options: OptionData[], label: string): boolean =>
|
||||
options.some((option) => {
|
||||
const lowerLabel = label.toLowerCase();
|
||||
|
||||
// Check in nested options if they exist
|
||||
if ('options' in option && Array.isArray(option.options)) {
|
||||
return option.options.some(
|
||||
(subOption) => subOption.label.toLowerCase() === lowerLabel,
|
||||
);
|
||||
}
|
||||
|
||||
// Check top-level option
|
||||
return option.label.toLowerCase() === lowerLabel;
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* Separates section and non-section options
|
||||
*/
|
||||
const splitOptions = useCallback((options: OptionData[]): {
|
||||
sectionOptions: OptionData[];
|
||||
nonSectionOptions: OptionData[];
|
||||
} => {
|
||||
const sectionOptions: OptionData[] = [];
|
||||
const nonSectionOptions: OptionData[] = [];
|
||||
|
||||
options.forEach((option) => {
|
||||
if ('options' in option && Array.isArray(option.options)) {
|
||||
sectionOptions.push(option);
|
||||
} else {
|
||||
nonSectionOptions.push(option);
|
||||
}
|
||||
});
|
||||
|
||||
return { sectionOptions, nonSectionOptions };
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Apply search filtering to options
|
||||
*/
|
||||
const filteredOptions = useMemo(
|
||||
(): OptionData[] => filterOptionsBySearch(options, searchText),
|
||||
[options, searchText],
|
||||
);
|
||||
|
||||
// ===== UI & Rendering Functions =====
|
||||
|
||||
/**
|
||||
* Highlights matched text in search results
|
||||
*/
|
||||
const highlightMatchedText = useCallback(
|
||||
(text: string, searchQuery: string): React.ReactNode => {
|
||||
if (!searchQuery || !highlightSearch) return text;
|
||||
|
||||
const parts = text.split(new RegExp(`(${searchQuery})`, 'gi'));
|
||||
return (
|
||||
<>
|
||||
{parts.map((part, i) => {
|
||||
// Create a deterministic but unique key
|
||||
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
|
||||
|
||||
return part.toLowerCase() === searchQuery.toLowerCase() ? (
|
||||
<span key={uniqueKey} className="highlight-text">
|
||||
{part}
|
||||
</span>
|
||||
) : (
|
||||
part
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
},
|
||||
[highlightSearch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Renders an individual option with proper keyboard navigation support
|
||||
*/
|
||||
const renderOptionItem = useCallback(
|
||||
(
|
||||
option: OptionData,
|
||||
isSelected: boolean,
|
||||
index?: number,
|
||||
): React.ReactElement => {
|
||||
const handleSelection = (): void => {
|
||||
if (onChange) {
|
||||
onChange(option.value, option);
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isActive = index === activeOptionIndex;
|
||||
const optionId = `option-${index}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.value}
|
||||
id={optionId}
|
||||
ref={(el): void => {
|
||||
if (index !== undefined) {
|
||||
optionRefs.current[index] = el;
|
||||
}
|
||||
}}
|
||||
className={cx('option-item', {
|
||||
selected: isSelected,
|
||||
active: isActive,
|
||||
})}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
handleSelection();
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter' || e.key === SPACEKEY) {
|
||||
e.preventDefault();
|
||||
handleSelection();
|
||||
}
|
||||
}}
|
||||
onMouseEnter={(): void => setActiveOptionIndex(index || -1)}
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
aria-disabled={option.disabled}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
>
|
||||
<div className="option-content">
|
||||
<div>{highlightMatchedText(String(option.label || ''), searchText)}</div>
|
||||
{option.type === 'custom' && (
|
||||
<div className="option-badge">{capitalize(option.type)}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[highlightMatchedText, searchText, onChange, activeOptionIndex],
|
||||
);
|
||||
|
||||
/**
|
||||
* Helper function to render option with index tracking
|
||||
*/
|
||||
const renderOptionWithIndex = useCallback(
|
||||
(option: OptionData, isSelected: boolean, idx: number) =>
|
||||
renderOptionItem(option, isSelected, idx),
|
||||
[renderOptionItem],
|
||||
);
|
||||
|
||||
/**
|
||||
* Custom clear button renderer
|
||||
*/
|
||||
const clearIcon = useCallback(
|
||||
() => (
|
||||
<CloseOutlined
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
if (onChange) onChange(undefined, []);
|
||||
if (onClear) onClear();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
[onChange, onClear],
|
||||
);
|
||||
|
||||
// ===== Event Handlers =====
|
||||
|
||||
/**
|
||||
* Handles search input changes
|
||||
*/
|
||||
const handleSearch = useCallback(
|
||||
(value: string): void => {
|
||||
const trimmedValue = value.trim();
|
||||
setSearchText(trimmedValue);
|
||||
|
||||
if (onSearch) onSearch(trimmedValue);
|
||||
},
|
||||
[onSearch],
|
||||
);
|
||||
|
||||
/**
|
||||
* Prevents event propagation for dropdown clicks
|
||||
*/
|
||||
const handleDropdownClick = useCallback((e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Comprehensive keyboard navigation handler
|
||||
*/
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent): void => {
|
||||
// Handle keyboard navigation when dropdown is open
|
||||
if (isOpen) {
|
||||
// Get flattened list of all selectable options
|
||||
const getFlatOptions = (): OptionData[] => {
|
||||
if (!filteredOptions) return [];
|
||||
|
||||
const flatList: OptionData[] = [];
|
||||
|
||||
// Process options
|
||||
const { sectionOptions, nonSectionOptions } = splitOptions(
|
||||
isEmpty(value)
|
||||
? filteredOptions
|
||||
: prioritizeOrAddOptionForSingleSelect(filteredOptions, value),
|
||||
);
|
||||
|
||||
// Add custom option if needed
|
||||
if (!isEmpty(searchText) && !isLabelPresent(filteredOptions, searchText)) {
|
||||
flatList.push({
|
||||
label: searchText,
|
||||
value: searchText,
|
||||
type: 'custom',
|
||||
});
|
||||
}
|
||||
|
||||
// Add all options to flat list
|
||||
flatList.push(...nonSectionOptions);
|
||||
sectionOptions.forEach((section) => {
|
||||
if (section.options) {
|
||||
flatList.push(...section.options);
|
||||
}
|
||||
});
|
||||
|
||||
return flatList;
|
||||
};
|
||||
|
||||
const options = getFlatOptions();
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
setActiveOptionIndex((prev) =>
|
||||
prev < options.length - 1 ? prev + 1 : 0,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
setActiveOptionIndex((prev) =>
|
||||
prev > 0 ? prev - 1 : options.length - 1,
|
||||
);
|
||||
break;
|
||||
|
||||
case 'Tab':
|
||||
// Tab navigation with Shift key support
|
||||
if (e.shiftKey) {
|
||||
e.preventDefault();
|
||||
setActiveOptionIndex((prev) =>
|
||||
prev > 0 ? prev - 1 : options.length - 1,
|
||||
);
|
||||
} else {
|
||||
e.preventDefault();
|
||||
setActiveOptionIndex((prev) =>
|
||||
prev < options.length - 1 ? prev + 1 : 0,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
if (activeOptionIndex >= 0 && activeOptionIndex < options.length) {
|
||||
// Select the focused option
|
||||
const selectedOption = options[activeOptionIndex];
|
||||
if (onChange) {
|
||||
onChange(selectedOption.value, selectedOption);
|
||||
setIsOpen(false);
|
||||
setActiveOptionIndex(-1);
|
||||
}
|
||||
} else if (!isEmpty(searchText)) {
|
||||
// Add custom value when no option is focused
|
||||
const customOption = {
|
||||
label: searchText,
|
||||
value: searchText,
|
||||
type: 'custom',
|
||||
};
|
||||
if (onChange) {
|
||||
onChange(customOption.value, customOption);
|
||||
setIsOpen(false);
|
||||
setActiveOptionIndex(-1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
setIsOpen(false);
|
||||
setActiveOptionIndex(-1);
|
||||
break;
|
||||
|
||||
case ' ': // Space key
|
||||
if (activeOptionIndex >= 0 && activeOptionIndex < options.length) {
|
||||
e.preventDefault();
|
||||
const selectedOption = options[activeOptionIndex];
|
||||
if (onChange) {
|
||||
onChange(selectedOption.value, selectedOption);
|
||||
setIsOpen(false);
|
||||
setActiveOptionIndex(-1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (e.key === 'ArrowDown' || e.key === 'Tab') {
|
||||
// Open dropdown when Down or Tab is pressed while closed
|
||||
e.preventDefault();
|
||||
setIsOpen(true);
|
||||
setActiveOptionIndex(0);
|
||||
}
|
||||
},
|
||||
[
|
||||
isOpen,
|
||||
activeOptionIndex,
|
||||
filteredOptions,
|
||||
searchText,
|
||||
onChange,
|
||||
splitOptions,
|
||||
value,
|
||||
isLabelPresent,
|
||||
],
|
||||
);
|
||||
|
||||
// ===== Dropdown Rendering =====
|
||||
|
||||
/**
|
||||
* Renders the custom dropdown with sections and keyboard navigation
|
||||
*/
|
||||
const customDropdownRender = useCallback((): React.ReactElement => {
|
||||
// Process options based on current value
|
||||
let processedOptions = isEmpty(value)
|
||||
? filteredOptions
|
||||
: prioritizeOrAddOptionForSingleSelect(filteredOptions, value);
|
||||
|
||||
if (!isEmpty(searchText)) {
|
||||
processedOptions = filterOptionsBySearch(processedOptions, searchText);
|
||||
}
|
||||
|
||||
const { sectionOptions, nonSectionOptions } = splitOptions(processedOptions);
|
||||
|
||||
// Check if we need to add a custom option based on search text
|
||||
const isSearchTextNotPresent =
|
||||
!isEmpty(searchText) && !isLabelPresent(processedOptions, searchText);
|
||||
|
||||
let optionIndex = 0;
|
||||
|
||||
// Add custom option if needed
|
||||
if (isSearchTextNotPresent) {
|
||||
nonSectionOptions.unshift({
|
||||
label: searchText,
|
||||
value: searchText,
|
||||
type: 'custom',
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to map options with index tracking
|
||||
const mapOptions = (options: OptionData[]): React.ReactNode =>
|
||||
options.map((option) => {
|
||||
const result = renderOptionWithIndex(
|
||||
option,
|
||||
option.value === value,
|
||||
optionIndex,
|
||||
);
|
||||
optionIndex += 1;
|
||||
return result;
|
||||
});
|
||||
|
||||
const customMenu = (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className="custom-select-dropdown"
|
||||
onClick={handleDropdownClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
role="listbox"
|
||||
tabIndex={-1}
|
||||
aria-activedescendant={
|
||||
activeOptionIndex >= 0 ? `option-${activeOptionIndex}` : undefined
|
||||
}
|
||||
>
|
||||
{/* Non-section options */}
|
||||
<div className="no-section-options">
|
||||
{nonSectionOptions.length > 0 && mapOptions(nonSectionOptions)}
|
||||
</div>
|
||||
|
||||
{/* Section options */}
|
||||
{sectionOptions.length > 0 &&
|
||||
sectionOptions.map((section) =>
|
||||
!isEmpty(section.options) ? (
|
||||
<div className="select-group" key={section.label}>
|
||||
<div className="group-label" role="heading" aria-level={2}>
|
||||
{section.label}
|
||||
</div>
|
||||
<div role="group" aria-label={`${section.label} options`}>
|
||||
{section.options && mapOptions(section.options)}
|
||||
</div>
|
||||
</div>
|
||||
) : null,
|
||||
)}
|
||||
|
||||
{/* Navigation help footer */}
|
||||
<div className="navigation-footer" role="note">
|
||||
{!loading && !errorMessage && !noDataMessage && (
|
||||
<section className="navigate">
|
||||
<ArrowDown size={8} className="icons" />
|
||||
<ArrowUp size={8} className="icons" />
|
||||
<span className="keyboard-text">to navigate</span>
|
||||
</section>
|
||||
)}
|
||||
{loading && (
|
||||
<div className="navigation-loading">
|
||||
<div className="navigation-icons">
|
||||
<LoadingOutlined />
|
||||
</div>
|
||||
<div className="navigation-text">We are updating the values...</div>
|
||||
</div>
|
||||
)}
|
||||
{errorMessage && !loading && (
|
||||
<div className="navigation-error">
|
||||
<div className="navigation-text">
|
||||
{errorMessage || SOMETHING_WENT_WRONG}
|
||||
</div>
|
||||
<div className="navigation-icons">
|
||||
<ReloadOutlined
|
||||
twoToneColor={Color.BG_CHERRY_400}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
if (onRetry) onRetry();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{noDataMessage && !loading && (
|
||||
<div className="navigation-text">{noDataMessage}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return dropdownRender ? dropdownRender(customMenu) : customMenu;
|
||||
}, [
|
||||
value,
|
||||
filteredOptions,
|
||||
searchText,
|
||||
splitOptions,
|
||||
isLabelPresent,
|
||||
handleDropdownClick,
|
||||
handleKeyDown,
|
||||
activeOptionIndex,
|
||||
loading,
|
||||
errorMessage,
|
||||
noDataMessage,
|
||||
dropdownRender,
|
||||
renderOptionWithIndex,
|
||||
onRetry,
|
||||
]);
|
||||
|
||||
// ===== Side Effects =====
|
||||
|
||||
// Clear search text when dropdown closes
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setSearchText('');
|
||||
setActiveOptionIndex(-1);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Auto-scroll to active option for keyboard navigation
|
||||
useEffect(() => {
|
||||
if (
|
||||
isOpen &&
|
||||
activeOptionIndex >= 0 &&
|
||||
optionRefs.current[activeOptionIndex]
|
||||
) {
|
||||
optionRefs.current[activeOptionIndex]?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
});
|
||||
}
|
||||
}, [isOpen, activeOptionIndex]);
|
||||
|
||||
// ===== Final Processing =====
|
||||
|
||||
// Apply highlight to matched text in options
|
||||
const optionsWithHighlight = useMemo(
|
||||
() =>
|
||||
options
|
||||
?.filter((option) =>
|
||||
String(option.label || '')
|
||||
.toLowerCase()
|
||||
.includes(searchText.toLowerCase()),
|
||||
)
|
||||
?.map((option) => ({
|
||||
...option,
|
||||
label: highlightMatchedText(String(option.label || ''), searchText),
|
||||
})),
|
||||
[options, searchText, highlightMatchedText],
|
||||
);
|
||||
|
||||
// ===== Component Rendering =====
|
||||
return (
|
||||
<Select
|
||||
ref={selectRef}
|
||||
className={cx('custom-select', className)}
|
||||
placeholder={placeholder}
|
||||
showSearch
|
||||
filterOption={false}
|
||||
onSearch={handleSearch}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onDropdownVisibleChange={setIsOpen}
|
||||
open={isOpen}
|
||||
options={optionsWithHighlight}
|
||||
defaultActiveFirstOption={defaultActiveFirstOption}
|
||||
popupMatchSelectWidth={popupMatchSelectWidth}
|
||||
allowClear={allowClear ? { clearIcon } : false}
|
||||
getPopupContainer={getPopupContainer ?? popupContainer}
|
||||
suffixIcon={<DownOutlined style={{ cursor: 'default' }} />}
|
||||
dropdownRender={customDropdownRender}
|
||||
menuItemSelectedIcon={null}
|
||||
popupClassName={cx('custom-select-dropdown-container', popupClassName)}
|
||||
listHeight={300}
|
||||
placement={placement}
|
||||
optionFilterProp="label"
|
||||
notFoundContent={<div className="empty-message">{noDataMessage}</div>}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomSelect;
|
||||
@@ -0,0 +1,263 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import CustomMultiSelect from '../CustomMultiSelect';
|
||||
|
||||
// Mock scrollIntoView which isn't available in JSDOM
|
||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||
|
||||
// Mock options data
|
||||
const mockOptions = [
|
||||
{ label: 'Option 1', value: 'option1' },
|
||||
{ label: 'Option 2', value: 'option2' },
|
||||
{ label: 'Option 3', value: 'option3' },
|
||||
];
|
||||
|
||||
const mockGroupedOptions = [
|
||||
{
|
||||
label: 'Group 1',
|
||||
options: [
|
||||
{ label: 'Group 1 - Option 1', value: 'g1-option1' },
|
||||
{ label: 'Group 1 - Option 2', value: 'g1-option2' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Group 2',
|
||||
options: [
|
||||
{ label: 'Group 2 - Option 1', value: 'g2-option1' },
|
||||
{ label: 'Group 2 - Option 2', value: 'g2-option2' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
describe('CustomMultiSelect Component', () => {
|
||||
it('renders with placeholder', () => {
|
||||
const handleChange = jest.fn();
|
||||
render(
|
||||
<CustomMultiSelect
|
||||
placeholder="Select multiple options"
|
||||
options={mockOptions}
|
||||
onChange={handleChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Check placeholder exists
|
||||
const placeholderElement = screen.getByText('Select multiple options');
|
||||
expect(placeholderElement).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens dropdown when clicked', async () => {
|
||||
const handleChange = jest.fn();
|
||||
render(<CustomMultiSelect options={mockOptions} onChange={handleChange} />);
|
||||
|
||||
// Click to open the dropdown
|
||||
const selectElement = screen.getByRole('combobox');
|
||||
fireEvent.mouseDown(selectElement);
|
||||
|
||||
// Wait for dropdown to appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('ALL')).toBeInTheDocument(); // The ALL option
|
||||
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option 3')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('selects multiple options', async () => {
|
||||
const handleChange = jest.fn();
|
||||
|
||||
// Start with option1 already selected
|
||||
render(
|
||||
<CustomMultiSelect
|
||||
options={mockOptions}
|
||||
onChange={handleChange}
|
||||
value={['option1']}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Open dropdown
|
||||
const selectElement = screen.getByRole('combobox');
|
||||
fireEvent.mouseDown(selectElement);
|
||||
|
||||
// Wait for dropdown to appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Option 3')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click on Option 3
|
||||
const option3 = screen.getByText('Option 3');
|
||||
fireEvent.click(option3);
|
||||
|
||||
// Verify onChange was called with the right values
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('selects ALL options when ALL is clicked', async () => {
|
||||
const handleChange = jest.fn();
|
||||
render(
|
||||
<CustomMultiSelect
|
||||
options={mockOptions}
|
||||
onChange={handleChange}
|
||||
enableAllSelection
|
||||
/>,
|
||||
);
|
||||
|
||||
// Open dropdown
|
||||
const selectElement = screen.getByRole('combobox');
|
||||
fireEvent.mouseDown(selectElement);
|
||||
|
||||
// Wait for dropdown to appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('ALL')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click on ALL option
|
||||
const allOption = screen.getByText('ALL');
|
||||
fireEvent.click(allOption);
|
||||
|
||||
// Verify onChange was called with all option values
|
||||
expect(handleChange).toHaveBeenCalledWith(
|
||||
['option1', 'option2', 'option3'],
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ value: 'option1' }),
|
||||
expect.objectContaining({ value: 'option2' }),
|
||||
expect.objectContaining({ value: 'option3' }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('displays selected options as tags', async () => {
|
||||
render(
|
||||
<CustomMultiSelect options={mockOptions} value={['option1', 'option2']} />,
|
||||
);
|
||||
|
||||
// Check that option values are shown as tags (not labels)
|
||||
expect(screen.getByText('option1')).toBeInTheDocument();
|
||||
expect(screen.getByText('option2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('removes a tag when clicked', async () => {
|
||||
const handleChange = jest.fn();
|
||||
render(
|
||||
<CustomMultiSelect
|
||||
options={mockOptions}
|
||||
value={['option1', 'option2']}
|
||||
onChange={handleChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Find close button on Option 1 tag and click it
|
||||
const closeButtons = document.querySelectorAll(
|
||||
'.ant-select-selection-item-remove',
|
||||
);
|
||||
fireEvent.click(closeButtons[0]);
|
||||
|
||||
// Verify onChange was called with remaining option
|
||||
expect(handleChange).toHaveBeenCalledWith(
|
||||
['option2'],
|
||||
expect.arrayContaining([expect.objectContaining({ value: 'option2' })]),
|
||||
);
|
||||
});
|
||||
|
||||
it('filters options when searching', async () => {
|
||||
render(<CustomMultiSelect options={mockOptions} />);
|
||||
|
||||
// Open dropdown
|
||||
const selectElement = screen.getByRole('combobox');
|
||||
fireEvent.mouseDown(selectElement);
|
||||
|
||||
// Type into search box - get input directly
|
||||
const inputElement = selectElement.querySelector('input');
|
||||
if (inputElement) {
|
||||
fireEvent.change(inputElement, { target: { value: '2' } });
|
||||
}
|
||||
|
||||
// Wait for the dropdown filtering to happen
|
||||
await waitFor(() => {
|
||||
// Check that the dropdown is present
|
||||
const dropdownElement = document.querySelector(
|
||||
'.custom-multiselect-dropdown',
|
||||
);
|
||||
expect(dropdownElement).toBeInTheDocument();
|
||||
|
||||
// Verify Option 2 is visible in the dropdown
|
||||
const options = document.querySelectorAll('.option-label-text');
|
||||
let foundOption2 = false;
|
||||
|
||||
options.forEach((option) => {
|
||||
const text = option.textContent || '';
|
||||
if (text.includes('Option 2')) foundOption2 = true;
|
||||
});
|
||||
|
||||
expect(foundOption2).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders grouped options correctly', async () => {
|
||||
render(<CustomMultiSelect options={mockGroupedOptions} />);
|
||||
|
||||
// Open dropdown
|
||||
const selectElement = screen.getByRole('combobox');
|
||||
fireEvent.mouseDown(selectElement);
|
||||
|
||||
// Check group headers and options
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Group 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Group 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Group 1 - Option 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Group 1 - Option 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Group 2 - Option 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Group 2 - Option 2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows loading state', () => {
|
||||
render(<CustomMultiSelect options={mockOptions} loading />);
|
||||
|
||||
// Open dropdown
|
||||
const selectElement = screen.getByRole('combobox');
|
||||
fireEvent.mouseDown(selectElement);
|
||||
|
||||
// Check loading text is displayed
|
||||
expect(screen.getByText('We are updating the values...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error message', () => {
|
||||
render(
|
||||
<CustomMultiSelect
|
||||
options={mockOptions}
|
||||
errorMessage="Test error message"
|
||||
/>,
|
||||
);
|
||||
|
||||
// Open dropdown
|
||||
const selectElement = screen.getByRole('combobox');
|
||||
fireEvent.mouseDown(selectElement);
|
||||
|
||||
// Check error message is displayed
|
||||
expect(screen.getByText('Test error message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows no data message', () => {
|
||||
render(<CustomMultiSelect options={[]} noDataMessage="No data available" />);
|
||||
|
||||
// Open dropdown
|
||||
const selectElement = screen.getByRole('combobox');
|
||||
fireEvent.mouseDown(selectElement);
|
||||
|
||||
// Check no data message is displayed
|
||||
expect(screen.getByText('No data available')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "ALL" tag when all options are selected', () => {
|
||||
render(
|
||||
<CustomMultiSelect
|
||||
options={mockOptions}
|
||||
value={['option1', 'option2', 'option3']}
|
||||
maxTagCount={2}
|
||||
/>,
|
||||
);
|
||||
|
||||
// When all options are selected, component shows ALL tag instead
|
||||
expect(screen.getByText('ALL')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
206
frontend/src/components/NewSelect/__test__/CustomSelect.test.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import CustomSelect from '../CustomSelect';
|
||||
|
||||
// Mock scrollIntoView which isn't available in JSDOM
|
||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||
|
||||
// Mock options data
|
||||
const mockOptions = [
|
||||
{ label: 'Option 1', value: 'option1' },
|
||||
{ label: 'Option 2', value: 'option2' },
|
||||
{ label: 'Option 3', value: 'option3' },
|
||||
];
|
||||
|
||||
const mockGroupedOptions = [
|
||||
{
|
||||
label: 'Group 1',
|
||||
options: [
|
||||
{ label: 'Group 1 - Option 1', value: 'g1-option1' },
|
||||
{ label: 'Group 1 - Option 2', value: 'g1-option2' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Group 2',
|
||||
options: [
|
||||
{ label: 'Group 2 - Option 1', value: 'g2-option1' },
|
||||
{ label: 'Group 2 - Option 2', value: 'g2-option2' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
describe('CustomSelect Component', () => {
|
||||
it('renders with placeholder and options', () => {
|
||||
const handleChange = jest.fn();
|
||||
render(
|
||||
<CustomSelect
|
||||
placeholder="Test placeholder"
|
||||
options={mockOptions}
|
||||
onChange={handleChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Check placeholder exists in the DOM (not using getByPlaceholderText)
|
||||
const placeholderElement = screen.getByText('Test placeholder');
|
||||
expect(placeholderElement).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens dropdown when clicked', async () => {
|
||||
const handleChange = jest.fn();
|
||||
render(<CustomSelect options={mockOptions} onChange={handleChange} />);
|
||||
|
||||
// Click to open the dropdown
|
||||
const selectElement = screen.getByRole('combobox');
|
||||
fireEvent.mouseDown(selectElement);
|
||||
|
||||
// Wait for dropdown to appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Option 3')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onChange when option is selected', async () => {
|
||||
const handleChange = jest.fn();
|
||||
render(<CustomSelect options={mockOptions} onChange={handleChange} />);
|
||||
|
||||
// Open dropdown
|
||||
const selectElement = screen.getByRole('combobox');
|
||||
fireEvent.mouseDown(selectElement);
|
||||
|
||||
// Click on an option
|
||||
await waitFor(() => {
|
||||
const option = screen.getByText('Option 2');
|
||||
fireEvent.click(option);
|
||||
});
|
||||
|
||||
// Check onChange was called with correct value
|
||||
expect(handleChange).toHaveBeenCalledWith('option2', expect.anything());
|
||||
});
|
||||
|
||||
it('filters options when searching', async () => {
|
||||
render(<CustomSelect options={mockOptions} />);
|
||||
|
||||
// Open dropdown
|
||||
const selectElement = screen.getByRole('combobox');
|
||||
fireEvent.mouseDown(selectElement);
|
||||
|
||||
// Type into search box
|
||||
fireEvent.change(selectElement, { target: { value: '2' } });
|
||||
|
||||
// Dropdown should only show Option 2
|
||||
await waitFor(() => {
|
||||
// Check that the dropdown is present
|
||||
const dropdownElement = document.querySelector('.custom-select-dropdown');
|
||||
expect(dropdownElement).toBeInTheDocument();
|
||||
|
||||
// Use a simple approach to verify filtering
|
||||
const allOptionsInDropdown = document.querySelectorAll('.option-item');
|
||||
let foundOption2 = false;
|
||||
|
||||
allOptionsInDropdown.forEach((option) => {
|
||||
if (option.textContent?.includes('Option 2')) {
|
||||
foundOption2 = true;
|
||||
}
|
||||
|
||||
// Should not show Options 1 or 3
|
||||
expect(option.textContent).not.toContain('Option 1');
|
||||
expect(option.textContent).not.toContain('Option 3');
|
||||
});
|
||||
|
||||
expect(foundOption2).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders grouped options correctly', async () => {
|
||||
const handleChange = jest.fn();
|
||||
render(<CustomSelect options={mockGroupedOptions} onChange={handleChange} />);
|
||||
|
||||
// Open dropdown
|
||||
const selectElement = screen.getByRole('combobox');
|
||||
fireEvent.mouseDown(selectElement);
|
||||
|
||||
// Check group headers and options
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Group 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Group 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Group 1 - Option 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Group 1 - Option 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Group 2 - Option 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Group 2 - Option 2')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows loading state', () => {
|
||||
render(<CustomSelect options={mockOptions} loading />);
|
||||
|
||||
// Open dropdown
|
||||
const selectElement = screen.getByRole('combobox');
|
||||
fireEvent.mouseDown(selectElement);
|
||||
|
||||
// Check loading text is displayed
|
||||
expect(screen.getByText('We are updating the values...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error message', () => {
|
||||
render(
|
||||
<CustomSelect options={mockOptions} errorMessage="Test error message" />,
|
||||
);
|
||||
|
||||
// Open dropdown
|
||||
const selectElement = screen.getByRole('combobox');
|
||||
fireEvent.mouseDown(selectElement);
|
||||
|
||||
// Check error message is displayed
|
||||
expect(screen.getByText('Test error message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows no data message', () => {
|
||||
render(<CustomSelect options={[]} noDataMessage="No data available" />);
|
||||
|
||||
// Open dropdown
|
||||
const selectElement = screen.getByRole('combobox');
|
||||
fireEvent.mouseDown(selectElement);
|
||||
|
||||
// Check no data message is displayed
|
||||
expect(screen.getByText('No data available')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('supports keyboard navigation', async () => {
|
||||
const handleChange = jest.fn();
|
||||
render(<CustomSelect options={mockOptions} onChange={handleChange} />);
|
||||
|
||||
// Open dropdown using keyboard
|
||||
const selectElement = screen.getByRole('combobox');
|
||||
fireEvent.focus(selectElement);
|
||||
|
||||
// Press down arrow to open dropdown
|
||||
fireEvent.keyDown(selectElement, { key: 'ArrowDown' });
|
||||
|
||||
// Wait for dropdown to appear
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles selection via keyboard', async () => {
|
||||
const handleChange = jest.fn();
|
||||
render(<CustomSelect options={mockOptions} onChange={handleChange} />);
|
||||
|
||||
// Open dropdown
|
||||
const selectElement = screen.getByRole('combobox');
|
||||
fireEvent.mouseDown(selectElement);
|
||||
|
||||
// Wait for dropdown to appear then press Enter
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Option 1')).toBeInTheDocument();
|
||||
|
||||
// Press Enter to select first option
|
||||
fireEvent.keyDown(screen.getByText('Option 1'), { key: 'Enter' });
|
||||
});
|
||||
|
||||
// Check onChange was called
|
||||
expect(handleChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
4
frontend/src/components/NewSelect/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import CustomMultiSelect from './CustomMultiSelect';
|
||||
import CustomSelect from './CustomSelect';
|
||||
|
||||
export { CustomMultiSelect, CustomSelect };
|
||||
838
frontend/src/components/NewSelect/styles.scss
Normal file
@@ -0,0 +1,838 @@
|
||||
// Main container styles
|
||||
|
||||
// make const of #2c3044
|
||||
$custom-border-color: #2c3044;
|
||||
|
||||
.custom-select {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
&.ant-select-focused {
|
||||
.ant-select-selector {
|
||||
border-color: var(--bg-robin-500);
|
||||
box-shadow: 0 0 0 2px rgba(78, 116, 248, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
color: rgba(192, 193, 195, 0.45);
|
||||
}
|
||||
|
||||
// Base styles are for dark mode
|
||||
.ant-select-selector {
|
||||
background-color: var(--bg-ink-400);
|
||||
border-color: var(--bg-slate-400);
|
||||
}
|
||||
|
||||
.ant-select-clear {
|
||||
background-color: var(--bg-ink-400);
|
||||
color: rgba(192, 193, 195, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep chip styles ONLY in the multi-select
|
||||
.custom-multiselect {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
.ant-select-selector {
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
scrollbar-width: thin;
|
||||
background-color: var(--bg-ink-400);
|
||||
border-color: var(--bg-slate-400);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: $custom-border-color;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-select-focused {
|
||||
.ant-select-selector {
|
||||
border-color: var(--bg-robin-500);
|
||||
box-shadow: 0 0 0 2px rgba(78, 116, 248, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
color: rgba(192, 193, 195, 0.45);
|
||||
}
|
||||
|
||||
// Customize tags in multiselect (dark mode by default)
|
||||
.ant-select-selection-item {
|
||||
background-color: var(--bg-slate-400);
|
||||
border-radius: 4px;
|
||||
border: 1px solid $custom-border-color;
|
||||
margin-right: 4px;
|
||||
transition: all 0.2s;
|
||||
color: var(--bg-vanilla-400);
|
||||
|
||||
// Style for active tag (keyboard navigation)
|
||||
&-active {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
background-color: rgba(78, 116, 248, 0.15) !important;
|
||||
outline: 2px solid rgba(78, 116, 248, 0.2);
|
||||
}
|
||||
|
||||
// Style for selected tags (via keyboard or mouse selection)
|
||||
&-selected {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
background-color: rgba(78, 116, 248, 0.15) !important;
|
||||
box-shadow: 0 0 0 2px rgba(78, 116, 248, 0.2);
|
||||
}
|
||||
|
||||
.ant-select-selection-item-content {
|
||||
color: var(--bg-vanilla-400);
|
||||
}
|
||||
|
||||
.ant-select-selection-item-remove {
|
||||
color: rgba(192, 193, 195, 0.7);
|
||||
&:hover {
|
||||
color: rgba(192, 193, 195, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Class applied when in selection mode
|
||||
&.has-selection {
|
||||
.ant-select-selection-item-selected {
|
||||
cursor: move; // Indicate draggable
|
||||
}
|
||||
|
||||
// Change cursor for selection
|
||||
.ant-select-selector {
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown styles
|
||||
.custom-select-dropdown-container,
|
||||
.custom-multiselect-dropdown-container {
|
||||
z-index: 1050 !important;
|
||||
padding: 0;
|
||||
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.5), 0 6px 16px 0 rgba(0, 0, 0, 0.4),
|
||||
0 9px 28px 8px rgba(0, 0, 0, 0.3);
|
||||
background-color: var(--bg-ink-400);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
|
||||
.ant-select-item {
|
||||
padding: 8px 12px;
|
||||
color: var(--bg-vanilla-400);
|
||||
|
||||
// Make keyboard navigation visible
|
||||
&-option-active {
|
||||
background-color: var(--bg-slate-400) !important;
|
||||
}
|
||||
|
||||
&-option-selected {
|
||||
background-color: rgba(78, 116, 248, 0.15) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-select-dropdown-container,
|
||||
.custom-multiselect-dropdown-container {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
resize: horizontal;
|
||||
min-width: 300px !important;
|
||||
|
||||
.empty-message {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
color: rgba(192, 193, 195, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
// Custom dropdown styles for single select
|
||||
.custom-select-dropdown {
|
||||
padding: 8px 0 0 0;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: thin;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
width: 100%;
|
||||
background-color: var(--bg-ink-400);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: $custom-border-color;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: var(--bg-slate-400);
|
||||
}
|
||||
|
||||
.no-section-options {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.select-group {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.group-label {
|
||||
font-weight: 500;
|
||||
padding: 4px 12px;
|
||||
font-size: 13px;
|
||||
color: var(--bg-vanilla-400);
|
||||
background-color: var(--bg-slate-400);
|
||||
border-bottom: 1px solid $custom-border-color;
|
||||
border-top: 1px solid $custom-border-color;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.option-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--bg-vanilla-400);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-slate-400);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: rgba(78, 116, 248, 0.15);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: rgba(78, 116, 248, 0.15);
|
||||
border-color: var(--bg-robin-500);
|
||||
}
|
||||
|
||||
.option-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.option-label-text {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.option-badge {
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background-color: $custom-border-color;
|
||||
color: var(--bg-vanilla-400);
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid var(--bg-slate-400);
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background-color: var(--bg-ink-400);
|
||||
z-index: 1;
|
||||
|
||||
.navigation-icons {
|
||||
display: flex;
|
||||
margin-right: 8px;
|
||||
color: var(--bg-vanilla-400);
|
||||
}
|
||||
|
||||
.navigation-text {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.navigation-error {
|
||||
.navigation-text,
|
||||
.navigation-icons {
|
||||
color: var(--bg-cherry-500) !important;
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.navigation-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.navigation-text,
|
||||
.navigation-icons {
|
||||
color: var(--bg-robin-600) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.navigate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 12px;
|
||||
gap: 6px;
|
||||
|
||||
.icons {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 2.286px;
|
||||
border-top: 1.143px solid var(--bg-ink-200);
|
||||
border-right: 1.143px solid var(--bg-ink-200);
|
||||
border-bottom: 2.286px solid var(--bg-ink-200);
|
||||
border-left: 1.143px solid var(--bg-ink-200);
|
||||
background: var(--Ink-400, var(--bg-ink-400));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom dropdown styles for multi-select
|
||||
.custom-multiselect-dropdown {
|
||||
padding: 8px 0 0 0;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: thin;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
width: 100%;
|
||||
background-color: var(--bg-ink-400);
|
||||
|
||||
.select-all-option,
|
||||
.custom-value-option {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid $custom-border-color;
|
||||
margin-bottom: 8px;
|
||||
background-color: var(--bg-slate-400);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.selected-values-section {
|
||||
padding: 0 0 8px 0;
|
||||
border-bottom: 1px solid $custom-border-color;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.selected-option {
|
||||
padding: 4px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.select-group {
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
.group-label {
|
||||
font-weight: 500;
|
||||
padding: 4px 12px;
|
||||
font-size: 13px;
|
||||
color: var(--bg-vanilla-400);
|
||||
background-color: var(--bg-slate-400);
|
||||
border-bottom: 1px solid $custom-border-color;
|
||||
border-top: 1px solid $custom-border-color;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.option-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--bg-vanilla-400);
|
||||
|
||||
&.active {
|
||||
background-color: rgba(78, 116, 248, 0.15);
|
||||
border-color: var(--bg-robin-500);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-slate-400);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: rgba(78, 116, 248, 0.15);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.all-option {
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid $custom-border-color;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.option-checkbox {
|
||||
width: 100%;
|
||||
|
||||
> span:not(.ant-checkbox) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.option-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.option-label-text {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.option-badge {
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background-color: $custom-border-color;
|
||||
color: var(--bg-vanilla-400);
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.only-btn {
|
||||
display: none;
|
||||
}
|
||||
.toggle-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.only-btn:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
.toggle-btn:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.option-content:hover {
|
||||
.only-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 21px;
|
||||
}
|
||||
.toggle-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-badge {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.option-checkbox:hover {
|
||||
.toggle-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 21px;
|
||||
}
|
||||
.option-badge {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
color: rgba(192, 193, 195, 0.45);
|
||||
}
|
||||
|
||||
.status-message {
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
color: rgba(192, 193, 195, 0.65);
|
||||
border-top: 1px dashed $custom-border-color;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom styles for highlight text
|
||||
.highlight-text {
|
||||
background-color: rgba(78, 116, 248, 0.2);
|
||||
padding: 0 1px;
|
||||
border-radius: 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// Custom option styles for keyboard navigation
|
||||
.custom-option {
|
||||
&.focused,
|
||||
&.ant-select-item-option-active {
|
||||
background-color: var(--bg-slate-400) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Improve the sticky headers appearance
|
||||
.custom-select-dropdown-container {
|
||||
.group-label,
|
||||
.ant-select-item-group {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background-color: var(--bg-slate-400);
|
||||
border-bottom: 1px solid $custom-border-color;
|
||||
padding: 4px 12px;
|
||||
margin: 0;
|
||||
width: 100%; // Ensure the header spans full width
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); // Add subtle shadow for separation
|
||||
}
|
||||
|
||||
// Ensure proper spacing between sections
|
||||
.select-group {
|
||||
margin-bottom: 8px;
|
||||
position: relative; // Create a positioning context
|
||||
}
|
||||
}
|
||||
|
||||
// Custom scrollbar styling (shared between components)
|
||||
@mixin custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(192, 193, 195, 0.3) rgba(29, 33, 45, 0.6);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: rgba(29, 33, 45, 0.6);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(192, 193, 195, 0.3);
|
||||
border-radius: 10px;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(192, 193, 195, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subtle nested scrollbar styling
|
||||
@mixin nested-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(192, 193, 195, 0.2) rgba(29, 33, 45, 0.6);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: rgba(29, 33, 45, 0.6);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(192, 193, 195, 0.2);
|
||||
border-radius: 10px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(192, 193, 195, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply to main dropdown containers
|
||||
.custom-select-dropdown,
|
||||
.custom-multiselect-dropdown {
|
||||
@include custom-scrollbar;
|
||||
|
||||
// Main content area
|
||||
.options-container {
|
||||
@include custom-scrollbar;
|
||||
padding-right: 2px; // Add slight padding to prevent content touching scrollbar
|
||||
}
|
||||
|
||||
// Non-sectioned options
|
||||
.no-section-options {
|
||||
@include nested-scrollbar;
|
||||
margin-right: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply to dropdown container wrappers
|
||||
.custom-select-dropdown-container,
|
||||
.custom-multiselect-dropdown-container {
|
||||
@include custom-scrollbar;
|
||||
|
||||
// Add subtle shadow inside to indicate scrollable area
|
||||
&.has-overflow {
|
||||
box-shadow: inset 0 -10px 10px -10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
// Light Mode Overrides
|
||||
.lightMode {
|
||||
.custom-select {
|
||||
.ant-select-selector {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
border-color: #e9e9e9;
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.ant-select-clear {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
&.ant-select-focused {
|
||||
.ant-select-selector {
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-multiselect {
|
||||
.ant-select-selector {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
border-color: #e9e9e9;
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #e8e8e8;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
|
||||
.ant-select-selection-item-content {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.ant-select-selection-item-remove {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
&:hover {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
&-active {
|
||||
border-color: var(--bg-robin-500) !important;
|
||||
background-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
|
||||
&-selected {
|
||||
border-color: #1890ff !important;
|
||||
background-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-select-dropdown-container,
|
||||
.custom-multiselect-dropdown-container {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
border: 1px solid #f0f0f0;
|
||||
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||
0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.empty-message {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.ant-select-item {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
|
||||
&-option-active {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
|
||||
&-option-selected {
|
||||
background-color: var(--bg-vanilla-300) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-select-dropdown,
|
||||
.custom-multiselect-dropdown {
|
||||
border: 1px solid #f0f0f0;
|
||||
background-color: var(--bg-vanilla-100);
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.select-group {
|
||||
.group-label {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
background-color: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
.option-item {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
border-color: #91d5ff;
|
||||
}
|
||||
|
||||
.option-content {
|
||||
.option-badge {
|
||||
background-color: #f0f0f0;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-footer {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
background-color: var(--bg-vanilla-100);
|
||||
|
||||
.navigation-icons {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.navigation-text {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.navigate {
|
||||
.icons {
|
||||
border-top: 1.143px solid var(--bg-ink-200);
|
||||
border-right: 1.143px solid var(--bg-ink-200);
|
||||
border-bottom: 2.286px solid var(--bg-ink-200);
|
||||
border-left: 1.143px solid var(--bg-ink-200);
|
||||
background: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-multiselect-dropdown {
|
||||
.select-all-option,
|
||||
.custom-value-option {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.selected-values-section {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
border-top: 1px dashed #f0f0f0;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
&.all-option {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.highlight-text {
|
||||
background-color: rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
.custom-option {
|
||||
&.focused,
|
||||
&.ant-select-item-option-active {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-select-dropdown-container {
|
||||
.group-label,
|
||||
.ant-select-item-group {
|
||||
background-color: #f5f0f0;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
// Light mode scrollbar overrides
|
||||
.custom-select-dropdown,
|
||||
.custom-multiselect-dropdown,
|
||||
.custom-select-dropdown-container,
|
||||
.custom-multiselect-dropdown-container {
|
||||
scrollbar-color: rgba(0, 0, 0, 0.2) rgba(0, 0, 0, 0.05);
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
frontend/src/components/NewSelect/types.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { SelectProps } from 'antd';
|
||||
|
||||
export interface OptionData {
|
||||
label: string;
|
||||
value?: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
options?: OptionData[];
|
||||
type?: 'defined' | 'custom' | 'regex';
|
||||
}
|
||||
|
||||
export interface CustomSelectProps extends Omit<SelectProps, 'options'> {
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
onSearch?: (value: string) => void;
|
||||
options?: OptionData[];
|
||||
defaultActiveFirstOption?: boolean;
|
||||
noDataMessage?: string;
|
||||
onClear?: () => void;
|
||||
getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
|
||||
dropdownRender?: (menu: React.ReactElement) => React.ReactElement;
|
||||
highlightSearch?: boolean;
|
||||
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
|
||||
popupMatchSelectWidth?: boolean;
|
||||
errorMessage?: string;
|
||||
allowClear?: SelectProps['allowClear'];
|
||||
onRetry?: () => void;
|
||||
}
|
||||
|
||||
export interface CustomTagProps {
|
||||
label: React.ReactNode;
|
||||
value: string;
|
||||
closable: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export interface CustomMultiSelectProps
|
||||
extends Omit<SelectProps<string[] | string>, 'options'> {
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
onSearch?: (value: string) => void;
|
||||
options?: OptionData[];
|
||||
defaultActiveFirstOption?: boolean;
|
||||
dropdownMatchSelectWidth?: boolean | number;
|
||||
noDataMessage?: string;
|
||||
onClear?: () => void;
|
||||
enableAllSelection?: boolean;
|
||||
getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
|
||||
dropdownRender?: (menu: React.ReactElement) => React.ReactElement;
|
||||
highlightSearch?: boolean;
|
||||
errorMessage?: string;
|
||||
popupClassName?: string;
|
||||
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
|
||||
maxTagCount?: number;
|
||||
allowClear?: SelectProps['allowClear'];
|
||||
onRetry?: () => void;
|
||||
}
|
||||
135
frontend/src/components/NewSelect/utils.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { OptionData } from './types';
|
||||
|
||||
export const SPACEKEY = ' ';
|
||||
|
||||
export const prioritizeOrAddOptionForSingleSelect = (
|
||||
options: OptionData[],
|
||||
value: string,
|
||||
label?: string,
|
||||
): OptionData[] => {
|
||||
let foundOption: OptionData | null = null;
|
||||
|
||||
// Separate the found option and the rest
|
||||
const filteredOptions = options
|
||||
.map((option) => {
|
||||
if ('options' in option && Array.isArray(option.options)) {
|
||||
// Filter out the value from nested options
|
||||
const remainingSubOptions = option.options.filter(
|
||||
(subOption) => subOption.value !== value,
|
||||
);
|
||||
const extractedOption = option.options.find(
|
||||
(subOption) => subOption.value === value,
|
||||
);
|
||||
|
||||
if (extractedOption) foundOption = extractedOption;
|
||||
|
||||
// Keep the group if it still has remaining options
|
||||
return remainingSubOptions.length > 0
|
||||
? { ...option, options: remainingSubOptions }
|
||||
: null;
|
||||
}
|
||||
|
||||
// Check top-level options
|
||||
if (option.value === value) {
|
||||
foundOption = option;
|
||||
return null; // Remove it from the list
|
||||
}
|
||||
|
||||
return option;
|
||||
})
|
||||
.filter(Boolean) as OptionData[]; // Remove null values
|
||||
|
||||
// If not found, create a new option
|
||||
if (!foundOption) {
|
||||
foundOption = { value, label: label ?? value };
|
||||
}
|
||||
|
||||
// Add the found/new option at the top
|
||||
return [foundOption, ...filteredOptions];
|
||||
};
|
||||
|
||||
export const prioritizeOrAddOptionForMultiSelect = (
|
||||
options: OptionData[],
|
||||
values: string[], // Only supports multiple values (string[])
|
||||
labels?: Record<string, string>,
|
||||
): OptionData[] => {
|
||||
const foundOptions: OptionData[] = [];
|
||||
|
||||
// Separate the found options and the rest
|
||||
const filteredOptions = options
|
||||
.map((option) => {
|
||||
if ('options' in option && Array.isArray(option.options)) {
|
||||
// Filter out selected values from nested options
|
||||
const remainingSubOptions = option.options.filter(
|
||||
(subOption) => subOption.value && !values.includes(subOption.value),
|
||||
);
|
||||
const extractedOptions = option.options.filter(
|
||||
(subOption) => subOption.value && values.includes(subOption.value),
|
||||
);
|
||||
|
||||
if (extractedOptions.length > 0) {
|
||||
foundOptions.push(...extractedOptions);
|
||||
}
|
||||
|
||||
// Keep the group if it still has remaining options
|
||||
return remainingSubOptions.length > 0
|
||||
? { ...option, options: remainingSubOptions }
|
||||
: null;
|
||||
}
|
||||
|
||||
// Check top-level options
|
||||
if (option.value && values.includes(option.value)) {
|
||||
foundOptions.push(option);
|
||||
return null; // Remove it from the list
|
||||
}
|
||||
|
||||
return option;
|
||||
})
|
||||
.filter(Boolean) as OptionData[]; // Remove null values
|
||||
|
||||
// Find missing values that were not present in the original options and create new ones
|
||||
const missingValues = values.filter(
|
||||
(value) => !foundOptions.some((opt) => opt.value === value),
|
||||
);
|
||||
|
||||
const newOptions = missingValues.map((value) => ({
|
||||
value,
|
||||
label: labels?.[value] ?? value, // Use provided label or default to value
|
||||
}));
|
||||
|
||||
// Add found & new options to the top
|
||||
return [...newOptions, ...foundOptions, ...filteredOptions];
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters options based on search text
|
||||
*/
|
||||
export const filterOptionsBySearch = (
|
||||
options: OptionData[],
|
||||
searchText: string,
|
||||
): OptionData[] => {
|
||||
if (!searchText.trim()) return options;
|
||||
|
||||
const lowerSearchText = searchText.toLowerCase();
|
||||
|
||||
return options
|
||||
.map((option) => {
|
||||
if ('options' in option && Array.isArray(option.options)) {
|
||||
// Filter nested options
|
||||
const filteredSubOptions = option.options.filter((subOption) =>
|
||||
subOption.label.toLowerCase().includes(lowerSearchText),
|
||||
);
|
||||
|
||||
return filteredSubOptions.length > 0
|
||||
? { ...option, options: filteredSubOptions }
|
||||
: undefined;
|
||||
}
|
||||
|
||||
// Filter top-level options
|
||||
return option.label.toLowerCase().includes(lowerSearchText)
|
||||
? option
|
||||
: undefined;
|
||||
})
|
||||
.filter(Boolean) as OptionData[];
|
||||
};
|
||||
@@ -6,6 +6,7 @@ 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';
|
||||
@@ -68,10 +69,14 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
<section className="header">
|
||||
<section className="left-actions">
|
||||
<FilterOutlined />
|
||||
<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>
|
||||
<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>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="right-actions">
|
||||
@@ -89,31 +94,33 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
</section>
|
||||
)}
|
||||
|
||||
<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>
|
||||
</TypicalOverlayScrollbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
111
frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
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
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,382 @@
|
||||
// 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>
|
||||
`;
|
||||
30
frontend/src/components/QuickFilters/tests/constants.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
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,4 +40,5 @@ export enum QuickFiltersSource {
|
||||
INFRA_MONITORING = 'infra-monitoring',
|
||||
TRACES_EXPLORER = 'traces-explorer',
|
||||
API_MONITORING = 'api-monitoring',
|
||||
EXCEPTIONS = 'exceptions',
|
||||
}
|
||||
|
||||
@@ -1,30 +1,12 @@
|
||||
// keep this consistent with backend constants.go
|
||||
export enum FeatureKeys {
|
||||
SSO = 'SSO',
|
||||
ENTERPRISE_PLAN = 'ENTERPRISE_PLAN',
|
||||
BASIC_PLAN = 'BASIC_PLAN',
|
||||
ALERT_CHANNEL_SLACK = 'ALERT_CHANNEL_SLACK',
|
||||
ALERT_CHANNEL_WEBHOOK = 'ALERT_CHANNEL_WEBHOOK',
|
||||
ALERT_CHANNEL_PAGERDUTY = 'ALERT_CHANNEL_PAGERDUTY',
|
||||
ALERT_CHANNEL_OPSGENIE = 'ALERT_CHANNEL_OPSGENIE',
|
||||
ALERT_CHANNEL_MSTEAMS = 'ALERT_CHANNEL_MSTEAMS',
|
||||
DurationSort = 'DurationSort',
|
||||
TimestampSort = 'TimestampSort',
|
||||
SMART_TRACE_DETAIL = 'SMART_TRACE_DETAIL',
|
||||
CUSTOM_METRICS_FUNCTION = 'CUSTOM_METRICS_FUNCTION',
|
||||
QUERY_BUILDER_PANELS = 'QUERY_BUILDER_PANELS',
|
||||
QUERY_BUILDER_ALERTS = 'QUERY_BUILDER_ALERTS',
|
||||
DISABLE_UPSELL = 'DISABLE_UPSELL',
|
||||
USE_SPAN_METRICS = 'USE_SPAN_METRICS',
|
||||
OSS = 'OSS',
|
||||
ONBOARDING = 'ONBOARDING',
|
||||
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
||||
GATEWAY = 'GATEWAY',
|
||||
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
|
||||
QUERY_BUILDER_SEARCH_V2 = 'QUERY_BUILDER_SEARCH_V2',
|
||||
ANOMALY_DETECTION = 'ANOMALY_DETECTION',
|
||||
AWS_INTEGRATION = 'AWS_INTEGRATION',
|
||||
ONBOARDING_V3 = 'ONBOARDING_V3',
|
||||
THIRD_PARTY_API = 'THIRD_PARTY_API',
|
||||
TRACE_FUNNELS = 'TRACE_FUNNELS',
|
||||
}
|
||||
|
||||
@@ -27,4 +27,5 @@ 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,6 +398,23 @@ 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,3 +16,51 @@ 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,16 +18,17 @@ 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 { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
||||
import { convertCompositeQueryToTraceSelectedTags } 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, useRef } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -109,10 +110,11 @@ function AllErrors(): JSX.Element {
|
||||
);
|
||||
|
||||
const { queries } = useResourceAttribute();
|
||||
const compositeData = useGetCompositeQueryParam();
|
||||
|
||||
const [{ isLoading, data }, errorCountResponse] = useQueries([
|
||||
{
|
||||
queryKey: ['getAllErrors', updatedPath, maxTime, minTime, queries],
|
||||
queryKey: ['getAllErrors', updatedPath, maxTime, minTime, compositeData],
|
||||
queryFn: (): Promise<SuccessResponse<PayloadProps> | ErrorResponse> =>
|
||||
getAll({
|
||||
end: maxTime,
|
||||
@@ -123,7 +125,9 @@ function AllErrors(): JSX.Element {
|
||||
orderParam: getUpdatedParams,
|
||||
exceptionType: getUpdatedExceptionType,
|
||||
serviceName: getUpdatedServiceName,
|
||||
tags: convertRawQueriesToTraceSelectedTags(queries),
|
||||
tags: convertCompositeQueryToTraceSelectedTags(
|
||||
compositeData?.builder.queryData?.[0]?.filters.items,
|
||||
),
|
||||
}),
|
||||
enabled: !loading,
|
||||
},
|
||||
@@ -134,7 +138,7 @@ function AllErrors(): JSX.Element {
|
||||
minTime,
|
||||
getUpdatedExceptionType,
|
||||
getUpdatedServiceName,
|
||||
queries,
|
||||
compositeData,
|
||||
],
|
||||
queryFn: (): Promise<ErrorResponse | SuccessResponse<number>> =>
|
||||
getErrorCounts({
|
||||
@@ -142,7 +146,9 @@ function AllErrors(): JSX.Element {
|
||||
start: minTime,
|
||||
exceptionType: getUpdatedExceptionType,
|
||||
serviceName: getUpdatedServiceName,
|
||||
tags: convertRawQueriesToTraceSelectedTags(queries),
|
||||
tags: convertCompositeQueryToTraceSelectedTags(
|
||||
compositeData?.builder.queryData?.[0]?.filters.items,
|
||||
),
|
||||
}),
|
||||
enabled: !loading,
|
||||
},
|
||||
@@ -429,12 +435,8 @@ function AllErrors(): JSX.Element {
|
||||
[pathname],
|
||||
);
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (
|
||||
!logEventCalledRef.current &&
|
||||
!isUndefined(errorCountResponse.data?.payload)
|
||||
) {
|
||||
if (!isUndefined(errorCountResponse.data?.payload)) {
|
||||
const selectedEnvironments = queries.find(
|
||||
(val) => val.tagKey === 'resource_deployment_environment',
|
||||
)?.tagValue;
|
||||
@@ -442,9 +444,12 @@ function AllErrors(): JSX.Element {
|
||||
logEvent('Exception: List page visited', {
|
||||
numberOfExceptions: errorCountResponse?.data?.payload,
|
||||
selectedEnvironments,
|
||||
resourceAttributeUsed: !!queries?.length,
|
||||
resourceAttributeUsed: !!compositeData?.builder.queryData?.[0]?.filters
|
||||
.items?.length,
|
||||
tags: convertCompositeQueryToTraceSelectedTags(
|
||||
compositeData?.builder.queryData?.[0]?.filters.items,
|
||||
),
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [errorCountResponse.data?.payload]);
|
||||
|
||||
114
frontend/src/container/AllError/tests/AllError.test.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
94
frontend/src/container/AllError/tests/constants.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
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,6 +339,8 @@ 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';
|
||||
|
||||
@@ -661,7 +663,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
isMessagingQueues() ||
|
||||
isCloudIntegrationPage() ||
|
||||
isInfraMonitoring() ||
|
||||
isApiMonitoringView()
|
||||
isApiMonitoringView() ||
|
||||
isExceptionsView()
|
||||
? 0
|
||||
: '0 1rem',
|
||||
|
||||
|
||||