Compare commits

...

2 Commits

Author SHA1 Message Date
Aniket
95adea860a feat: added simultaneous temporality changes | 6175 2025-01-24 08:39:46 +05:30
Aniket
b581657bb7 feat: added simultaneous temporality changes | 6175 2025-01-24 00:49:57 +05:30
532 changed files with 8463 additions and 36351 deletions

View File

@@ -1,36 +0,0 @@
name: prereleaser
on:
# schedule every wednesday 9:30 AM UTC (3pm IST)
schedule:
- cron: '30 9 * * 3'
# allow manual triggering of the workflow by a maintainer
workflow_dispatch:
inputs:
release_type:
description: "Type of the release"
type: choice
required: true
options:
- 'patch'
- 'minor'
- 'major'
jobs:
verify:
uses: signoz/primus.workflows/.github/workflows/github-verify.yaml@main
secrets: inherit
with:
PRIMUS_REF: main
GITHUB_TEAM_NAME: releaser
GITHUB_MEMBER_NAME: ${{ github.actor }}
signoz:
if: ${{ always() && (needs.verify.result == 'success' || github.event.name == 'schedule') }}
uses: signoz/primus.workflows/.github/workflows/releaser.yaml@main
secrets: inherit
needs: [verify]
with:
PRIMUS_REF: main
PROJECT_NAME: signoz
RELEASE_TYPE: ${{ inputs.release_type || 'minor' }}

View File

@@ -57,17 +57,6 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- 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
- name: Setup golang - name: Setup golang
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:

View File

@@ -1,32 +0,0 @@
name: releaser
on:
# trigger on new latest release
release:
types: [published]
jobs:
detect:
runs-on: ubuntu-latest
outputs:
release_type: ${{ steps.find.outputs.release_type }}
steps:
- id: find
name: find
run: |
release_tag=${{ github.event.release.tag_name }}
patch_number=$(echo $release_tag | awk -F. '{print $3}')
release_type="minor"
if [[ $patch_number -ne 0 ]]; then
release_type="patch"
fi
echo "release_type=${release_type}" >> "$GITHUB_OUTPUT"
charts:
uses: signoz/primus.workflows/.github/workflows/github-trigger.yaml@main
secrets: inherit
needs: [detect]
with:
PRIMUS_REF: main
GITHUB_REPOSITORY_NAME: charts
GITHUB_EVENT_NAME: prereleaser
GITHUB_EVENT_PAYLOAD: "{\"release_type\": \"${{ needs.detect.outputs.release_type }}\"}"

View File

@@ -98,12 +98,12 @@ build-query-service-static-arm64:
# Steps to build static binary of query service for all platforms # Steps to build static binary of query service for all platforms
.PHONY: build-query-service-static-all .PHONY: build-query-service-static-all
build-query-service-static-all: build-query-service-static-amd64 build-query-service-static-arm64 build-frontend-static build-query-service-static-all: build-query-service-static-amd64 build-query-service-static-arm64
# Steps to build and push docker image of query service # Steps to build and push docker image of query service
.PHONY: build-query-service-amd64 build-push-query-service .PHONY: build-query-service-amd64 build-push-query-service
# Step to build docker image of query service in amd64 (used in build pipeline) # Step to build docker image of query service in amd64 (used in build pipeline)
build-query-service-amd64: build-query-service-static-amd64 build-frontend-static build-query-service-amd64: build-query-service-static-amd64
@echo "------------------" @echo "------------------"
@echo "--> Building query-service docker image for amd64" @echo "--> Building query-service docker image for amd64"
@echo "------------------" @echo "------------------"
@@ -190,4 +190,4 @@ check-no-ee-references:
fi fi
test: test:
go test ./pkg/... go test ./pkg/query-service/...

View File

@@ -1,80 +0,0 @@
##################### SigNoz Configuration Example #####################
#
# Do not modify this file
#
##################### Instrumentation #####################
instrumentation:
logs:
level: info
enabled: false
processors:
batch:
exporter:
otlp:
endpoint: localhost:4317
traces:
enabled: false
processors:
batch:
exporter:
otlp:
endpoint: localhost:4317
metrics:
enabled: true
readers:
pull:
exporter:
prometheus:
host: "0.0.0.0"
port: 9090
##################### Web #####################
web:
# Whether to enable the web frontend
enabled: true
# The prefix to serve web on
prefix: /
# The directory containing the static build files.
directory: /etc/signoz/web
##################### Cache #####################
cache:
# specifies the caching provider to use.
provider: memory
# memory: Uses in-memory caching.
memory:
# Time-to-live for cache entries in memory. Specify the duration in ns
ttl: 60000000000
# The interval at which the cache will be cleaned up
cleanupInterval: 1m
# redis: Uses Redis as the caching backend.
redis:
# The hostname or IP address of the Redis server.
host: localhost
# The port on which the Redis server is running. Default is usually 6379.
port: 6379
# The password for authenticating with the Redis server, if required.
password:
# The Redis database number to use
db: 0
##################### SQLStore #####################
sqlstore:
# specifies the SQLStore provider to use.
provider: sqlite
# The maximum number of open connections to the database.
max_open_conns: 100
sqlite:
# The path to the SQLite database file.
path: /var/lib/signoz/signoz.db
##################### APIServer #####################
apiserver:
timeout:
default: 60s
max: 600s
excluded_routes:
- /api/v1/logs/tail
- /api/v3/logs/livetail

View File

@@ -1,4 +1,5 @@
version: "3.9" version: "3.9"
x-clickhouse-defaults: &clickhouse-defaults x-clickhouse-defaults: &clickhouse-defaults
image: clickhouse/clickhouse-server:24.1.2-alpine image: clickhouse/clickhouse-server:24.1.2-alpine
tty: true tty: true
@@ -15,7 +16,14 @@ x-clickhouse-defaults: &clickhouse-defaults
max-file: "3" max-file: "3"
healthcheck: healthcheck:
# "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'" # "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'"
test: ["CMD", "wget", "--spider", "-q", "0.0.0.0:8123/ping"] test:
[
"CMD",
"wget",
"--spider",
"-q",
"0.0.0.0:8123/ping"
]
interval: 30s interval: 30s
timeout: 5s timeout: 5s
retries: 3 retries: 3
@@ -24,12 +32,15 @@ x-clickhouse-defaults: &clickhouse-defaults
nofile: nofile:
soft: 262144 soft: 262144
hard: 262144 hard: 262144
x-db-depend: &db-depend x-db-depend: &db-depend
depends_on: depends_on:
- clickhouse - clickhouse
- otel-collector-migrator - otel-collector-migrator
# - clickhouse-2 # - clickhouse-2
# - clickhouse-3 # - clickhouse-3
services: services:
zookeeper-1: zookeeper-1:
image: bitnami/zookeeper:3.7.1 image: bitnami/zookeeper:3.7.1
@@ -46,6 +57,7 @@ services:
# - ZOO_SERVERS=0.0.0.0:2888:3888,zookeeper-2:2888:3888,zookeeper-3:2888:3888 # - ZOO_SERVERS=0.0.0.0:2888:3888,zookeeper-2:2888:3888,zookeeper-3:2888:3888
- ALLOW_ANONYMOUS_LOGIN=yes - ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1 - ZOO_AUTOPURGE_INTERVAL=1
# zookeeper-2: # zookeeper-2:
# image: bitnami/zookeeper:3.7.0 # image: bitnami/zookeeper:3.7.0
# hostname: zookeeper-2 # hostname: zookeeper-2
@@ -77,8 +89,9 @@ services:
# - ZOO_SERVERS=zookeeper-1:2888:3888,zookeeper-2:2888:3888,0.0.0.0:2888:3888 # - ZOO_SERVERS=zookeeper-1:2888:3888,zookeeper-2:2888:3888,0.0.0.0:2888:3888
# - ALLOW_ANONYMOUS_LOGIN=yes # - ALLOW_ANONYMOUS_LOGIN=yes
# - ZOO_AUTOPURGE_INTERVAL=1 # - ZOO_AUTOPURGE_INTERVAL=1
clickhouse: clickhouse:
!!merge <<: *clickhouse-defaults <<: *clickhouse-defaults
hostname: clickhouse hostname: clickhouse
# ports: # ports:
# - "9000:9000" # - "9000:9000"
@@ -90,6 +103,7 @@ services:
- ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
# - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
- ./data/clickhouse/:/var/lib/clickhouse/ - ./data/clickhouse/:/var/lib/clickhouse/
# clickhouse-2: # clickhouse-2:
# <<: *clickhouse-defaults # <<: *clickhouse-defaults
# hostname: clickhouse-2 # hostname: clickhouse-2
@@ -117,6 +131,7 @@ services:
# - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml # - ./clickhouse-cluster.xml:/etc/clickhouse-server/config.d/cluster.xml
# # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml # # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
# - ./data/clickhouse-3/:/var/lib/clickhouse/ # - ./data/clickhouse-3/:/var/lib/clickhouse/
alertmanager: alertmanager:
image: signoz/alertmanager:0.23.7 image: signoz/alertmanager:0.23.7
volumes: volumes:
@@ -129,9 +144,15 @@ services:
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
query-service: query-service:
image: signoz/query-service:0.69.0 image: signoz/query-service:0.64.0
command: ["-config=/root/config/prometheus.yml", "--use-logs-new-schema=true", "--use-trace-new-schema=true"] command:
[
"-config=/root/config/prometheus.yml",
"--use-logs-new-schema=true",
"--use-trace-new-schema=true"
]
# ports: # ports:
# - "6060:6060" # pprof port # - "6060:6060" # pprof port
# - "8080:8080" # query-service port # - "8080:8080" # query-service port
@@ -149,16 +170,24 @@ services:
- TELEMETRY_ENABLED=true - TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-swarm - DEPLOYMENT_TYPE=docker-swarm
healthcheck: healthcheck:
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/health"] test:
[
"CMD",
"wget",
"--spider",
"-q",
"localhost:8080/api/v1/health"
]
interval: 30s interval: 30s
timeout: 5s timeout: 5s
retries: 3 retries: 3
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
!!merge <<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:0.69.0 image: signoz/frontend:0.64.0
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
@@ -169,9 +198,15 @@ services:
- "3301:3301" - "3301:3301"
volumes: volumes:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:0.111.24 image: signoz/signoz-otel-collector:0.111.16
command: ["--config=/etc/otel-collector-config.yaml", "--manager-config=/etc/manager-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"] command:
[
"--config=/etc/otel-collector-config.yaml",
"--manager-config=/etc/manager-config.yaml",
"--feature-gates=-pkg.translator.prometheus.NormalizeName"
]
user: root # required for reading docker container logs user: root # required for reading docker container logs
volumes: volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
@@ -201,20 +236,22 @@ services:
- clickhouse - clickhouse
- otel-collector-migrator - otel-collector-migrator
- query-service - query-service
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.111.24 image: signoz/signoz-schema-migrator:0.111.16
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
delay: 5s delay: 5s
command: command:
- "sync" - "sync"
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
- "--up=" - "--up="
depends_on: depends_on:
- clickhouse - clickhouse
# - clickhouse-2 # - clickhouse-2
# - clickhouse-3 # - clickhouse-3
logspout: logspout:
image: "gliderlabs/logspout:v3.2.14" image: "gliderlabs/logspout:v3.2.14"
volumes: volumes:
@@ -227,15 +264,17 @@ services:
mode: global mode: global
restart_policy: restart_policy:
condition: on-failure condition: on-failure
hotrod: hotrod:
image: jaegertracing/example-hotrod:1.30 image: jaegertracing/example-hotrod:1.30
command: ["all"] command: [ "all" ]
environment: environment:
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces - JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
logging: logging:
options: options:
max-size: 50m max-size: 50m
max-file: "3" max-file: "3"
load-hotrod: load-hotrod:
image: "signoz/locust:1.2.3" image: "signoz/locust:1.2.3"
hostname: load-hotrod hostname: load-hotrod

View File

@@ -1,6 +1,8 @@
version: "2.4" version: "2.4"
include: include:
- test-app-docker-compose.yaml - test-app-docker-compose.yaml
services: services:
zookeeper-1: zookeeper-1:
image: bitnami/zookeeper:3.7.1 image: bitnami/zookeeper:3.7.1
@@ -18,6 +20,7 @@ services:
# - ZOO_SERVERS=0.0.0.0:2888:3888,zookeeper-2:2888:3888,zookeeper-3:2888:3888 # - ZOO_SERVERS=0.0.0.0:2888:3888,zookeeper-2:2888:3888,zookeeper-3:2888:3888
- ALLOW_ANONYMOUS_LOGIN=yes - ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1 - ZOO_AUTOPURGE_INTERVAL=1
clickhouse: clickhouse:
image: clickhouse/clickhouse-server:24.1.2-alpine image: clickhouse/clickhouse-server:24.1.2-alpine
container_name: signoz-clickhouse container_name: signoz-clickhouse
@@ -40,10 +43,18 @@ services:
max-file: "3" max-file: "3"
healthcheck: healthcheck:
# "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'" # "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'"
test: ["CMD", "wget", "--spider", "-q", "0.0.0.0:8123/ping"] test:
[
"CMD",
"wget",
"--spider",
"-q",
"0.0.0.0:8123/ping"
]
interval: 30s interval: 30s
timeout: 5s timeout: 5s
retries: 3 retries: 3
alertmanager: alertmanager:
container_name: signoz-alertmanager container_name: signoz-alertmanager
image: signoz/alertmanager:0.23.7 image: signoz/alertmanager:0.23.7
@@ -56,25 +67,33 @@ services:
command: command:
- --queryService.url=http://query-service:8085 - --queryService.url=http://query-service:8085
- --storage.path=/data - --storage.path=/data
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.16}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "sync" - "sync"
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
- "--up=" - "--up="
depends_on: depends_on:
clickhouse: clickhouse:
condition: service_healthy condition: service_healthy
# clickhouse-2: # clickhouse-2:
# condition: service_healthy # condition: service_healthy
# clickhouse-3: # clickhouse-3:
# condition: service_healthy # condition: service_healthy
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector: otel-collector:
container_name: signoz-otel-collector container_name: signoz-otel-collector
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.24} image: signoz/signoz-otel-collector:0.111.16
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"] 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"
]
# user: root # required for reading docker container logs # user: root # required for reading docker container logs
volumes: volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
@@ -103,6 +122,7 @@ services:
condition: service_completed_successfully condition: service_completed_successfully
query-service: query-service:
condition: service_healthy condition: service_healthy
logspout: logspout:
image: "gliderlabs/logspout:v3.2.14" image: "gliderlabs/logspout:v3.2.14"
container_name: signoz-logspout container_name: signoz-logspout

View File

@@ -13,7 +13,14 @@ x-clickhouse-defaults: &clickhouse-defaults
max-file: "3" max-file: "3"
healthcheck: healthcheck:
# "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'" # "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'"
test: ["CMD", "wget", "--spider", "-q", "0.0.0.0:8123/ping"] test:
[
"CMD",
"wget",
"--spider",
"-q",
"0.0.0.0:8123/ping"
]
interval: 30s interval: 30s
timeout: 5s timeout: 5s
retries: 3 retries: 3
@@ -22,17 +29,20 @@ x-clickhouse-defaults: &clickhouse-defaults
nofile: nofile:
soft: 262144 soft: 262144
hard: 262144 hard: 262144
x-db-depend: &db-depend x-db-depend: &db-depend
depends_on: depends_on:
clickhouse: clickhouse:
condition: service_healthy condition: service_healthy
otel-collector-migrator-sync: otel-collector-migrator-sync:
condition: service_completed_successfully condition: service_completed_successfully
# clickhouse-2: # clickhouse-2:
# condition: service_healthy # condition: service_healthy
# clickhouse-3: # clickhouse-3:
# condition: service_healthy # condition: service_healthy
services: services:
zookeeper-1: zookeeper-1:
image: bitnami/zookeeper:3.7.1 image: bitnami/zookeeper:3.7.1
container_name: signoz-zookeeper-1 container_name: signoz-zookeeper-1
@@ -49,6 +59,7 @@ services:
# - ZOO_SERVERS=0.0.0.0:2888:3888,zookeeper-2:2888:3888,zookeeper-3:2888:3888 # - ZOO_SERVERS=0.0.0.0:2888:3888,zookeeper-2:2888:3888,zookeeper-3:2888:3888
- ALLOW_ANONYMOUS_LOGIN=yes - ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1 - ZOO_AUTOPURGE_INTERVAL=1
# zookeeper-2: # zookeeper-2:
# image: bitnami/zookeeper:3.7.0 # image: bitnami/zookeeper:3.7.0
# container_name: signoz-zookeeper-2 # container_name: signoz-zookeeper-2
@@ -82,8 +93,9 @@ services:
# - ZOO_SERVERS=zookeeper-1:2888:3888,zookeeper-2:2888:3888,0.0.0.0:2888:3888 # - ZOO_SERVERS=zookeeper-1:2888:3888,zookeeper-2:2888:3888,0.0.0.0:2888:3888
# - ALLOW_ANONYMOUS_LOGIN=yes # - ALLOW_ANONYMOUS_LOGIN=yes
# - ZOO_AUTOPURGE_INTERVAL=1 # - ZOO_AUTOPURGE_INTERVAL=1
clickhouse: clickhouse:
!!merge <<: *clickhouse-defaults <<: *clickhouse-defaults
container_name: signoz-clickhouse container_name: signoz-clickhouse
hostname: clickhouse hostname: clickhouse
ports: ports:
@@ -98,6 +110,7 @@ services:
# - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
- ./data/clickhouse/:/var/lib/clickhouse/ - ./data/clickhouse/:/var/lib/clickhouse/
- ./user_scripts:/var/lib/clickhouse/user_scripts/ - ./user_scripts:/var/lib/clickhouse/user_scripts/
# clickhouse-2: # clickhouse-2:
# <<: *clickhouse-defaults # <<: *clickhouse-defaults
# container_name: signoz-clickhouse-2 # container_name: signoz-clickhouse-2
@@ -115,6 +128,7 @@ services:
# - ./data/clickhouse-2/:/var/lib/clickhouse/ # - ./data/clickhouse-2/:/var/lib/clickhouse/
# - ./user_scripts:/var/lib/clickhouse/user_scripts/ # - ./user_scripts:/var/lib/clickhouse/user_scripts/
# clickhouse-3: # clickhouse-3:
# <<: *clickhouse-defaults # <<: *clickhouse-defaults
# container_name: signoz-clickhouse-3 # container_name: signoz-clickhouse-3
@@ -131,6 +145,7 @@ services:
# # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml # # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
# - ./data/clickhouse-3/:/var/lib/clickhouse/ # - ./data/clickhouse-3/:/var/lib/clickhouse/
# - ./user_scripts:/var/lib/clickhouse/user_scripts/ # - ./user_scripts:/var/lib/clickhouse/user_scripts/
alertmanager: alertmanager:
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.7} image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.7}
container_name: signoz-alertmanager container_name: signoz-alertmanager
@@ -143,11 +158,18 @@ services:
command: command:
- --queryService.url=http://query-service:8085 - --queryService.url=http://query-service:8085
- --storage.path=/data - --storage.path=/data
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service: query-service:
image: signoz/query-service:${DOCKER_TAG:-0.69.0} image: signoz/query-service:${DOCKER_TAG:-0.64.0}
container_name: signoz-query-service container_name: signoz-query-service
command: ["-config=/root/config/prometheus.yml", "--use-logs-new-schema=true", "--use-trace-new-schema=true"] command:
[
"-config=/root/config/prometheus.yml",
"--use-logs-new-schema=true",
"--use-trace-new-schema=true"
]
# ports: # ports:
# - "6060:6060" # pprof port # - "6060:6060" # pprof port
# - "8080:8080" # query-service port # - "8080:8080" # query-service port
@@ -166,13 +188,21 @@ services:
- DEPLOYMENT_TYPE=docker-standalone-amd - DEPLOYMENT_TYPE=docker-standalone-amd
restart: on-failure restart: on-failure
healthcheck: healthcheck:
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/health"] test:
[
"CMD",
"wget",
"--spider",
"-q",
"localhost:8080/api/v1/health"
]
interval: 30s interval: 30s
timeout: 5s timeout: 5s
retries: 3 retries: 3
!!merge <<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:${DOCKER_TAG:-0.69.0} image: signoz/frontend:${DOCKER_TAG:-0.64.0}
container_name: signoz-frontend container_name: signoz-frontend
restart: on-failure restart: on-failure
depends_on: depends_on:
@@ -182,8 +212,9 @@ services:
- "3301:3301" - "3301:3301"
volumes: volumes:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator-sync: otel-collector-migrator-sync:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.16}
container_name: otel-migrator-sync container_name: otel-migrator-sync
command: command:
- "sync" - "sync"
@@ -192,12 +223,13 @@ services:
depends_on: depends_on:
clickhouse: clickhouse:
condition: service_healthy condition: service_healthy
# clickhouse-2: # clickhouse-2:
# condition: service_healthy # condition: service_healthy
# clickhouse-3: # clickhouse-3:
# condition: service_healthy # condition: service_healthy
otel-collector-migrator-async: otel-collector-migrator-async:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.16}
container_name: otel-migrator-async container_name: otel-migrator-async
command: command:
- "async" - "async"
@@ -208,14 +240,21 @@ services:
condition: service_healthy condition: service_healthy
otel-collector-migrator-sync: otel-collector-migrator-sync:
condition: service_completed_successfully condition: service_completed_successfully
# clickhouse-2: # clickhouse-2:
# condition: service_healthy # condition: service_healthy
# clickhouse-3: # clickhouse-3:
# condition: service_healthy # condition: service_healthy
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.24} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.16}
container_name: signoz-otel-collector 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"] 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"
]
user: root # required for reading docker container logs user: root # required for reading docker container logs
volumes: volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
@@ -245,6 +284,7 @@ services:
condition: service_completed_successfully condition: service_completed_successfully
query-service: query-service:
condition: service_healthy condition: service_healthy
logspout: logspout:
image: "gliderlabs/logspout:v3.2.14" image: "gliderlabs/logspout:v3.2.14"
container_name: signoz-logspout container_name: signoz-logspout

View File

@@ -1,6 +1,8 @@
version: "2.4" version: "2.4"
include: include:
- test-app-docker-compose.yaml - test-app-docker-compose.yaml
x-clickhouse-defaults: &clickhouse-defaults x-clickhouse-defaults: &clickhouse-defaults
restart: on-failure restart: on-failure
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab # addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
@@ -16,7 +18,14 @@ x-clickhouse-defaults: &clickhouse-defaults
max-file: "3" max-file: "3"
healthcheck: healthcheck:
# "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'" # "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'"
test: ["CMD", "wget", "--spider", "-q", "0.0.0.0:8123/ping"] test:
[
"CMD",
"wget",
"--spider",
"-q",
"0.0.0.0:8123/ping"
]
interval: 30s interval: 30s
timeout: 5s timeout: 5s
retries: 3 retries: 3
@@ -25,17 +34,20 @@ x-clickhouse-defaults: &clickhouse-defaults
nofile: nofile:
soft: 262144 soft: 262144
hard: 262144 hard: 262144
x-db-depend: &db-depend x-db-depend: &db-depend
depends_on: depends_on:
clickhouse: clickhouse:
condition: service_healthy condition: service_healthy
otel-collector-migrator: otel-collector-migrator:
condition: service_completed_successfully condition: service_completed_successfully
# clickhouse-2: # clickhouse-2:
# condition: service_healthy # condition: service_healthy
# clickhouse-3: # clickhouse-3:
# condition: service_healthy # condition: service_healthy
services: services:
zookeeper-1: zookeeper-1:
image: bitnami/zookeeper:3.7.1 image: bitnami/zookeeper:3.7.1
container_name: signoz-zookeeper-1 container_name: signoz-zookeeper-1
@@ -52,6 +64,7 @@ services:
# - ZOO_SERVERS=0.0.0.0:2888:3888,zookeeper-2:2888:3888,zookeeper-3:2888:3888 # - ZOO_SERVERS=0.0.0.0:2888:3888,zookeeper-2:2888:3888,zookeeper-3:2888:3888
- ALLOW_ANONYMOUS_LOGIN=yes - ALLOW_ANONYMOUS_LOGIN=yes
- ZOO_AUTOPURGE_INTERVAL=1 - ZOO_AUTOPURGE_INTERVAL=1
# zookeeper-2: # zookeeper-2:
# image: bitnami/zookeeper:3.7.0 # image: bitnami/zookeeper:3.7.0
# container_name: signoz-zookeeper-2 # container_name: signoz-zookeeper-2
@@ -85,8 +98,9 @@ services:
# - ZOO_SERVERS=zookeeper-1:2888:3888,zookeeper-2:2888:3888,0.0.0.0:2888:3888 # - ZOO_SERVERS=zookeeper-1:2888:3888,zookeeper-2:2888:3888,0.0.0.0:2888:3888
# - ALLOW_ANONYMOUS_LOGIN=yes # - ALLOW_ANONYMOUS_LOGIN=yes
# - ZOO_AUTOPURGE_INTERVAL=1 # - ZOO_AUTOPURGE_INTERVAL=1
clickhouse: clickhouse:
!!merge <<: *clickhouse-defaults <<: *clickhouse-defaults
container_name: signoz-clickhouse container_name: signoz-clickhouse
hostname: clickhouse hostname: clickhouse
ports: ports:
@@ -101,6 +115,7 @@ services:
# - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
- ./data/clickhouse/:/var/lib/clickhouse/ - ./data/clickhouse/:/var/lib/clickhouse/
- ./user_scripts:/var/lib/clickhouse/user_scripts/ - ./user_scripts:/var/lib/clickhouse/user_scripts/
# clickhouse-2: # clickhouse-2:
# <<: *clickhouse-defaults # <<: *clickhouse-defaults
# container_name: signoz-clickhouse-2 # container_name: signoz-clickhouse-2
@@ -118,6 +133,7 @@ services:
# - ./data/clickhouse-2/:/var/lib/clickhouse/ # - ./data/clickhouse-2/:/var/lib/clickhouse/
# - ./user_scripts:/var/lib/clickhouse/user_scripts/ # - ./user_scripts:/var/lib/clickhouse/user_scripts/
# clickhouse-3: # clickhouse-3:
# <<: *clickhouse-defaults # <<: *clickhouse-defaults
# container_name: signoz-clickhouse-3 # container_name: signoz-clickhouse-3
@@ -134,6 +150,7 @@ services:
# # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml # # - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
# - ./data/clickhouse-3/:/var/lib/clickhouse/ # - ./data/clickhouse-3/:/var/lib/clickhouse/
# - ./user_scripts:/var/lib/clickhouse/user_scripts/ # - ./user_scripts:/var/lib/clickhouse/user_scripts/
alertmanager: alertmanager:
image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.7} image: signoz/alertmanager:${ALERTMANAGER_TAG:-0.23.7}
container_name: signoz-alertmanager container_name: signoz-alertmanager
@@ -146,11 +163,19 @@ services:
command: command:
- --queryService.url=http://query-service:8085 - --queryService.url=http://query-service:8085
- --storage.path=/data - --storage.path=/data
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service: query-service:
image: signoz/query-service:${DOCKER_TAG:-0.69.0} image: signoz/query-service:${DOCKER_TAG:-0.64.0}
container_name: signoz-query-service container_name: signoz-query-service
command: ["-config=/root/config/prometheus.yml", "-gateway-url=https://api.staging.signoz.cloud", "--use-logs-new-schema=true", "--use-trace-new-schema=true"] command:
[
"-config=/root/config/prometheus.yml",
"-gateway-url=https://api.staging.signoz.cloud",
"--use-logs-new-schema=true",
"--use-trace-new-schema=true"
]
# ports: # ports:
# - "6060:6060" # pprof port # - "6060:6060" # pprof port
# - "8080:8080" # query-service port # - "8080:8080" # query-service port
@@ -170,13 +195,21 @@ services:
- KAFKA_SPAN_EVAL=${KAFKA_SPAN_EVAL:-false} - KAFKA_SPAN_EVAL=${KAFKA_SPAN_EVAL:-false}
restart: on-failure restart: on-failure
healthcheck: healthcheck:
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/health"] test:
[
"CMD",
"wget",
"--spider",
"-q",
"localhost:8080/api/v1/health"
]
interval: 30s interval: 30s
timeout: 5s timeout: 5s
retries: 3 retries: 3
!!merge <<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:${DOCKER_TAG:-0.69.0} image: signoz/frontend:${DOCKER_TAG:-0.64.0}
container_name: signoz-frontend container_name: signoz-frontend
restart: on-failure restart: on-failure
depends_on: depends_on:
@@ -186,22 +219,31 @@ services:
- "3301:3301" - "3301:3301"
volumes: volumes:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.24} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.16}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
depends_on: depends_on:
clickhouse: clickhouse:
condition: service_healthy condition: service_healthy
# clickhouse-2: # clickhouse-2:
# condition: service_healthy # condition: service_healthy
# clickhouse-3: # clickhouse-3:
# condition: service_healthy # condition: service_healthy
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.24} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.16}
container_name: signoz-otel-collector 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"] 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"
]
user: root # required for reading docker container logs user: root # required for reading docker container logs
volumes: volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
@@ -231,6 +273,7 @@ services:
condition: service_completed_successfully condition: service_completed_successfully
query-service: query-service:
condition: service_healthy condition: service_healthy
logspout: logspout:
image: "gliderlabs/logspout:v3.2.14" image: "gliderlabs/logspout:v3.2.14"
container_name: signoz-logspout container_name: signoz-logspout

View File

@@ -32,11 +32,6 @@ has_cmd() {
command -v "$1" > /dev/null 2>&1 command -v "$1" > /dev/null 2>&1
} }
# Check if docker compose plugin is present
has_docker_compose_plugin() {
docker compose version > /dev/null 2>&1
}
is_mac() { is_mac() {
[[ $OSTYPE == darwin* ]] [[ $OSTYPE == darwin* ]]
} }
@@ -188,7 +183,9 @@ install_docker() {
$sudo_cmd yum-config-manager --add-repo https://download.docker.com/linux/$os/docker-ce.repo $sudo_cmd yum-config-manager --add-repo https://download.docker.com/linux/$os/docker-ce.repo
echo "Installing docker" echo "Installing docker"
$yum_cmd install docker-ce docker-ce-cli containerd.io $yum_cmd install docker-ce docker-ce-cli containerd.io
fi fi
} }
compose_version () { compose_version () {
@@ -230,6 +227,12 @@ start_docker() {
echo "Starting docker service" echo "Starting docker service"
$sudo_cmd systemctl start docker.service $sudo_cmd systemctl start docker.service
fi fi
# if [[ -z $sudo_cmd ]]; then
# docker ps > /dev/null && true
# if [[ $? -ne 0 ]]; then
# request_sudo
# fi
# fi
if [[ -z $sudo_cmd ]]; then if [[ -z $sudo_cmd ]]; then
if ! docker ps > /dev/null && true; then if ! docker ps > /dev/null && true; then
request_sudo request_sudo
@@ -262,7 +265,7 @@ bye() { # Prints a friendly good bye message and exits the script.
echo "🔴 The containers didn't seem to start correctly. Please run the following command to check containers that may have errored out:" echo "🔴 The containers didn't seem to start correctly. Please run the following command to check containers that may have errored out:"
echo "" echo ""
echo -e "$sudo_cmd $docker_compose_cmd -f ./docker/clickhouse-setup/docker-compose.yaml ps -a" echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml ps -a"
echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/" echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/"
echo "or reach us for support in #help channel in our Slack Community https://signoz.io/slack" echo "or reach us for support in #help channel in our Slack Community https://signoz.io/slack"
@@ -293,6 +296,11 @@ request_sudo() {
if (( $EUID != 0 )); then if (( $EUID != 0 )); then
sudo_cmd="sudo" sudo_cmd="sudo"
echo -e "Please enter your sudo password, if prompted." echo -e "Please enter your sudo password, if prompted."
# $sudo_cmd -l | grep -e "NOPASSWD: ALL" > /dev/null
# if [[ $? -ne 0 ]] && ! $sudo_cmd -v; then
# echo "Need sudo privileges to proceed with the installation."
# exit 1;
# fi
if ! $sudo_cmd -l | grep -e "NOPASSWD: ALL" > /dev/null && ! $sudo_cmd -v; then if ! $sudo_cmd -l | grep -e "NOPASSWD: ALL" > /dev/null && ! $sudo_cmd -v; then
echo "Need sudo privileges to proceed with the installation." echo "Need sudo privileges to proceed with the installation."
exit 1; exit 1;
@@ -309,7 +317,6 @@ echo -e "👋 Thank you for trying out SigNoz! "
echo "" echo ""
sudo_cmd="" sudo_cmd=""
docker_compose_cmd=""
# Check sudo permissions # Check sudo permissions
if (( $EUID != 0 )); then if (( $EUID != 0 )); then
@@ -355,8 +362,28 @@ else
SIGNOZ_INSTALLATION_ID=$(echo "$sysinfo" | $digest_cmd | grep -E -o '[a-zA-Z0-9]{64}') SIGNOZ_INSTALLATION_ID=$(echo "$sysinfo" | $digest_cmd | grep -E -o '[a-zA-Z0-9]{64}')
fi fi
# echo ""
# echo -e "👉 ${RED}Two ways to go forward\n"
# echo -e "${RED}1) ClickHouse as database (default)\n"
# read -p "⚙️ Enter your preference (1/2):" choice_setup
# while [[ $choice_setup != "1" && $choice_setup != "2" && $choice_setup != "" ]]
# do
# # echo $choice_setup
# echo -e "\n❌ ${CYAN}Please enter either 1 or 2"
# read -p "⚙️ Enter your preference (1/2): " choice_setup
# # echo $choice_setup
# done
# if [[ $choice_setup == "1" || $choice_setup == "" ]];then
# setup_type='clickhouse'
# fi
setup_type='clickhouse' setup_type='clickhouse'
# echo -e "\n✅ ${CYAN}You have chosen: ${setup_type} setup\n"
# Run bye if failure happens # Run bye if failure happens
trap bye EXIT trap bye EXIT
@@ -428,6 +455,8 @@ if [[ $desired_os -eq 0 ]]; then
send_event "os_not_supported" send_event "os_not_supported"
fi fi
# check_ports_occupied
# Check is Docker daemon is installed and available. If not, the install & start Docker for Linux machines. We cannot automatically install Docker Desktop on Mac OS # Check is Docker daemon is installed and available. If not, the install & start Docker for Linux machines. We cannot automatically install Docker Desktop on Mac OS
if ! is_command_present docker; then if ! is_command_present docker; then
@@ -457,39 +486,27 @@ if ! is_command_present docker; then
fi fi
fi fi
if has_docker_compose_plugin; then
echo "docker compose plugin is present, using it"
docker_compose_cmd="docker compose"
# Install docker-compose # Install docker-compose
else if ! is_command_present docker-compose; then
docker_compose_cmd="docker-compose" request_sudo
if ! is_command_present docker-compose; then install_docker_compose
request_sudo
install_docker_compose
fi
fi fi
start_docker start_docker
# check for open ports, if signoz is not installed # $sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml up -d --remove-orphans || true
if is_command_present docker-compose; then
if $sudo_cmd $docker_compose_cmd -f ./docker/clickhouse-setup/docker-compose.yaml ps | grep "signoz-query-service" | grep -q "healthy" > /dev/null 2>&1; then
echo "SigNoz already installed, skipping the occupied ports check"
else
check_ports_occupied
fi
fi
echo "" echo ""
echo -e "\n🟡 Pulling the latest container images for SigNoz.\n" echo -e "\n🟡 Pulling the latest container images for SigNoz.\n"
$sudo_cmd $docker_compose_cmd -f ./docker/clickhouse-setup/docker-compose.yaml pull $sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml pull
echo "" echo ""
echo "🟡 Starting the SigNoz containers. It may take a few minutes ..." echo "🟡 Starting the SigNoz containers. It may take a few minutes ..."
echo echo
# The $docker_compose_cmd command does some nasty stuff for the `--detach` functionality. So we add a `|| true` so that the # The docker-compose command does some nasty stuff for the `--detach` functionality. So we add a `|| true` so that the
# script doesn't exit because this command looks like it failed to do it's thing. # script doesn't exit because this command looks like it failed to do it's thing.
$sudo_cmd $docker_compose_cmd -f ./docker/clickhouse-setup/docker-compose.yaml up --detach --remove-orphans || true $sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml up --detach --remove-orphans || true
wait_for_containers_start 60 wait_for_containers_start 60
echo "" echo ""
@@ -499,7 +516,7 @@ if [[ $status_code -ne 200 ]]; then
echo "🔴 The containers didn't seem to start correctly. Please run the following command to check containers that may have errored out:" echo "🔴 The containers didn't seem to start correctly. Please run the following command to check containers that may have errored out:"
echo "" echo ""
echo -e "$sudo_cmd $docker_compose_cmd -f ./docker/clickhouse-setup/docker-compose.yaml ps -a" echo -e "$sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml ps -a"
echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/" echo "Please read our troubleshooting guide https://signoz.io/docs/install/troubleshooting/"
echo "or reach us on SigNoz for support https://signoz.io/slack" echo "or reach us on SigNoz for support https://signoz.io/slack"
@@ -520,7 +537,7 @@ else
echo " By default, retention period is set to 15 days for logs and traces, and 30 days for metrics." echo " By default, retention period is set to 15 days for logs and traces, and 30 days for metrics."
echo -e "To change this, navigate to the General tab on the Settings page of SigNoz UI. For more details, refer to https://signoz.io/docs/userguide/retention-period \n" echo -e "To change this, navigate to the General tab on the Settings page of SigNoz UI. For more details, refer to https://signoz.io/docs/userguide/retention-period \n"
echo " To bring down SigNoz and clean volumes : $sudo_cmd $docker_compose_cmd -f ./docker/clickhouse-setup/docker-compose.yaml down -v" echo " To bring down SigNoz and clean volumes : $sudo_cmd docker-compose -f ./docker/clickhouse-setup/docker-compose.yaml down -v"
echo "" echo ""
echo "+++++++++++++++++++++++++++++++++++++++++++++++++" echo "+++++++++++++++++++++++++++++++++++++++++++++++++"

View File

@@ -23,9 +23,6 @@ COPY pkg/query-service/templates /root/templates
# Make query-service executable for non-root users # Make query-service executable for non-root users
RUN chmod 755 /root /root/query-service RUN chmod 755 /root /root/query-service
# Copy frontend
COPY frontend/build/ /etc/signoz/web/
# run the binary # run the binary
ENTRYPOINT ["./query-service"] ENTRYPOINT ["./query-service"]

View File

@@ -12,7 +12,6 @@ import (
"go.signoz.io/signoz/ee/query-service/license" "go.signoz.io/signoz/ee/query-service/license"
"go.signoz.io/signoz/ee/query-service/usage" "go.signoz.io/signoz/ee/query-service/usage"
baseapp "go.signoz.io/signoz/pkg/query-service/app" baseapp "go.signoz.io/signoz/pkg/query-service/app"
"go.signoz.io/signoz/pkg/query-service/app/cloudintegrations"
"go.signoz.io/signoz/pkg/query-service/app/integrations" "go.signoz.io/signoz/pkg/query-service/app/integrations"
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline" "go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
"go.signoz.io/signoz/pkg/query-service/cache" "go.signoz.io/signoz/pkg/query-service/cache"
@@ -35,7 +34,6 @@ type APIHandlerOptions struct {
FeatureFlags baseint.FeatureLookup FeatureFlags baseint.FeatureLookup
LicenseManager *license.Manager LicenseManager *license.Manager
IntegrationsController *integrations.Controller IntegrationsController *integrations.Controller
CloudIntegrationsController *cloudintegrations.Controller
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
Cache cache.Cache Cache cache.Cache
Gateway *httputil.ReverseProxy Gateway *httputil.ReverseProxy
@@ -64,7 +62,6 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
RuleManager: opts.RulesManager, RuleManager: opts.RulesManager,
FeatureFlags: opts.FeatureFlags, FeatureFlags: opts.FeatureFlags,
IntegrationsController: opts.IntegrationsController, IntegrationsController: opts.IntegrationsController,
CloudIntegrationsController: opts.CloudIntegrationsController,
LogsParsingPipelineController: opts.LogsParsingPipelineController, LogsParsingPipelineController: opts.LogsParsingPipelineController,
Cache: opts.Cache, Cache: opts.Cache,
FluxInterval: opts.FluxInterval, FluxInterval: opts.FluxInterval,

View File

@@ -7,7 +7,6 @@ import (
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"go.signoz.io/signoz/pkg/cache"
basechr "go.signoz.io/signoz/pkg/query-service/app/clickhouseReader" basechr "go.signoz.io/signoz/pkg/query-service/app/clickhouseReader"
"go.signoz.io/signoz/pkg/query-service/interfaces" "go.signoz.io/signoz/pkg/query-service/interfaces"
) )
@@ -28,10 +27,8 @@ func NewDataConnector(
cluster string, cluster string,
useLogsNewSchema bool, useLogsNewSchema bool,
useTraceNewSchema bool, useTraceNewSchema bool,
fluxIntervalForTraceDetail time.Duration,
cache cache.Cache,
) *ClickhouseReader { ) *ClickhouseReader {
ch := basechr.NewReader(localDB, promConfigPath, lm, maxIdleConns, maxOpenConns, dialTimeout, cluster, useLogsNewSchema, useTraceNewSchema, fluxIntervalForTraceDetail, cache) ch := basechr.NewReader(localDB, promConfigPath, lm, maxIdleConns, maxOpenConns, dialTimeout, cluster, useLogsNewSchema, useTraceNewSchema)
return &ClickhouseReader{ return &ClickhouseReader{
conn: ch.GetConn(), conn: ch.GetConn(),
appdb: localDB, appdb: localDB,

View File

@@ -29,18 +29,15 @@ import (
"go.signoz.io/signoz/ee/query-service/integrations/gateway" "go.signoz.io/signoz/ee/query-service/integrations/gateway"
"go.signoz.io/signoz/ee/query-service/interfaces" "go.signoz.io/signoz/ee/query-service/interfaces"
"go.signoz.io/signoz/ee/query-service/rules" "go.signoz.io/signoz/ee/query-service/rules"
"go.signoz.io/signoz/pkg/http/middleware"
baseauth "go.signoz.io/signoz/pkg/query-service/auth" baseauth "go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/migrate"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3" v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/signoz"
"go.signoz.io/signoz/pkg/web"
licensepkg "go.signoz.io/signoz/ee/query-service/license" licensepkg "go.signoz.io/signoz/ee/query-service/license"
"go.signoz.io/signoz/ee/query-service/usage" "go.signoz.io/signoz/ee/query-service/usage"
"go.signoz.io/signoz/pkg/query-service/agentConf" "go.signoz.io/signoz/pkg/query-service/agentConf"
baseapp "go.signoz.io/signoz/pkg/query-service/app" baseapp "go.signoz.io/signoz/pkg/query-service/app"
"go.signoz.io/signoz/pkg/query-service/app/cloudintegrations"
"go.signoz.io/signoz/pkg/query-service/app/dashboards" "go.signoz.io/signoz/pkg/query-service/app/dashboards"
baseexplorer "go.signoz.io/signoz/pkg/query-service/app/explorer" baseexplorer "go.signoz.io/signoz/pkg/query-service/app/explorer"
"go.signoz.io/signoz/pkg/query-service/app/integrations" "go.signoz.io/signoz/pkg/query-service/app/integrations"
@@ -64,26 +61,23 @@ import (
const AppDbEngine = "sqlite" const AppDbEngine = "sqlite"
type ServerOptions struct { type ServerOptions struct {
Config signoz.Config
SigNoz *signoz.SigNoz
PromConfigPath string PromConfigPath string
SkipTopLvlOpsPath string SkipTopLvlOpsPath string
HTTPHostPort string HTTPHostPort string
PrivateHostPort string PrivateHostPort string
// alert specific params // alert specific params
DisableRules bool DisableRules bool
RuleRepoURL string RuleRepoURL string
PreferSpanMetrics bool PreferSpanMetrics bool
MaxIdleConns int MaxIdleConns int
MaxOpenConns int MaxOpenConns int
DialTimeout time.Duration DialTimeout time.Duration
CacheConfigPath string CacheConfigPath string
FluxInterval string FluxInterval string
FluxIntervalForTraceDetail string Cluster string
Cluster string GatewayUrl string
GatewayUrl string UseLogsNewSchema bool
UseLogsNewSchema bool UseTraceNewSchema bool
UseTraceNewSchema bool
} }
// Server runs HTTP api service // Server runs HTTP api service
@@ -114,22 +108,25 @@ func (s Server) HealthCheckStatus() chan healthcheck.Status {
// NewServer creates and initializes Server // NewServer creates and initializes Server
func NewServer(serverOptions *ServerOptions) (*Server, error) { func NewServer(serverOptions *ServerOptions) (*Server, error) {
modelDao, err := dao.InitDao(serverOptions.SigNoz.SQLStore.SQLxDB())
modelDao, err := dao.InitDao("sqlite", baseconst.RELATIONAL_DATASOURCE_PATH)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := baseexplorer.InitWithDSN(serverOptions.SigNoz.SQLStore.SQLxDB()); err != nil { baseexplorer.InitWithDSN(baseconst.RELATIONAL_DATASOURCE_PATH)
if err := preferences.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH); err != nil {
return nil, err return nil, err
} }
if err := preferences.InitDB(serverOptions.SigNoz.SQLStore.SQLxDB()); err != nil { localDB, err := dashboards.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH)
if err != nil {
return nil, err return nil, err
} }
if err := dashboards.InitDB(serverOptions.SigNoz.SQLStore.SQLxDB()); err != nil { localDB.SetMaxOpenConns(10)
return nil, err
}
gatewayProxy, err := gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix) gatewayProxy, err := gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
if err != nil { if err != nil {
@@ -137,7 +134,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
} }
// initiate license manager // initiate license manager
lm, err := licensepkg.StartManager(serverOptions.SigNoz.SQLStore.SQLxDB()) lm, err := licensepkg.StartManager("sqlite", localDB)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -146,17 +143,12 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
modelDao.SetFlagProvider(lm) modelDao.SetFlagProvider(lm)
readerReady := make(chan bool) readerReady := make(chan bool)
fluxIntervalForTraceDetail, err := time.ParseDuration(serverOptions.FluxIntervalForTraceDetail)
if err != nil {
return nil, err
}
var reader interfaces.DataConnector var reader interfaces.DataConnector
storage := os.Getenv("STORAGE") storage := os.Getenv("STORAGE")
if storage == "clickhouse" { if storage == "clickhouse" {
zap.L().Info("Using ClickHouse as datastore ...") zap.L().Info("Using ClickHouse as datastore ...")
qb := db.NewDataConnector( qb := db.NewDataConnector(
serverOptions.SigNoz.SQLStore.SQLxDB(), localDB,
serverOptions.PromConfigPath, serverOptions.PromConfigPath,
lm, lm,
serverOptions.MaxIdleConns, serverOptions.MaxIdleConns,
@@ -165,8 +157,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
serverOptions.Cluster, serverOptions.Cluster,
serverOptions.UseLogsNewSchema, serverOptions.UseLogsNewSchema,
serverOptions.UseTraceNewSchema, serverOptions.UseTraceNewSchema,
fluxIntervalForTraceDetail,
serverOptions.SigNoz.Cache,
) )
go qb.Start(readerReady) go qb.Start(readerReady)
reader = qb reader = qb
@@ -194,7 +184,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
rm, err := makeRulesManager(serverOptions.PromConfigPath, rm, err := makeRulesManager(serverOptions.PromConfigPath,
baseconst.GetAlertManagerApiPrefix(), baseconst.GetAlertManagerApiPrefix(),
serverOptions.RuleRepoURL, serverOptions.RuleRepoURL,
serverOptions.SigNoz.SQLStore.SQLxDB(), localDB,
reader, reader,
c, c,
serverOptions.DisableRules, serverOptions.DisableRules,
@@ -207,29 +197,29 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
return nil, err return nil, err
} }
go func() {
err = migrate.ClickHouseMigrate(reader.GetConn(), serverOptions.Cluster)
if err != nil {
zap.L().Error("error while running clickhouse migrations", zap.Error(err))
}
}()
// initiate opamp // initiate opamp
_, err = opAmpModel.InitDB(serverOptions.SigNoz.SQLStore.SQLxDB()) _, err = opAmpModel.InitDB(localDB)
if err != nil { if err != nil {
return nil, err return nil, err
} }
integrationsController, err := integrations.NewController(serverOptions.SigNoz.SQLStore.SQLxDB()) integrationsController, err := integrations.NewController(localDB)
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"couldn't create integrations controller: %w", err, "couldn't create integrations controller: %w", err,
) )
} }
cloudIntegrationsController, err := cloudintegrations.NewController(serverOptions.SigNoz.SQLStore.SQLxDB())
if err != nil {
return nil, fmt.Errorf(
"couldn't create cloud provider integrations controller: %w", err,
)
}
// ingestion pipelines manager // ingestion pipelines manager
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController( logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
serverOptions.SigNoz.SQLStore.SQLxDB(), integrationsController.GetPipelinesForInstalledIntegrations, localDB, "sqlite", integrationsController.GetPipelinesForInstalledIntegrations,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -237,7 +227,8 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
// initiate agent config handler // initiate agent config handler
agentConfMgr, err := agentConf.Initiate(&agentConf.ManagerOptions{ agentConfMgr, err := agentConf.Initiate(&agentConf.ManagerOptions{
DB: serverOptions.SigNoz.SQLStore.SQLxDB(), DB: localDB,
DBEngine: AppDbEngine,
AgentFeatures: []agentConf.AgentFeature{logParsingPipelineController}, AgentFeatures: []agentConf.AgentFeature{logParsingPipelineController},
}) })
if err != nil { if err != nil {
@@ -245,7 +236,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
} }
// start the usagemanager // start the usagemanager
usageManager, err := usage.New(modelDao, lm.GetRepo(), reader.GetConn()) usageManager, err := usage.New("sqlite", modelDao, lm.GetRepo(), reader.GetConn())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -258,6 +249,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
telemetry.GetInstance().SetSaasOperator(constants.SaasSegmentKey) telemetry.GetInstance().SetSaasOperator(constants.SaasSegmentKey)
fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval) fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -275,7 +267,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
FeatureFlags: lm, FeatureFlags: lm,
LicenseManager: lm, LicenseManager: lm,
IntegrationsController: integrationsController, IntegrationsController: integrationsController,
CloudIntegrationsController: cloudIntegrationsController,
LogsParsingPipelineController: logParsingPipelineController, LogsParsingPipelineController: logParsingPipelineController,
Cache: c, Cache: c,
FluxInterval: fluxInterval, FluxInterval: fluxInterval,
@@ -298,7 +289,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
usageManager: usageManager, usageManager: usageManager,
} }
httpServer, err := s.createPublicServer(apiHandler, serverOptions.SigNoz.Web) httpServer, err := s.createPublicServer(apiHandler)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -324,13 +315,10 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
r := baseapp.NewRouter() r := baseapp.NewRouter()
r.Use(middleware.NewTimeout(zap.L(), r.Use(setTimeoutMiddleware)
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes, r.Use(s.analyticsMiddleware)
s.serverOptions.Config.APIServer.Timeout.Default, r.Use(loggingMiddlewarePrivate)
s.serverOptions.Config.APIServer.Timeout.Max, r.Use(baseapp.LogCommentEnricher)
).Wrap)
r.Use(middleware.NewAnalytics(zap.L()).Wrap)
r.Use(middleware.NewLogging(zap.L()).Wrap)
apiHandler.RegisterPrivateRoutes(r) apiHandler.RegisterPrivateRoutes(r)
@@ -350,7 +338,7 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
}, nil }, nil
} }
func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*http.Server, error) { func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, error) {
r := baseapp.NewRouter() r := baseapp.NewRouter()
@@ -370,18 +358,14 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
} }
am := baseapp.NewAuthMiddleware(getUserFromRequest) am := baseapp.NewAuthMiddleware(getUserFromRequest)
r.Use(middleware.NewTimeout(zap.L(), r.Use(setTimeoutMiddleware)
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes, r.Use(s.analyticsMiddleware)
s.serverOptions.Config.APIServer.Timeout.Default, r.Use(loggingMiddleware)
s.serverOptions.Config.APIServer.Timeout.Max, r.Use(baseapp.LogCommentEnricher)
).Wrap)
r.Use(middleware.NewAnalytics(zap.L()).Wrap)
r.Use(middleware.NewLogging(zap.L()).Wrap)
apiHandler.RegisterRoutes(r, am) apiHandler.RegisterRoutes(r, am)
apiHandler.RegisterLogsRoutes(r, am) apiHandler.RegisterLogsRoutes(r, am)
apiHandler.RegisterIntegrationRoutes(r, am) apiHandler.RegisterIntegrationRoutes(r, am)
apiHandler.RegisterCloudIntegrationsRoutes(r, am)
apiHandler.RegisterQueryRangeV3Routes(r, am) apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterInfraMetricsRoutes(r, am) apiHandler.RegisterInfraMetricsRoutes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am) apiHandler.RegisterQueryRangeV4Routes(r, am)
@@ -398,16 +382,36 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
handler = handlers.CompressHandler(handler) handler = handlers.CompressHandler(handler)
err := web.AddToRouter(r)
if err != nil {
return nil, err
}
return &http.Server{ return &http.Server{
Handler: handler, Handler: handler,
}, nil }, nil
} }
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// loggingMiddleware is used for logging public api calls
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
route := mux.CurrentRoute(r)
path, _ := route.GetPathTemplate()
startTime := time.Now()
next.ServeHTTP(w, r)
zap.L().Info(path, zap.Duration("timeTaken", time.Since(startTime)), zap.String("path", path))
})
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go
// loggingMiddlewarePrivate is used for logging private api calls
// from internal services like alert manager
func loggingMiddlewarePrivate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
route := mux.CurrentRoute(r)
path, _ := route.GetPathTemplate()
startTime := time.Now()
next.ServeHTTP(w, r)
zap.L().Info(path, zap.Duration("timeTaken", time.Since(startTime)), zap.String("path", path), zap.Bool("tprivatePort", true))
})
}
// TODO(remove): Implemented at pkg/http/middleware/logging.go // TODO(remove): Implemented at pkg/http/middleware/logging.go
type loggingResponseWriter struct { type loggingResponseWriter struct {
http.ResponseWriter http.ResponseWriter
@@ -487,29 +491,32 @@ func extractQueryRangeData(path string, r *http.Request) (map[string]interface{}
zap.L().Error("error while matching the trace explorer: ", zap.Error(err)) zap.L().Error("error while matching the trace explorer: ", zap.Error(err))
} }
queryInfoResult := telemetry.GetInstance().CheckQueryInfo(postData) signozMetricsUsed := false
signozLogsUsed := false
signozTracesUsed := false
if postData != nil {
if (queryInfoResult.MetricsUsed || queryInfoResult.LogsUsed || queryInfoResult.TracesUsed) && (queryInfoResult.FilterApplied) { if postData.CompositeQuery != nil {
if queryInfoResult.MetricsUsed { data["queryType"] = postData.CompositeQuery.QueryType
data["panelType"] = postData.CompositeQuery.PanelType
signozLogsUsed, signozMetricsUsed, signozTracesUsed = telemetry.GetInstance().CheckSigNozSignals(postData)
}
}
if signozMetricsUsed || signozLogsUsed || signozTracesUsed {
if signozMetricsUsed {
telemetry.GetInstance().AddActiveMetricsUser() telemetry.GetInstance().AddActiveMetricsUser()
} }
if queryInfoResult.LogsUsed { if signozLogsUsed {
telemetry.GetInstance().AddActiveLogsUser() telemetry.GetInstance().AddActiveLogsUser()
} }
if queryInfoResult.TracesUsed { if signozTracesUsed {
telemetry.GetInstance().AddActiveTracesUser() telemetry.GetInstance().AddActiveTracesUser()
} }
data["metricsUsed"] = queryInfoResult.MetricsUsed data["metricsUsed"] = signozMetricsUsed
data["logsUsed"] = queryInfoResult.LogsUsed data["logsUsed"] = signozLogsUsed
data["tracesUsed"] = queryInfoResult.TracesUsed data["tracesUsed"] = signozTracesUsed
data["filterApplied"] = queryInfoResult.FilterApplied
data["groupByApplied"] = queryInfoResult.GroupByApplied
data["aggregateOperator"] = queryInfoResult.AggregateOperator
data["aggregateAttributeKey"] = queryInfoResult.AggregateAttributeKey
data["numberOfQueries"] = queryInfoResult.NumberOfQueries
data["queryType"] = queryInfoResult.QueryType
data["panelType"] = queryInfoResult.PanelType
userEmail, err := baseauth.GetEmailFromJwt(r.Context()) userEmail, err := baseauth.GetEmailFromJwt(r.Context())
if err == nil { if err == nil {
// switch case to set data["screen"] based on the referrer // switch case to set data["screen"] based on the referrer
@@ -576,6 +583,23 @@ func (s *Server) analyticsMiddleware(next http.Handler) http.Handler {
}) })
} }
// TODO(remove): Implemented at pkg/http/middleware/timeout.go
func setTimeoutMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var cancel context.CancelFunc
// check if route is not excluded
url := r.URL.Path
if _, ok := baseconst.TimeoutExcludedRoutes[url]; !ok {
ctx, cancel = context.WithTimeout(r.Context(), baseconst.ContextTimeout)
defer cancel()
}
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
// initListeners initialises listeners of the server // initListeners initialises listeners of the server
func (s *Server) initListeners() error { func (s *Server) initListeners() error {
// listen on public port // listen on public port

View File

@@ -1,10 +1,18 @@
package dao package dao
import ( import (
"github.com/jmoiron/sqlx" "fmt"
"go.signoz.io/signoz/ee/query-service/dao/sqlite" "go.signoz.io/signoz/ee/query-service/dao/sqlite"
) )
func InitDao(inputDB *sqlx.DB) (ModelDao, error) { func InitDao(engine, path string) (ModelDao, error) {
return sqlite.InitDB(inputDB)
switch engine {
case "sqlite":
return sqlite.InitDB(path)
default:
return nil, fmt.Errorf("qsdb type: %s is not supported in query service", engine)
}
} }

View File

@@ -65,8 +65,8 @@ func columnExists(db *sqlx.DB, tableName, columnName string) bool {
} }
// InitDB creates and extends base model DB repository // InitDB creates and extends base model DB repository
func InitDB(inputDB *sqlx.DB) (*modelDao, error) { func InitDB(dataSourceName string) (*modelDao, error) {
dao, err := basedsql.InitDB(inputDB) dao, err := basedsql.InitDB(dataSourceName)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -28,8 +28,13 @@ func NewLicenseRepo(db *sqlx.DB) Repo {
} }
} }
func (r *Repo) InitDB(inputDB *sqlx.DB) error { func (r *Repo) InitDB(engine string) error {
return sqlite.InitDB(inputDB) switch engine {
case "sqlite3", "sqlite":
return sqlite.InitDB(r.db)
default:
return fmt.Errorf("unsupported db")
}
} }
func (r *Repo) GetLicenses(ctx context.Context) ([]model.License, error) { func (r *Repo) GetLicenses(ctx context.Context) ([]model.License, error) {

View File

@@ -51,13 +51,13 @@ type Manager struct {
activeFeatures basemodel.FeatureSet activeFeatures basemodel.FeatureSet
} }
func StartManager(db *sqlx.DB, features ...basemodel.Feature) (*Manager, error) { func StartManager(dbType string, db *sqlx.DB, features ...basemodel.Feature) (*Manager, error) {
if LM != nil { if LM != nil {
return LM, nil return LM, nil
} }
repo := NewLicenseRepo(db) repo := NewLicenseRepo(db)
err := repo.InitDB(db) err := repo.InitDB(dbType)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to initiate license repo: %v", err) return nil, fmt.Errorf("failed to initiate license repo: %v", err)

View File

@@ -13,14 +13,10 @@ import (
"go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0" semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.signoz.io/signoz/ee/query-service/app" "go.signoz.io/signoz/ee/query-service/app"
"go.signoz.io/signoz/pkg/config"
"go.signoz.io/signoz/pkg/config/envprovider"
"go.signoz.io/signoz/pkg/config/fileprovider"
"go.signoz.io/signoz/pkg/query-service/auth" "go.signoz.io/signoz/pkg/query-service/auth"
baseconst "go.signoz.io/signoz/pkg/query-service/constants" baseconst "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/migrate" "go.signoz.io/signoz/pkg/query-service/migrate"
"go.signoz.io/signoz/pkg/query-service/version" "go.signoz.io/signoz/pkg/query-service/version"
"go.signoz.io/signoz/pkg/signoz"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
@@ -99,7 +95,7 @@ func main() {
var useLogsNewSchema bool var useLogsNewSchema bool
var useTraceNewSchema bool var useTraceNewSchema bool
var cacheConfigPath, fluxInterval, fluxIntervalForTraceDetail string var cacheConfigPath, fluxInterval string
var enableQueryServiceLogOTLPExport bool var enableQueryServiceLogOTLPExport bool
var preferSpanMetrics bool var preferSpanMetrics bool
@@ -121,11 +117,11 @@ func main() {
flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)") 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(&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(&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.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)") flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)")
flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')") flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')")
flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)") flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)")
flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses") flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses")
flag.Parse() flag.Parse()
loggerMgr := initZapLog(enableQueryServiceLogOTLPExport) loggerMgr := initZapLog(enableQueryServiceLogOTLPExport)
@@ -135,42 +131,23 @@ func main() {
version.PrintVersion() version.PrintVersion()
config, err := signoz.NewConfig(context.Background(), config.ResolverConfig{
Uris: []string{"env:"},
ProviderFactories: []config.ProviderFactory{
envprovider.NewFactory(),
fileprovider.NewFactory(),
},
})
if err != nil {
zap.L().Fatal("Failed to create config", zap.Error(err))
}
signoz, err := signoz.New(context.Background(), config, signoz.NewProviderConfig())
if err != nil {
zap.L().Fatal("Failed to create signoz struct", zap.Error(err))
}
serverOptions := &app.ServerOptions{ serverOptions := &app.ServerOptions{
Config: config, HTTPHostPort: baseconst.HTTPHostPort,
SigNoz: signoz, PromConfigPath: promConfigPath,
HTTPHostPort: baseconst.HTTPHostPort, SkipTopLvlOpsPath: skipTopLvlOpsPath,
PromConfigPath: promConfigPath, PreferSpanMetrics: preferSpanMetrics,
SkipTopLvlOpsPath: skipTopLvlOpsPath, PrivateHostPort: baseconst.PrivateHostPort,
PreferSpanMetrics: preferSpanMetrics, DisableRules: disableRules,
PrivateHostPort: baseconst.PrivateHostPort, RuleRepoURL: ruleRepoURL,
DisableRules: disableRules, MaxIdleConns: maxIdleConns,
RuleRepoURL: ruleRepoURL, MaxOpenConns: maxOpenConns,
MaxIdleConns: maxIdleConns, DialTimeout: dialTimeout,
MaxOpenConns: maxOpenConns, CacheConfigPath: cacheConfigPath,
DialTimeout: dialTimeout, FluxInterval: fluxInterval,
CacheConfigPath: cacheConfigPath, Cluster: cluster,
FluxInterval: fluxInterval, GatewayUrl: gatewayUrl,
FluxIntervalForTraceDetail: fluxIntervalForTraceDetail, UseLogsNewSchema: useLogsNewSchema,
Cluster: cluster, UseTraceNewSchema: useTraceNewSchema,
GatewayUrl: gatewayUrl,
UseLogsNewSchema: useLogsNewSchema,
UseTraceNewSchema: useTraceNewSchema,
} }
// Read the jwt secret key // Read the jwt secret key
@@ -182,7 +159,7 @@ func main() {
zap.L().Info("JWT secret key set successfully.") zap.L().Info("JWT secret key set successfully.")
} }
if err := migrate.Migrate(signoz.SQLStore.SQLxDB()); err != nil { if err := migrate.Migrate(baseconst.RELATIONAL_DATASOURCE_PATH); err != nil {
zap.L().Error("Failed to migrate", zap.Error(err)) zap.L().Error("Failed to migrate", zap.Error(err))
} else { } else {
zap.L().Info("Migration successful") zap.L().Info("Migration successful")

View File

@@ -46,7 +46,7 @@ type Manager struct {
tenantID string tenantID string
} }
func New(modelDao dao.ModelDao, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn) (*Manager, error) { func New(dbType string, modelDao dao.ModelDao, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn) (*Manager, error) {
hostNameRegex := regexp.MustCompile(`tcp://(?P<hostname>.*):`) hostNameRegex := regexp.MustCompile(`tcp://(?P<hostname>.*):`)
hostNameRegexMatches := hostNameRegex.FindStringSubmatch(os.Getenv("ClickHouseUrl")) hostNameRegexMatches := hostNameRegex.FindStringSubmatch(os.Getenv("ClickHouseUrl"))

View File

@@ -24,7 +24,7 @@ const config: Config.InitialOptions = {
'^.+\\.(js|jsx)$': 'babel-jest', '^.+\\.(js|jsx)$': 'babel-jest',
}, },
transformIgnorePatterns: [ transformIgnorePatterns: [
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|d3-interpolate|d3-color|api)/)', 'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|d3-interpolate|d3-color)/)',
], ],
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'], setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'], testPathIgnorePatterns: ['/node_modules/', '/public/'],

View File

@@ -21,7 +21,7 @@
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*", "husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
"commitlint": "commitlint --edit $1", "commitlint": "commitlint --edit $1",
"test": "jest --coverage", "test": "jest --coverage",
"test:changedsince": "jest --changedSince=main --coverage --silent" "test:changedsince": "jest --changedSince=develop --coverage --silent"
}, },
"engines": { "engines": {
"node": ">=16.15.0" "node": ">=16.15.0"
@@ -43,7 +43,6 @@
"@sentry/react": "8.41.0", "@sentry/react": "8.41.0",
"@sentry/webpack-plugin": "2.22.6", "@sentry/webpack-plugin": "2.22.6",
"@signozhq/design-tokens": "1.1.4", "@signozhq/design-tokens": "1.1.4",
"@tanstack/react-table": "8.20.6",
"@uiw/react-md-editor": "3.23.5", "@uiw/react-md-editor": "3.23.5",
"@visx/group": "3.3.0", "@visx/group": "3.3.0",
"@visx/shape": "3.5.0", "@visx/shape": "3.5.0",
@@ -154,7 +153,6 @@
"@babel/preset-typescript": "^7.21.4", "@babel/preset-typescript": "^7.21.4",
"@commitlint/cli": "^16.3.0", "@commitlint/cli": "^16.3.0",
"@commitlint/config-conventional": "^16.2.4", "@commitlint/config-conventional": "^16.2.4",
"@faker-js/faker": "9.3.0",
"@jest/globals": "^27.5.1", "@jest/globals": "^27.5.1",
"@playwright/test": "^1.22.0", "@playwright/test": "^1.22.0",
"@testing-library/jest-dom": "5.16.5", "@testing-library/jest-dom": "5.16.5",
@@ -244,8 +242,6 @@
"xml2js": "0.5.0", "xml2js": "0.5.0",
"phin": "^3.7.1", "phin": "^3.7.1",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"http-proxy-middleware": "3.0.3", "http-proxy-middleware": "3.0.3"
"cross-spawn": "7.0.5",
"cookie": "^0.7.1"
} }
} }

View File

@@ -3,10 +3,9 @@
"alert_channels": "Alert Channels", "alert_channels": "Alert Channels",
"organization_settings": "Organization Settings", "organization_settings": "Organization Settings",
"ingestion_settings": "Ingestion Settings", "ingestion_settings": "Ingestion Settings",
"api_keys": "API Keys", "api_keys": "Access Tokens",
"my_settings": "My Settings", "my_settings": "My Settings",
"overview_metrics": "Overview Metrics", "overview_metrics": "Overview Metrics",
"custom_domain_settings": "Custom Domain Settings",
"dbcall_metrics": "Database Calls", "dbcall_metrics": "Database Calls",
"external_metrics": "External Calls", "external_metrics": "External Calls",
"pipeline": "Pipeline", "pipeline": "Pipeline",

View File

@@ -26,8 +26,7 @@
"MY_SETTINGS": "SigNoz | My Settings", "MY_SETTINGS": "SigNoz | My Settings",
"ORG_SETTINGS": "SigNoz | Organization Settings", "ORG_SETTINGS": "SigNoz | Organization Settings",
"INGESTION_SETTINGS": "SigNoz | Ingestion Settings", "INGESTION_SETTINGS": "SigNoz | Ingestion Settings",
"API_KEYS": "SigNoz | API Keys", "API_KEYS": "SigNoz | Access Tokens",
"CUSTOM_DOMAIN_SETTINGS": "SigNoz | Custom Domain Settings",
"SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong", "SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong",
"UN_AUTHORIZED": "SigNoz | Unauthorized", "UN_AUTHORIZED": "SigNoz | Unauthorized",
"NOT_FOUND": "SigNoz | Page Not Found", "NOT_FOUND": "SigNoz | Page Not Found",
@@ -43,6 +42,5 @@
"DEFAULT": "Open source Observability Platform | SigNoz", "DEFAULT": "Open source Observability Platform | SigNoz",
"ALERT_HISTORY": "SigNoz | Alert Rule History", "ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview", "ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring", "INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring"
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring"
} }

View File

@@ -1,3 +1,3 @@
{ {
"delete_confirm_message": "Are you sure you want to delete {{keyName}} key? Deleting a key is irreversible and cannot be undone." "delete_confirm_message": "Are you sure you want to delete {{keyName}} token? Deleting a token is irreversible and cannot be undone."
} }

View File

@@ -3,9 +3,7 @@
"billing": "Billing", "billing": "Billing",
"manage_billing_and_costs": "Manage your billing information, invoices, and monitor costs.", "manage_billing_and_costs": "Manage your billing information, invoices, and monitor costs.",
"enterprise_cloud": "Enterprise Cloud", "enterprise_cloud": "Enterprise Cloud",
"teams_cloud": "Teams Cloud",
"enterprise": "Enterprise", "enterprise": "Enterprise",
"teams": "Teams",
"card_details_recieved_and_billing_info": "We have received your card details, your billing will only start after the end of your free trial period.", "card_details_recieved_and_billing_info": "We have received your card details, your billing will only start after the end of your free trial period.",
"upgrade_plan": "Upgrade Plan", "upgrade_plan": "Upgrade Plan",
"manage_billing": "Manage Billing", "manage_billing": "Manage Billing",

View File

@@ -3,10 +3,9 @@
"alert_channels": "Alert Channels", "alert_channels": "Alert Channels",
"organization_settings": "Organization Settings", "organization_settings": "Organization Settings",
"ingestion_settings": "Ingestion Settings", "ingestion_settings": "Ingestion Settings",
"api_keys": "API Keys", "api_keys": "Access Tokens",
"my_settings": "My Settings", "my_settings": "My Settings",
"overview_metrics": "Overview Metrics", "overview_metrics": "Overview Metrics",
"custom_domain_settings": "Custom Domain Settings",
"dbcall_metrics": "Database Calls", "dbcall_metrics": "Database Calls",
"external_metrics": "External Calls", "external_metrics": "External Calls",
"pipeline": "Pipeline", "pipeline": "Pipeline",

View File

@@ -32,8 +32,7 @@
"MY_SETTINGS": "SigNoz | My Settings", "MY_SETTINGS": "SigNoz | My Settings",
"ORG_SETTINGS": "SigNoz | Organization Settings", "ORG_SETTINGS": "SigNoz | Organization Settings",
"INGESTION_SETTINGS": "SigNoz | Ingestion Settings", "INGESTION_SETTINGS": "SigNoz | Ingestion Settings",
"API_KEYS": "SigNoz | API Keys", "API_KEYS": "SigNoz | Access Tokens",
"CUSTOM_DOMAIN_SETTINGS": "SigNoz | Custom Domain Settings",
"SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong", "SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong",
"UN_AUTHORIZED": "SigNoz | Unauthorized", "UN_AUTHORIZED": "SigNoz | Unauthorized",
"NOT_FOUND": "SigNoz | Page Not Found", "NOT_FOUND": "SigNoz | Page Not Found",
@@ -56,6 +55,5 @@
"ALERT_HISTORY": "SigNoz | Alert Rule History", "ALERT_HISTORY": "SigNoz | Alert Rule History",
"ALERT_OVERVIEW": "SigNoz | Alert Rule Overview", "ALERT_OVERVIEW": "SigNoz | Alert Rule Overview",
"MESSAGING_QUEUES": "SigNoz | Messaging Queues", "MESSAGING_QUEUES": "SigNoz | Messaging Queues",
"INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring", "INFRASTRUCTURE_MONITORING_HOSTS": "SigNoz | Infra Monitoring"
"INFRASTRUCTURE_MONITORING_KUBERNETES": "SigNoz | Infra Monitoring"
} }

View File

@@ -1,17 +1,29 @@
/* eslint-disable react-hooks/exhaustive-deps */
import getLocalStorageApi from 'api/browser/localstorage/get'; import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import getOrgUser from 'api/user/getOrgUser'; import getOrgUser from 'api/user/getOrgUser';
import loginApi from 'api/user/login';
import { Logout } from 'api/utils';
import Spinner from 'components/Spinner';
import { LOCALSTORAGE } from 'constants/localStorage'; import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history'; import history from 'lib/history';
import { isEmpty } from 'lodash-es'; import { isEmpty, isNull } from 'lodash-es';
import { useAppContext } from 'providers/App/App'; import { useAppContext } from 'providers/App/App';
import { ReactChild, useCallback, useEffect, useMemo, useState } from 'react'; import { ReactChild, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { matchPath, useLocation } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux';
import { matchPath, Redirect, useLocation } from 'react-router-dom';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import { getInitialUserTokenRefreshToken } from 'store/utils';
import AppActions from 'types/actions';
import { UPDATE_USER_IS_FETCH } from 'types/actions/app';
import { LicenseState, LicenseStatus } from 'types/api/licensesV3/getActive'; import { LicenseState, LicenseStatus } from 'types/api/licensesV3/getActive';
import { Organization } from 'types/api/user/getOrganization'; import { Organization } from 'types/api/user/getOrganization';
import { USER_ROLES } from 'types/roles'; import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app'; import { isCloudUser } from 'utils/app';
import { routePermission } from 'utils/permission'; import { routePermission } from 'utils/permission';
@@ -19,30 +31,32 @@ import routes, {
LIST_LICENSES, LIST_LICENSES,
oldNewRoutesMapping, oldNewRoutesMapping,
oldRoutes, oldRoutes,
ROUTES_NOT_TO_BE_OVERRIDEN,
SUPPORT_ROUTE,
} from './routes'; } from './routes';
import afterLogin from './utils';
function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const location = useLocation(); const location = useLocation();
const { pathname } = location; const { pathname } = location;
const [isLoading, setIsLoading] = useState<boolean>(true);
const { const {
org, org,
orgPreferences, orgPreferences,
user, user,
role,
isUserFetching,
isUserFetchingError,
isLoggedIn: isLoggedInState, isLoggedIn: isLoggedInState,
isFetchingOrgPreferences, isFetchingOrgPreferences,
licenses, } = useSelector<AppState, AppReducer>((state) => state.app);
isFetchingLicenses,
activeLicenseV3, const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext();
isFetchingActiveLicenseV3,
} = useAppContext();
const isAdmin = user.role === USER_ROLES.ADMIN;
const mapRoutes = useMemo( const mapRoutes = useMemo(
() => () =>
new Map( new Map(
[...routes, LIST_LICENSES, SUPPORT_ROUTE].map((e) => { [...routes, LIST_LICENSES].map((e) => {
const currentPath = matchPath(pathname, { const currentPath = matchPath(pathname, {
path: e.path, path: e.path,
}); });
@@ -51,13 +65,52 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
), ),
[pathname], [pathname],
); );
const isOldRoute = oldRoutes.indexOf(pathname) > -1;
const currentRoute = mapRoutes.get('current'); const isOnboardingComplete = useMemo(
() =>
orgPreferences?.find(
(preference: Record<string, any>) => preference.key === 'ORG_ONBOARDING',
)?.value,
[orgPreferences],
);
const {
data: licensesData,
isFetching: isFetchingLicensesData,
} = useLicense();
const { t } = useTranslation(['common']);
const isCloudUserVal = isCloudUser(); const isCloudUserVal = isCloudUser();
const localStorageUserAuthToken = getInitialUserTokenRefreshToken();
const dispatch = useDispatch<Dispatch<AppActions>>();
const { notifications } = useNotifications();
const currentRoute = mapRoutes.get('current');
const isOldRoute = oldRoutes.indexOf(pathname) > -1;
const [orgData, setOrgData] = useState<Organization | undefined>(undefined); const [orgData, setOrgData] = useState<Organization | undefined>(undefined);
const { data: orgUsers, isFetching: isFetchingOrgUsers } = useQuery({ const isLocalStorageLoggedIn =
getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true';
const navigateToLoginIfNotLoggedIn = (isLoggedIn = isLoggedInState): void => {
dispatch({
type: UPDATE_USER_IS_FETCH,
payload: {
isUserFetching: false,
},
});
if (!isLoggedIn) {
history.push(ROUTES.LOGIN, { from: pathname });
}
};
const { data: orgUsers, isLoading: isLoadingOrgUsers } = useQuery({
queryFn: () => { queryFn: () => {
if (orgData && orgData.id !== undefined) { if (orgData && orgData.id !== undefined) {
return getOrgUser({ return getOrgUser({
@@ -67,10 +120,10 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
return undefined; return undefined;
}, },
queryKey: ['getOrgUser'], queryKey: ['getOrgUser'],
enabled: !isEmpty(orgData) && user.role === 'ADMIN', enabled: !isEmpty(orgData),
}); });
const checkFirstTimeUser = useCallback((): boolean => { const checkFirstTimeUser = (): boolean => {
const users = orgUsers?.payload || []; const users = orgUsers?.payload || [];
const remainingUsers = users.filter( const remainingUsers = users.filter(
@@ -78,91 +131,154 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
); );
return remainingUsers.length === 1; return remainingUsers.length === 1;
}, [orgUsers?.payload]); };
useEffect(() => { // Check if the onboarding should be shown based on the org users and onboarding completion status, wait for org users and preferences to load
const shouldShowOnboarding = (): boolean => {
// Only run this effect if the org users and preferences are loaded
if (!isLoadingOrgUsers && !isFetchingOrgPreferences) {
const isFirstUser = checkFirstTimeUser();
// Redirect to get started if it's not the first user or if the onboarding is complete
return isFirstUser && !isOnboardingComplete;
}
return false;
};
const handleRedirectForOrgOnboarding = (key: string): void => {
if ( if (
isLoggedInState &&
isCloudUserVal && isCloudUserVal &&
!isFetchingOrgPreferences && !isFetchingOrgPreferences &&
orgPreferences && !isLoadingOrgUsers &&
!isFetchingOrgUsers && !isEmpty(orgUsers?.payload) &&
orgUsers && !isNull(orgPreferences)
orgUsers.payload
) { ) {
const isOnboardingComplete = orgPreferences?.find( if (key === 'ONBOARDING' && isOnboardingComplete) {
(preference: Record<string, any>) => preference.key === 'ORG_ONBOARDING', history.push(ROUTES.APPLICATION);
)?.value; }
const isFirstUser = checkFirstTimeUser(); const isFirstTimeUser = checkFirstTimeUser();
if (
isFirstUser && if (isFirstTimeUser && !isOnboardingComplete) {
!isOnboardingComplete &&
// if the current route is allowed to be overriden by org onboarding then only do the same
!ROUTES_NOT_TO_BE_OVERRIDEN.includes(pathname)
) {
history.push(ROUTES.ONBOARDING); history.push(ROUTES.ONBOARDING);
} }
} }
}, [
checkFirstTimeUser, if (!isCloudUserVal && key === 'ONBOARDING') {
isCloudUserVal, history.push(ROUTES.APPLICATION);
isFetchingOrgPreferences, }
isFetchingOrgUsers, };
orgPreferences,
orgUsers, const handleUserLoginIfTokenPresent = async (
pathname, key: keyof typeof ROUTES,
]); ): Promise<void> => {
if (localStorageUserAuthToken?.refreshJwt) {
// localstorage token is present
// renew web access token
const response = await loginApi({
refreshToken: localStorageUserAuthToken?.refreshJwt,
});
if (response.statusCode === 200) {
const route = routePermission[key];
// get all resource and put it over redux
const userResponse = await afterLogin(
response.payload.userId,
response.payload.accessJwt,
response.payload.refreshJwt,
);
handleRedirectForOrgOnboarding(key);
if (
userResponse &&
route &&
route.find((e) => e === userResponse.payload.role) === undefined
) {
history.push(ROUTES.UN_AUTHORIZED);
}
} else {
Logout();
notifications.error({
message: response.error || t('something_went_wrong'),
});
}
}
};
const handlePrivateRoutes = async (
key: keyof typeof ROUTES,
): Promise<void> => {
if (
localStorageUserAuthToken &&
localStorageUserAuthToken.refreshJwt &&
isUserFetching
) {
handleUserLoginIfTokenPresent(key);
} else {
handleRedirectForOrgOnboarding(key);
navigateToLoginIfNotLoggedIn(isLocalStorageLoggedIn);
}
};
const navigateToWorkSpaceBlocked = (route: any): void => { const navigateToWorkSpaceBlocked = (route: any): void => {
const { path } = route; const { path } = route;
const isRouteEnabledForWorkspaceBlockedState = if (path && path !== ROUTES.WORKSPACE_LOCKED) {
isAdmin &&
(path === ROUTES.ORG_SETTINGS ||
path === ROUTES.BILLING ||
path === ROUTES.MY_SETTINGS);
if (
path &&
path !== ROUTES.WORKSPACE_LOCKED &&
!isRouteEnabledForWorkspaceBlockedState
) {
history.push(ROUTES.WORKSPACE_LOCKED); history.push(ROUTES.WORKSPACE_LOCKED);
dispatch({
type: UPDATE_USER_IS_FETCH,
payload: {
isUserFetching: false,
},
});
} }
}; };
useEffect(() => { useEffect(() => {
if (!isFetchingLicenses) { if (!isFetchingLicensesData) {
const currentRoute = mapRoutes.get('current'); const shouldBlockWorkspace = licensesData?.payload?.workSpaceBlock;
const shouldBlockWorkspace = licenses?.workSpaceBlock;
if (shouldBlockWorkspace && currentRoute) { if (shouldBlockWorkspace) {
navigateToWorkSpaceBlocked(currentRoute); navigateToWorkSpaceBlocked(currentRoute);
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [isFetchingLicensesData]);
}, [isFetchingLicenses, licenses?.workSpaceBlock, mapRoutes, pathname]);
const navigateToWorkSpaceSuspended = (route: any): void => { const navigateToWorkSpaceSuspended = (route: any): void => {
const { path } = route; const { path } = route;
if (path && path !== ROUTES.WORKSPACE_SUSPENDED) { if (path && path !== ROUTES.WORKSPACE_SUSPENDED) {
history.push(ROUTES.WORKSPACE_SUSPENDED); history.push(ROUTES.WORKSPACE_SUSPENDED);
dispatch({
type: UPDATE_USER_IS_FETCH,
payload: {
isUserFetching: false,
},
});
} }
}; };
useEffect(() => { useEffect(() => {
if (!isFetchingActiveLicenseV3 && activeLicenseV3) { if (!isFetchingActiveLicenseV3 && activeLicenseV3) {
const currentRoute = mapRoutes.get('current');
const shouldSuspendWorkspace = const shouldSuspendWorkspace =
activeLicenseV3.status === LicenseStatus.SUSPENDED && activeLicenseV3.status === LicenseStatus.SUSPENDED &&
activeLicenseV3.state === LicenseState.PAYMENT_FAILED; activeLicenseV3.state === LicenseState.PAYMENT_FAILED;
if (shouldSuspendWorkspace && currentRoute) { if (shouldSuspendWorkspace) {
navigateToWorkSpaceSuspended(currentRoute); navigateToWorkSpaceSuspended(currentRoute);
} }
} }
}, [isFetchingActiveLicenseV3, activeLicenseV3, mapRoutes, pathname]); }, [isFetchingActiveLicenseV3, activeLicenseV3]);
useEffect(() => { useEffect(() => {
if (org && org.length > 0 && org[0].id !== undefined) { if (org && org.length > 0 && org[0].id !== undefined) {
@@ -170,70 +286,103 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
} }
}, [org]); }, [org]);
const handleRouting = (): void => {
const showOrgOnboarding = shouldShowOnboarding();
if (showOrgOnboarding && !isOnboardingComplete && isCloudUserVal) {
history.push(ROUTES.ONBOARDING);
} else {
history.push(ROUTES.APPLICATION);
}
};
useEffect(() => {
const { isPrivate } = currentRoute || {
isPrivate: false,
};
if (isLoggedInState && role && role !== 'ADMIN') {
setIsLoading(false);
}
if (!isPrivate) {
setIsLoading(false);
}
if (
!isEmpty(user) &&
!isFetchingOrgPreferences &&
!isEmpty(orgUsers?.payload) &&
!isNull(orgPreferences)
) {
setIsLoading(false);
}
}, [currentRoute, user, role, orgUsers, orgPreferences]);
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => { useEffect(() => {
// if it is an old route navigate to the new route (async (): Promise<void> => {
if (isOldRoute) { try {
const redirectUrl = oldNewRoutesMapping[pathname]; if (isOldRoute) {
const redirectUrl = oldNewRoutesMapping[pathname];
const newLocation = { const newLocation = {
...location, ...location,
pathname: redirectUrl, pathname: redirectUrl,
}; };
history.replace(newLocation); history.replace(newLocation);
return; }
}
// if the current route if (currentRoute) {
if (currentRoute) { const { isPrivate, key } = currentRoute;
const { isPrivate, key } = currentRoute;
if (isPrivate) { if (isPrivate && key !== String(ROUTES.WORKSPACE_LOCKED)) {
if (isLoggedInState) { handlePrivateRoutes(key);
const route = routePermission[key]; } else {
if (route && route.find((e) => e === user.role) === undefined) { // no need to fetch the user and make user fetching false
history.push(ROUTES.UN_AUTHORIZED); if (getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true') {
handleRouting();
}
dispatch({
type: UPDATE_USER_IS_FETCH,
payload: {
isUserFetching: false,
},
});
}
} else if (pathname === ROUTES.HOME_PAGE) {
// routing to application page over root page
if (isLoggedInState) {
handleRouting();
} else {
navigateToLoginIfNotLoggedIn();
} }
} else { } else {
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, pathname); // not found
history.push(ROUTES.LOGIN); navigateToLoginIfNotLoggedIn(isLocalStorageLoggedIn);
} }
} else if (isLoggedInState) { } catch (error) {
const fromPathname = getLocalStorageApi( // something went wrong
LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, history.push(ROUTES.SOMETHING_WENT_WRONG);
);
if (fromPathname) {
history.push(fromPathname);
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, '');
} else if (pathname !== ROUTES.SOMETHING_WENT_WRONG) {
history.push(ROUTES.APPLICATION);
}
} else {
// do nothing as the unauthenticated routes are LOGIN and SIGNUP and the LOGIN container takes care of routing to signup if
// setup is not completed
} }
} else if (isLoggedInState) { })();
const fromPathname = getLocalStorageApi(
LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT,
);
if (fromPathname) {
history.push(fromPathname);
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, '');
} else {
history.push(ROUTES.APPLICATION);
}
} else {
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, pathname);
history.push(ROUTES.LOGIN);
}
}, [ }, [
licenses, dispatch,
isLoggedInState, isLoggedInState,
pathname,
user,
isOldRoute,
currentRoute, currentRoute,
location, licensesData,
orgUsers,
orgPreferences,
]); ]);
if (isUserFetchingError) {
return <Redirect to={ROUTES.SOMETHING_WENT_WRONG} />;
}
if (isUserFetching || isLoading) {
return <Spinner tip="Loading..." />;
}
// NOTE: disabling this rule as there is no need to have div // NOTE: disabling this rule as there is no need to have div
// eslint-disable-next-line react/jsx-no-useless-fragment // eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>; return <>{children}</>;

View File

@@ -1,6 +1,8 @@
import { ConfigProvider } from 'antd'; import { ConfigProvider } from 'antd';
import getLocalStorageApi from 'api/browser/localstorage/get'; import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set'; import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import getAllOrgPreferences from 'api/preferences/getAllOrgPreferences';
import NotFound from 'components/NotFound'; import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
@@ -9,21 +11,35 @@ import ROUTES from 'constants/routes';
import AppLayout from 'container/AppLayout'; import AppLayout from 'container/AppLayout';
import useAnalytics from 'hooks/analytics/useAnalytics'; import useAnalytics from 'hooks/analytics/useAnalytics';
import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys'; import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useThemeConfig } from 'hooks/useDarkMode'; import { useIsDarkMode, useThemeConfig } from 'hooks/useDarkMode';
import { LICENSE_PLAN_KEY } from 'hooks/useLicense'; import { THEME_MODE } from 'hooks/useDarkMode/constant';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useGetFeatureFlag from 'hooks/useGetFeatureFlag';
import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense';
import { NotificationProvider } from 'hooks/useNotifications'; import { NotificationProvider } from 'hooks/useNotifications';
import { ResourceProvider } from 'hooks/useResourceAttribute'; import { ResourceProvider } from 'hooks/useResourceAttribute';
import history from 'lib/history'; import history from 'lib/history';
import { identity, pickBy } from 'lodash-es'; import { identity, pick, pickBy } from 'lodash-es';
import posthog from 'posthog-js'; import posthog from 'posthog-js';
import AlertRuleProvider from 'providers/Alert'; import AlertRuleProvider from 'providers/Alert';
import { useAppContext } from 'providers/App/App'; import { AppProvider } from 'providers/App/App';
import { IUser } from 'providers/App/types';
import { DashboardProvider } from 'providers/Dashboard/Dashboard'; import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { QueryBuilderProvider } from 'providers/QueryBuilder'; import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { Suspense, useCallback, useEffect, useState } from 'react'; import { Suspense, useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import { useDispatch, useSelector } from 'react-redux';
import { Route, Router, Switch } from 'react-router-dom'; import { Route, Router, Switch } from 'react-router-dom';
import { CompatRouter } from 'react-router-dom-v5-compat'; import { CompatRouter } from 'react-router-dom-v5-compat';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
UPDATE_FEATURE_FLAG_RESPONSE,
UPDATE_IS_FETCHING_ORG_PREFERENCES,
UPDATE_ORG_PREFERENCES,
} from 'types/actions/app';
import AppReducer, { User } from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { extractDomain, isCloudUser, isEECloudUser } from 'utils/app'; import { extractDomain, isCloudUser, isEECloudUser } from 'utils/app';
import PrivateRoute from './Private'; import PrivateRoute from './Private';
@@ -35,20 +51,14 @@ import defaultRoutes, {
function App(): JSX.Element { function App(): JSX.Element {
const themeConfig = useThemeConfig(); const themeConfig = useThemeConfig();
const { const { data: licenseData } = useLicense();
licenses,
user,
isFetchingUser,
isFetchingLicenses,
isFetchingFeatureFlags,
userFetchError,
licensesFetchError,
featureFlagsFetchError,
isLoggedIn: isLoggedInState,
featureFlags,
org,
} = useAppContext();
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes); const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
const { role, isLoggedIn: isLoggedInState, user, org } = useSelector<
AppState,
AppReducer
>((state) => state.app);
const dispatch = useDispatch<Dispatch<AppActions>>();
const { trackPageView } = useAnalytics(); const { trackPageView } = useAnalytics();
@@ -56,114 +66,164 @@ function App(): JSX.Element {
const isCloudUserVal = isCloudUser(); const isCloudUserVal = isCloudUser();
const enableAnalytics = useCallback( const isDarkMode = useIsDarkMode();
(user: IUser): void => {
// wait for the required data to be loaded before doing init for anything!
if (!isFetchingLicenses && licenses && org) {
const orgName =
org && Array.isArray(org) && org.length > 0 ? org[0].name : '';
const { name, email, role } = user; const isChatSupportEnabled =
useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active || false;
const identifyPayload = { const isPremiumSupportEnabled =
email, useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
name,
company_name: orgName,
role,
source: 'signoz-ui',
};
const sanitizedIdentifyPayload = pickBy(identifyPayload, identity); const { data: orgPreferences, isLoading: isLoadingOrgPreferences } = useQuery({
const domain = extractDomain(email); queryFn: () => getAllOrgPreferences(),
const hostNameParts = hostname.split('.'); queryKey: ['getOrgPreferences'],
enabled: isLoggedInState && role === USER_ROLES.ADMIN,
});
const groupTraits = {
name: orgName,
tenant_id: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
company_domain: domain,
source: 'signoz-ui',
};
window.analytics.identify(email, sanitizedIdentifyPayload);
window.analytics.group(domain, groupTraits);
posthog?.identify(email, {
email,
name,
orgName,
tenant_id: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
company_domain: domain,
source: 'signoz-ui',
isPaidUser: !!licenses?.trialConvertedToSubscription,
});
posthog?.group('company', domain, {
name: orgName,
tenant_id: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
company_domain: domain,
source: 'signoz-ui',
isPaidUser: !!licenses?.trialConvertedToSubscription,
});
}
},
[hostname, isFetchingLicenses, licenses, org],
);
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => { useEffect(() => {
if ( if (orgPreferences && !isLoadingOrgPreferences) {
!isFetchingLicenses && dispatch({
licenses && type: UPDATE_IS_FETCHING_ORG_PREFERENCES,
!isFetchingUser && payload: {
user && isFetchingOrgPreferences: false,
!!user.email },
) { });
const isOnBasicPlan =
licenses.licenses?.some(
(license) =>
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
) || licenses.licenses === null;
const isIdentifiedUser = getLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER); dispatch({
type: UPDATE_ORG_PREFERENCES,
if (isLoggedInState && user && user.id && user.email && !isIdentifiedUser) { payload: {
setLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER, 'true'); orgPreferences: orgPreferences.payload?.data || null,
} },
});
let updatedRoutes = defaultRoutes;
// if the user is a cloud user
if (isCloudUserVal || isEECloudUser()) {
// if the user is on basic plan then remove billing
if (isOnBasicPlan) {
updatedRoutes = updatedRoutes.filter(
(route) => route?.path !== ROUTES.BILLING,
);
}
// always add support route for cloud users
updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE];
} else {
// if not a cloud user then remove billing and add list licenses route
updatedRoutes = updatedRoutes.filter(
(route) => route?.path !== ROUTES.BILLING,
);
updatedRoutes = [...updatedRoutes, LIST_LICENSES];
}
setRoutes(updatedRoutes);
} }
}, [ }, [orgPreferences, dispatch, isLoadingOrgPreferences]);
isLoggedInState,
user, useEffect(() => {
licenses, if (isLoggedInState && role !== USER_ROLES.ADMIN) {
isCloudUserVal, dispatch({
isFetchingLicenses, type: UPDATE_IS_FETCHING_ORG_PREFERENCES,
isFetchingUser, payload: {
]); isFetchingOrgPreferences: false,
},
});
}
}, [isLoggedInState, role, dispatch]);
const featureResponse = useGetFeatureFlag((allFlags) => {
dispatch({
type: UPDATE_FEATURE_FLAG_RESPONSE,
payload: {
featureFlag: allFlags,
refetch: featureResponse.refetch,
},
});
const isOnboardingEnabled =
allFlags.find((flag) => flag.name === FeatureKeys.ONBOARDING)?.active ||
false;
if (!isOnboardingEnabled || !isCloudUserVal) {
const newRoutes = routes.filter(
(route) => route?.path !== ROUTES.GET_STARTED,
);
setRoutes(newRoutes);
}
});
const isOnBasicPlan =
licenseData?.payload?.licenses?.some(
(license) =>
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
) || licenseData?.payload?.licenses === null;
const enableAnalytics = (user: User): void => {
const orgName =
org && Array.isArray(org) && org.length > 0 ? org[0].name : '';
const { name, email } = user;
const identifyPayload = {
email,
name,
company_name: orgName,
role,
source: 'signoz-ui',
};
const sanitizedIdentifyPayload = pickBy(identifyPayload, identity);
const domain = extractDomain(email);
const hostNameParts = hostname.split('.');
const groupTraits = {
name: orgName,
tenant_id: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
company_domain: domain,
source: 'signoz-ui',
};
window.analytics.identify(email, sanitizedIdentifyPayload);
window.analytics.group(domain, groupTraits);
posthog?.identify(email, {
email,
name,
orgName,
tenant_id: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
company_domain: domain,
source: 'signoz-ui',
isPaidUser: !!licenseData?.payload?.trialConvertedToSubscription,
});
posthog?.group('company', domain, {
name: orgName,
tenant_id: hostNameParts[0],
data_region: hostNameParts[1],
tenant_url: hostname,
company_domain: domain,
source: 'signoz-ui',
isPaidUser: !!licenseData?.payload?.trialConvertedToSubscription,
});
};
useEffect(() => {
const isIdentifiedUser = getLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER);
if (
isLoggedInState &&
user &&
user.userId &&
user.email &&
!isIdentifiedUser
) {
setLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER, 'true');
}
if (
isOnBasicPlan ||
(isLoggedInState && role && role !== 'ADMIN') ||
!(isCloudUserVal || isEECloudUser())
) {
const newRoutes = routes.filter((route) => route?.path !== ROUTES.BILLING);
setRoutes(newRoutes);
}
if (isCloudUserVal || isEECloudUser()) {
const newRoutes = [...routes, SUPPORT_ROUTE];
setRoutes(newRoutes);
} else {
const newRoutes = [...routes, LIST_LICENSES];
setRoutes(newRoutes);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoggedInState, isOnBasicPlan, user]);
useEffect(() => { useEffect(() => {
if (pathname === ROUTES.ONBOARDING) { if (pathname === ROUTES.ONBOARDING) {
@@ -177,123 +237,99 @@ function App(): JSX.Element {
} }
trackPageView(pathname); trackPageView(pathname);
}, [pathname, trackPageView]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname]);
useEffect(() => { useEffect(() => {
// feature flag shouldn't be loading and featureFlags or fetchError any one of this should be true indicating that req is complete const showAddCreditCardModal =
// licenses should also be present. there is no check for licenses for loading and error as that is mandatory if not present then routing !isPremiumSupportEnabled &&
// to something went wrong which would ideally need a reload. !licenseData?.payload?.trialConvertedToSubscription;
if (
!isFetchingFeatureFlags &&
(featureFlags || featureFlagsFetchError) &&
licenses
) {
let isChatSupportEnabled = false;
let isPremiumSupportEnabled = false;
if (featureFlags && featureFlags.length > 0) {
isChatSupportEnabled =
featureFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)
?.active || false;
isPremiumSupportEnabled = if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) {
featureFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT) window.Intercom('boot', {
?.active || false; app_id: process.env.INTERCOM_APP_ID,
} email: user?.email || '',
const showAddCreditCardModal = name: user?.name || '',
!isPremiumSupportEnabled && !licenses.trialConvertedToSubscription; });
if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) {
window.Intercom('boot', {
app_id: process.env.INTERCOM_APP_ID,
email: user?.email || '',
name: user?.name || '',
});
}
} }
}, [ }, [
isLoggedInState, isLoggedInState,
isChatSupportEnabled,
user, user,
licenseData,
isPremiumSupportEnabled,
pathname, pathname,
licenses?.trialConvertedToSubscription,
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
licenses,
]); ]);
useEffect(() => { useEffect(() => {
if (!isFetchingUser && isCloudUserVal && user && user.email) { if (user && user?.email && user?.userId && user?.name) {
try {
const isThemeAnalyticsSent = getLocalStorageApi(
LOCALSTORAGE.THEME_ANALYTICS_V1,
);
if (!isThemeAnalyticsSent) {
logEvent('Theme Analytics', {
theme: isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT,
user: pick(user, ['email', 'userId', 'name']),
org,
});
setLocalStorageApi(LOCALSTORAGE.THEME_ANALYTICS_V1, 'true');
}
} catch {
console.error('Failed to parse local storage theme analytics event');
}
}
if (isCloudUserVal && user && user.email) {
enableAnalytics(user); enableAnalytics(user);
} }
}, [user, isFetchingUser, isCloudUserVal, enableAnalytics]);
// if the user is in logged in state // eslint-disable-next-line react-hooks/exhaustive-deps
if (isLoggedInState) { }, [user]);
if (pathname === ROUTES.HOME_PAGE) {
history.replace(ROUTES.APPLICATION);
}
// if the setup calls are loading then return a spinner
if (isFetchingLicenses || isFetchingUser || isFetchingFeatureFlags) {
return <Spinner tip="Loading..." />;
}
// if the required calls fails then return a something went wrong error useEffect(() => {
// this needs to be on top of data missing error because if there is an error, data will never be loaded and it will console.info('We are hiring! https://jobs.gem.com/signoz');
// move to indefinitive loading }, []);
if (
(userFetchError || licensesFetchError) &&
pathname !== ROUTES.SOMETHING_WENT_WRONG
) {
history.replace(ROUTES.SOMETHING_WENT_WRONG);
}
// if all of the data is not set then return a spinner, this is required because there is some gap between loading states and data setting
if (
(!licenses || !user.email || !featureFlags) &&
!userFetchError &&
!licensesFetchError
) {
return <Spinner tip="Loading..." />;
}
}
return ( return (
<ConfigProvider theme={themeConfig}> <AppProvider>
<Router history={history}> <ConfigProvider theme={themeConfig}>
<CompatRouter> <Router history={history}>
<NotificationProvider> <CompatRouter>
<PrivateRoute> <NotificationProvider>
<ResourceProvider> <PrivateRoute>
<QueryBuilderProvider> <ResourceProvider>
<DashboardProvider> <QueryBuilderProvider>
<KeyboardHotkeysProvider> <DashboardProvider>
<AlertRuleProvider> <KeyboardHotkeysProvider>
<AppLayout> <AlertRuleProvider>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}> <AppLayout>
<Switch> <Suspense fallback={<Spinner size="large" tip="Loading..." />}>
{routes.map(({ path, component, exact }) => ( <Switch>
<Route {routes.map(({ path, component, exact }) => (
key={`${path}`} <Route
exact={exact} key={`${path}`}
path={path} exact={exact}
component={component} path={path}
/> component={component}
))} />
))}
<Route path="*" component={NotFound} /> <Route path="*" component={NotFound} />
</Switch> </Switch>
</Suspense> </Suspense>
</AppLayout> </AppLayout>
</AlertRuleProvider> </AlertRuleProvider>
</KeyboardHotkeysProvider> </KeyboardHotkeysProvider>
</DashboardProvider> </DashboardProvider>
</QueryBuilderProvider> </QueryBuilderProvider>
</ResourceProvider> </ResourceProvider>
</PrivateRoute> </PrivateRoute>
</NotificationProvider> </NotificationProvider>
</CompatRouter> </CompatRouter>
</Router> </Router>
</ConfigProvider> </ConfigProvider>
</AppProvider>
); );
} }

View File

@@ -145,11 +145,6 @@ export const MySettings = Loadable(
() => import(/* webpackChunkName: "All MySettings" */ 'pages/MySettings'), () => import(/* webpackChunkName: "All MySettings" */ 'pages/MySettings'),
); );
export const CustomDomainSettings = Loadable(
() =>
import(/* webpackChunkName: "Custom Domain Settings" */ 'pages/Settings'),
);
export const Logs = Loadable( export const Logs = Loadable(
() => import(/* webpackChunkName: "Logs" */ 'pages/LogsModulePage'), () => import(/* webpackChunkName: "Logs" */ 'pages/LogsModulePage'),
); );
@@ -185,7 +180,7 @@ export const PasswordReset = Loadable(
export const SomethingWentWrong = Loadable( export const SomethingWentWrong = Loadable(
() => () =>
import( import(
/* webpackChunkName: "ErrorBoundaryFallback" */ 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback' /* webpackChunkName: "SomethingWentWrong" */ 'pages/SomethingWentWrong'
), ),
); );

View File

@@ -10,7 +10,6 @@ import {
BillingPage, BillingPage,
CreateAlertChannelAlerts, CreateAlertChannelAlerts,
CreateNewAlerts, CreateNewAlerts,
CustomDomainSettings,
DashboardPage, DashboardPage,
DashboardWidget, DashboardWidget,
EditAlertChannelsAlerts, EditAlertChannelsAlerts,
@@ -289,13 +288,6 @@ const routes: AppRoutes[] = [
isPrivate: true, isPrivate: true,
key: 'MY_SETTINGS', key: 'MY_SETTINGS',
}, },
{
path: ROUTES.CUSTOM_DOMAIN_SETTINGS,
exact: true,
component: CustomDomainSettings,
isPrivate: true,
key: 'CUSTOM_DOMAIN_SETTINGS',
},
{ {
path: ROUTES.LOGS, path: ROUTES.LOGS,
exact: true, exact: true,
@@ -415,13 +407,6 @@ const routes: AppRoutes[] = [
key: 'INFRASTRUCTURE_MONITORING_HOSTS', key: 'INFRASTRUCTURE_MONITORING_HOSTS',
isPrivate: true, isPrivate: true,
}, },
{
path: ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,
exact: true,
component: InfrastructureMonitoring,
key: 'INFRASTRUCTURE_MONITORING_KUBERNETES',
isPrivate: true,
},
]; ];
export const SUPPORT_ROUTE: AppRoutes = { export const SUPPORT_ROUTE: AppRoutes = {
@@ -442,27 +427,24 @@ export const LIST_LICENSES: AppRoutes = {
export const oldRoutes = [ export const oldRoutes = [
'/pipelines', '/pipelines',
'/logs/old-logs-explorer',
'/logs-explorer', '/logs-explorer',
'/logs-explorer/live', '/logs-explorer/live',
'/logs-save-views', '/logs-save-views',
'/traces-save-views', '/traces-save-views',
'/settings/access-tokens', '/settings/api-keys',
]; ];
export const oldNewRoutesMapping: Record<string, string> = { export const oldNewRoutesMapping: Record<string, string> = {
'/pipelines': '/logs/pipelines', '/pipelines': '/logs/pipelines',
'/logs/old-logs-explorer': '/logs/old-logs-explorer',
'/logs-explorer': '/logs/logs-explorer', '/logs-explorer': '/logs/logs-explorer',
'/logs-explorer/live': '/logs/logs-explorer/live', '/logs-explorer/live': '/logs/logs-explorer/live',
'/logs-save-views': '/logs/saved-views', '/logs-save-views': '/logs/saved-views',
'/traces-save-views': '/traces/saved-views', '/traces-save-views': '/traces/saved-views',
'/settings/access-tokens': '/settings/api-keys', '/settings/api-keys': '/settings/access-tokens',
}; };
export const ROUTES_NOT_TO_BE_OVERRIDEN: string[] = [
ROUTES.WORKSPACE_LOCKED,
ROUTES.WORKSPACE_SUSPENDED,
];
export interface AppRoutes { export interface AppRoutes {
component: RouteProps['component']; component: RouteProps['component'];
path: RouteProps['path']; path: RouteProps['path'];

View File

@@ -1,28 +1,92 @@
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set'; import setLocalStorageApi from 'api/browser/localstorage/set';
import getUserApi from 'api/user/getUser';
import { Logout } from 'api/utils';
import { LOCALSTORAGE } from 'constants/localStorage'; import { LOCALSTORAGE } from 'constants/localStorage';
import store from 'store';
import AppActions from 'types/actions';
import {
LOGGED_IN,
UPDATE_USER,
UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN,
UPDATE_USER_IS_FETCH,
} from 'types/actions/app';
import { SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/user/getUser';
const afterLogin = ( const afterLogin = async (
userId: string, userId: string,
authToken: string, authToken: string,
refreshToken: string, refreshToken: string,
interceptorRejected?: boolean, ): Promise<SuccessResponse<PayloadProps> | undefined> => {
): void => {
setLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN, authToken); setLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN, authToken);
setLocalStorageApi(LOCALSTORAGE.REFRESH_AUTH_TOKEN, refreshToken); setLocalStorageApi(LOCALSTORAGE.REFRESH_AUTH_TOKEN, refreshToken);
setLocalStorageApi(LOCALSTORAGE.USER_ID, userId);
setLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN, 'true');
if (!interceptorRejected) { store.dispatch<AppActions>({
window.dispatchEvent( type: UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN,
new CustomEvent('AFTER_LOGIN', { payload: {
detail: { accessJwt: authToken,
accessJWT: authToken, refreshJwt: refreshToken,
refreshJWT: refreshToken, },
id: userId, });
},
}), const [getUserResponse] = await Promise.all([
); getUserApi({
userId,
token: authToken,
}),
]);
if (getUserResponse.statusCode === 200 && getUserResponse.payload) {
store.dispatch<AppActions>({
type: LOGGED_IN,
payload: {
isLoggedIn: true,
},
});
const { payload } = getUserResponse;
store.dispatch<AppActions>({
type: UPDATE_USER,
payload: {
ROLE: payload.role,
email: payload.email,
name: payload.name,
orgName: payload.organization,
profilePictureURL: payload.profilePictureURL,
userId: payload.id,
orgId: payload.orgId,
userFlags: payload.flags,
},
});
const isLoggedInLocalStorage = getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN);
if (isLoggedInLocalStorage === null) {
setLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN, 'true');
}
store.dispatch({
type: UPDATE_USER_IS_FETCH,
payload: {
isUserFetching: false,
},
});
return getUserResponse;
} }
store.dispatch({
type: UPDATE_USER_IS_FETCH,
payload: {
isUserFetching: false,
},
});
Logout();
return undefined;
}; };
export default afterLogin; export default afterLogin;

View File

@@ -1,7 +0,0 @@
import { GatewayApiV2Instance as axios } from 'api';
import { AxiosResponse } from 'axios';
import { DeploymentsDataProps } from 'types/api/customDomain/types';
export const getDeploymentsData = (): Promise<
AxiosResponse<DeploymentsDataProps>
> => axios.get(`/deployments/me`);

View File

@@ -1,16 +0,0 @@
import { GatewayApiV2Instance as axios } from 'api';
import { AxiosError } from 'axios';
import { SuccessResponse } from 'types/api';
import {
PayloadProps,
UpdateCustomDomainProps,
} from 'types/api/customDomain/types';
const updateSubDomainAPI = async (
props: UpdateCustomDomainProps,
): Promise<SuccessResponse<PayloadProps> | AxiosError> =>
axios.put(`/deployments/me/host`, {
...props.data,
});
export default updateSubDomainAPI;

View File

@@ -7,6 +7,7 @@ import afterLogin from 'AppRoutes/utils';
import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios'; import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { ENVIRONMENT } from 'constants/env'; import { ENVIRONMENT } from 'constants/env';
import { LOCALSTORAGE } from 'constants/localStorage'; import { LOCALSTORAGE } from 'constants/localStorage';
import store from 'store';
import apiV1, { import apiV1, {
apiAlertManager, apiAlertManager,
@@ -25,7 +26,10 @@ const interceptorsResponse = (
const interceptorsRequestResponse = ( const interceptorsRequestResponse = (
value: InternalAxiosRequestConfig, value: InternalAxiosRequestConfig,
): InternalAxiosRequestConfig => { ): InternalAxiosRequestConfig => {
const token = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) || ''; const token =
store.getState().app.user?.accessJwt ||
getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) ||
'';
if (value && value.headers) { if (value && value.headers) {
value.headers.Authorization = token ? `Bearer ${token}` : ''; value.headers.Authorization = token ? `Bearer ${token}` : '';
@@ -43,36 +47,41 @@ const interceptorRejected = async (
// reject the refresh token error // reject the refresh token error
if (response.status === 401 && response.config.url !== '/login') { if (response.status === 401 && response.config.url !== '/login') {
const response = await loginApi({ const response = await loginApi({
refreshToken: getLocalStorageApi(LOCALSTORAGE.REFRESH_AUTH_TOKEN) || '', refreshToken: store.getState().app.user?.refreshJwt,
}); });
if (response.statusCode === 200) { if (response.statusCode === 200) {
afterLogin( const user = await afterLogin(
response.payload.userId, response.payload.userId,
response.payload.accessJwt, response.payload.accessJwt,
response.payload.refreshJwt, response.payload.refreshJwt,
true,
); );
const reResponse = await axios( if (user) {
`${value.config.baseURL}${value.config.url?.substring(1)}`, const reResponse = await axios(
{ `${value.config.baseURL}${value.config.url?.substring(1)}`,
method: value.config.method, {
headers: { method: value.config.method,
...value.config.headers, headers: {
Authorization: `Bearer ${response.payload.accessJwt}`, ...value.config.headers,
Authorization: `Bearer ${response.payload.accessJwt}`,
},
data: {
...JSON.parse(value.config.data || '{}'),
},
}, },
data: { );
...JSON.parse(value.config.data || '{}'),
},
},
);
if (reResponse.status === 200) { if (reResponse.status === 200) {
return await Promise.resolve(reResponse); return await Promise.resolve(reResponse);
}
Logout();
return await Promise.reject(reResponse);
} }
Logout(); Logout();
return await Promise.reject(reResponse);
return await Promise.reject(value);
} }
Logout(); Logout();
} }

View File

@@ -2,7 +2,6 @@ import { ApiBaseInstance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError, AxiosResponse } from 'axios'; import { AxiosError, AxiosResponse } from 'axios';
import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder'; import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder';
import { K8sCategory } from 'container/InfraMonitoringK8s/constants';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields'; import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
import { import {
@@ -12,18 +11,12 @@ import {
export const getHostAttributeKeys = async ( export const getHostAttributeKeys = async (
searchText = '', searchText = '',
entity: K8sCategory,
): Promise<SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse> => { ): Promise<SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse> => {
try { try {
const response: AxiosResponse<{ const response: AxiosResponse<{
data: IQueryAutocompleteResponse; data: IQueryAutocompleteResponse;
}> = await ApiBaseInstance.get( }> = await ApiBaseInstance.get(
`/${entity}/attribute_keys?dataSource=metrics&searchText=${searchText}`, `/hosts/attribute_keys?dataSource=metrics&searchText=${searchText}`,
{
params: {
limit: 500,
},
},
); );
const payload: BaseAutocompleteData[] = const payload: BaseAutocompleteData[] =

View File

@@ -1,4 +1,4 @@
import axios from 'api'; import { ApiBaseInstance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
@@ -59,7 +59,7 @@ export const getHostLists = async (
headers?: Record<string, string>, headers?: Record<string, string>,
): Promise<SuccessResponse<HostListResponse> | ErrorResponse> => { ): Promise<SuccessResponse<HostListResponse> | ErrorResponse> => {
try { try {
const response = await axios.post('/hosts/list', props, { const response = await ApiBaseInstance.post('/hosts/list', props, {
signal, signal,
headers, headers,
}); });

View File

@@ -1,4 +1,4 @@
import axios from 'api'; import { ApiBaseInstance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import createQueryParams from 'lib/createQueryParams'; import createQueryParams from 'lib/createQueryParams';
@@ -14,17 +14,15 @@ export const getInfraAttributesValues = async ({
filterAttributeKeyDataType, filterAttributeKeyDataType,
tagType, tagType,
searchText, searchText,
aggregateAttribute,
}: IGetAttributeValuesPayload): Promise< }: IGetAttributeValuesPayload): Promise<
SuccessResponse<IAttributeValuesResponse> | ErrorResponse SuccessResponse<IAttributeValuesResponse> | ErrorResponse
> => { > => {
try { try {
const response = await axios.get( const response = await ApiBaseInstance.get(
`/hosts/attribute_values?${createQueryParams({ `/hosts/attribute_values?${createQueryParams({
dataSource, dataSource,
attributeKey, attributeKey,
searchText, searchText,
aggregateAttribute,
})}&filterAttributeKeyDataType=${filterAttributeKeyDataType}&tagType=${tagType}`, })}&filterAttributeKeyDataType=${filterAttributeKeyDataType}&tagType=${tagType}`,
); );

View File

@@ -1,64 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface K8sClustersListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface K8sClustersData {
clusterUID: string;
cpuUsage: number;
cpuAllocatable: number;
memoryUsage: number;
memoryAllocatable: number;
meta: {
k8s_cluster_name: string;
k8s_cluster_uid: string;
};
}
export interface K8sClustersListResponse {
status: string;
data: {
type: string;
records: K8sClustersData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
export const getK8sClustersList = async (
props: K8sClustersListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<K8sClustersListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/clusters/list', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,70 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface K8sDeploymentsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface K8sDeploymentsData {
deploymentName: string;
cpuUsage: number;
memoryUsage: number;
desiredPods: number;
availablePods: number;
cpuRequest: number;
memoryRequest: number;
cpuLimit: number;
memoryLimit: number;
restarts: number;
meta: {
k8s_cluster_name: string;
k8s_deployment_name: string;
k8s_namespace_name: string;
};
}
export interface K8sDeploymentsListResponse {
status: string;
data: {
type: string;
records: K8sDeploymentsData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
export const getK8sDeploymentsList = async (
props: K8sDeploymentsListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<K8sDeploymentsListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/deployments/list', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,62 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface K8sNamespacesListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface K8sNamespacesData {
namespaceName: string;
cpuUsage: number;
memoryUsage: number;
meta: {
k8s_cluster_name: string;
k8s_namespace_name: string;
};
}
export interface K8sNamespacesListResponse {
status: string;
data: {
type: string;
records: K8sNamespacesData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
export const getK8sNamespacesList = async (
props: K8sNamespacesListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<K8sNamespacesListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/namespaces/list', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,65 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface K8sNodesListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface K8sNodesData {
nodeUID: string;
nodeCPUUsage: number;
nodeCPUAllocatable: number;
nodeMemoryUsage: number;
nodeMemoryAllocatable: number;
meta: {
k8s_node_name: string;
k8s_node_uid: string;
k8s_cluster_name: string;
};
}
export interface K8sNodesListResponse {
status: string;
data: {
type: string;
records: K8sNodesData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
export const getK8sNodesList = async (
props: K8sNodesListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<K8sNodesListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/nodes/list', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,93 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
export interface K8sPodsListPayload {
filters: TagFilter;
groupBy?: BaseAutocompleteData[];
offset?: number;
limit?: number;
orderBy?: {
columnName: string;
order: 'asc' | 'desc';
};
}
export interface TimeSeriesValue {
timestamp: number;
value: string;
}
export interface TimeSeries {
labels: Record<string, string>;
labelsArray: Array<Record<string, string>>;
values: TimeSeriesValue[];
}
export interface K8sPodsData {
podUID: string;
podCPU: number;
podCPURequest: number;
podCPULimit: number;
podMemory: number;
podMemoryRequest: number;
podMemoryLimit: number;
restartCount: number;
meta: {
k8s_cronjob_name: string;
k8s_daemonset_name: string;
k8s_deployment_name: string;
k8s_job_name: string;
k8s_namespace_name: string;
k8s_node_name: string;
k8s_pod_name: string;
k8s_pod_uid: string;
k8s_statefulset_name: string;
k8s_cluster_name: string;
};
countByPhase: {
pending: number;
running: number;
succeeded: number;
failed: number;
unknown: number;
};
}
export interface K8sPodsListResponse {
status: string;
data: {
type: string;
records: K8sPodsData[];
groups: null;
total: number;
sentAnyHostMetricsData: boolean;
isSendingK8SAgentMetrics: boolean;
};
}
export const getK8sPodsList = async (
props: K8sPodsListPayload,
signal?: AbortSignal,
headers?: Record<string, string>,
): Promise<SuccessResponse<K8sPodsListResponse> | ErrorResponse> => {
try {
const response = await axios.post('/pods/list', props, {
signal,
headers,
});
return {
statusCode: 200,
error: null,
message: 'Success',
payload: response.data,
params: props,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};

View File

@@ -1,18 +1,24 @@
import { ApiV2Instance as axios } from 'api'; import { ApiV2Instance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/licenses/getAll'; import { PayloadProps } from 'types/api/licenses/getAll';
const getAll = async (): Promise< const getAll = async (): Promise<
SuccessResponse<PayloadProps> | ErrorResponse SuccessResponse<PayloadProps> | ErrorResponse
> => { > => {
const response = await axios.get('/licenses'); try {
const response = await axios.get('/licenses');
return { return {
statusCode: 200, statusCode: 200,
error: null, error: null,
message: response.data.status, message: response.data.status,
payload: response.data.data, payload: response.data.data,
}; };
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
}; };
export default getAll; export default getAll;

View File

@@ -1,10 +1,9 @@
import axios from 'api'; import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { import {
MessagingQueueServicePayload, MessagingQueueServicePayload,
MessagingQueuesPayloadProps, MessagingQueuesPayloadProps,
} from './getConsumerLagDetails'; } from 'pages/MessagingQueues/MQDetails/MQTables/getConsumerLagDetails';
import { ErrorResponse, SuccessResponse } from 'types/api';
export const getTopicThroughputOverview = async ( export const getTopicThroughputOverview = async (
props: Omit<MessagingQueueServicePayload, 'variables'>, props: Omit<MessagingQueueServicePayload, 'variables'>,

View File

@@ -1,18 +1,28 @@
import axios from 'api'; import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/user/getUser'; import { PayloadProps, Props } from 'types/api/user/getUser';
const getUser = async ( const getUser = async (
props: Props, props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => { ): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const response = await axios.get(`/user/${props.userId}`); try {
const response = await axios.get(`/user/${props.userId}`, {
headers: {
Authorization: `bearer ${props.token}`,
},
});
return { return {
statusCode: 200, statusCode: 200,
error: null, error: null,
message: 'Success', message: 'Success',
payload: response.data, payload: response.data,
}; };
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
}; };
export default getUser; export default getUser;

View File

@@ -2,6 +2,14 @@ import deleteLocalStorageKey from 'api/browser/localstorage/remove';
import { LOCALSTORAGE } from 'constants/localStorage'; import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import history from 'lib/history'; import history from 'lib/history';
import store from 'store';
import {
LOGGED_IN,
UPDATE_ORG,
UPDATE_USER,
UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN,
UPDATE_USER_ORG_ROLE,
} from 'types/actions/app';
export const Logout = (): void => { export const Logout = (): void => {
deleteLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN); deleteLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN);
@@ -11,9 +19,50 @@ export const Logout = (): void => {
deleteLocalStorageKey(LOCALSTORAGE.LOGGED_IN_USER_EMAIL); deleteLocalStorageKey(LOCALSTORAGE.LOGGED_IN_USER_EMAIL);
deleteLocalStorageKey(LOCALSTORAGE.LOGGED_IN_USER_NAME); deleteLocalStorageKey(LOCALSTORAGE.LOGGED_IN_USER_NAME);
deleteLocalStorageKey(LOCALSTORAGE.CHAT_SUPPORT); deleteLocalStorageKey(LOCALSTORAGE.CHAT_SUPPORT);
deleteLocalStorageKey(LOCALSTORAGE.USER_ID);
window.dispatchEvent(new CustomEvent('LOGOUT')); store.dispatch({
type: LOGGED_IN,
payload: {
isLoggedIn: false,
},
});
store.dispatch({
type: UPDATE_USER_ORG_ROLE,
payload: {
org: null,
role: null,
},
});
store.dispatch({
type: UPDATE_USER,
payload: {
ROLE: 'VIEWER',
email: '',
name: '',
orgId: '',
orgName: '',
profilePictureURL: '',
userId: '',
userFlags: {},
},
});
store.dispatch({
type: UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN,
payload: {
accessJwt: '',
refreshJwt: '',
},
});
store.dispatch({
type: UPDATE_ORG,
payload: {
org: [],
},
});
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore

View File

@@ -2,9 +2,9 @@ import { Button, Modal, Typography } from 'antd';
import updateCreditCardApi from 'api/billing/checkout'; import updateCreditCardApi from 'api/billing/checkout';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { CreditCard, X } from 'lucide-react'; import { CreditCard, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useMutation } from 'react-query'; import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
@@ -20,16 +20,16 @@ export default function ChatSupportGateway(): JSX.Element {
false, false,
); );
const { licenses, isFetchingLicenses } = useAppContext(); const { data: licenseData, isFetching } = useLicense();
useEffect(() => { useEffect(() => {
if (!isFetchingLicenses && licenses) { const activeValidLicense =
const activeValidLicense = licenseData?.payload?.licenses?.find(
licenses.licenses?.find((license) => license.isCurrent === true) || null; (license) => license.isCurrent === true,
) || null;
setActiveLicense(activeValidLicense); setActiveLicense(activeValidLicense);
} }, [licenseData, isFetching]);
}, [licenses, isFetchingLicenses]);
const handleBillingOnSuccess = ( const handleBillingOnSuccess = (
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>, data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,

View File

@@ -3,7 +3,6 @@
import './CustomTimePicker.styles.scss'; import './CustomTimePicker.styles.scss';
import { Input, Popover, Tooltip, Typography } from 'antd'; import { Input, Popover, Tooltip, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames'; import cx from 'classnames';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal'; import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import { import {
@@ -298,18 +297,6 @@ function CustomTimePicker({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname]); }, [location.pathname]);
const handleTimezoneHintClick = (e: React.MouseEvent): void => {
e.stopPropagation();
handleViewChange('timezone');
setIsOpenedFromFooter(false);
logEvent(
'DateTimePicker: Timezone picker opened from time range input badge',
{
page: location.pathname,
},
);
};
return ( return (
<div className="custom-time-picker"> <div className="custom-time-picker">
<Popover <Popover
@@ -373,7 +360,14 @@ function CustomTimePicker({
suffix={ suffix={
<> <>
{!!isTimezoneOverridden && activeTimezoneOffset && ( {!!isTimezoneOverridden && activeTimezoneOffset && (
<div className="timezone-badge" onClick={handleTimezoneHintClick}> <div
className="timezone-badge"
onClick={(e): void => {
e.stopPropagation();
handleViewChange('timezone');
setIsOpenedFromFooter(false);
}}
>
<span>{activeTimezoneOffset}</span> <span>{activeTimezoneOffset}</span>
</div> </div>
)} )}

View File

@@ -2,7 +2,6 @@ import './CustomTimePicker.styles.scss';
import { Color } from '@signozhq/design-tokens'; import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd'; import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames'; import cx from 'classnames';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal'; import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
@@ -82,12 +81,6 @@ function CustomTimePickerPopoverContent({
const handleTimezoneHintClick = (): void => { const handleTimezoneHintClick = (): void => {
setActiveView('timezone'); setActiveView('timezone');
setIsOpenedFromFooter(true); setIsOpenedFromFooter(true);
logEvent(
'DateTimePicker: Timezone picker opened from time range picker footer',
{
page: pathname,
},
);
}; };
if (activeView === 'timezone') { if (activeView === 'timezone') {

View File

@@ -3,9 +3,9 @@ import './RangePickerModal.styles.scss';
import { DatePicker } from 'antd'; import { DatePicker } from 'antd';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal'; import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config'; import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs, { Dayjs } from 'dayjs'; import dayjs from 'dayjs';
import { useTimezone } from 'providers/Timezone'; import { useTimezone } from 'providers/Timezone';
import { Dispatch, SetStateAction, useMemo } from 'react'; import { Dispatch, SetStateAction } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
@@ -53,32 +53,22 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
} }
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER); onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
}; };
const { timezone } = useTimezone(); const { timezone } = useTimezone();
const rangeValue: [Dayjs, Dayjs] = useMemo(
() => [
dayjs(minTime / 1000_000).tz(timezone.value),
dayjs(maxTime / 1000_000).tz(timezone.value),
],
[maxTime, minTime, timezone.value],
);
return ( return (
<div className="custom-date-picker"> <div className="custom-date-picker">
<RangePicker <RangePicker
disabledDate={disabledDate} disabledDate={disabledDate}
allowClear allowClear
showTime={{ showTime
use12Hours: true, format="YYYY-MM-DD hh:mm A"
format: 'hh:mm A',
}}
format={(date: Dayjs): string =>
date.tz(timezone.value).format('YYYY-MM-DD hh:mm A')
}
onOk={onModalOkHandler} onOk={onModalOkHandler}
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...(selectedTime === 'custom' && { {...(selectedTime === 'custom' && {
value: rangeValue, defaultValue: [
dayjs(minTime / 1000000).tz(timezone.value),
dayjs(maxTime / 1000000).tz(timezone.value),
],
})} })}
/> />
</div> </div>

View File

@@ -2,7 +2,6 @@ import './TimezonePicker.styles.scss';
import { Color } from '@signozhq/design-tokens'; import { Color } from '@signozhq/design-tokens';
import { Input } from 'antd'; import { Input } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames'; import cx from 'classnames';
import { TimezonePickerShortcuts } from 'constants/shortcuts/TimezonePickerShortcuts'; import { TimezonePickerShortcuts } from 'constants/shortcuts/TimezonePickerShortcuts';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys'; import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
@@ -127,6 +126,7 @@ function TimezonePicker({
setIsOpen, setIsOpen,
isOpenedFromFooter, isOpenedFromFooter,
}: TimezonePickerProps): JSX.Element { }: TimezonePickerProps): JSX.Element {
console.log({ isOpenedFromFooter });
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const { timezone, updateTimezone } = useTimezone(); const { timezone, updateTimezone } = useTimezone();
const [selectedTimezone, setSelectedTimezone] = useState<string>( const [selectedTimezone, setSelectedTimezone] = useState<string>(
@@ -157,12 +157,6 @@ function TimezonePicker({
updateTimezone(timezone); updateTimezone(timezone);
handleCloseTimezonePicker(); handleCloseTimezonePicker();
setIsOpen(false); setIsOpen(false);
logEvent('DateTimePicker: New Timezone Selected', {
timezone: {
name: timezone.name,
offset: timezone.offset,
},
});
}, },
[handleCloseTimezonePicker, setIsOpen, updateTimezone], [handleCloseTimezonePicker, setIsOpen, updateTimezone],
); );

View File

@@ -1,22 +1,15 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { Table } from 'antd'; import { Table } from 'antd';
import { matchMedia } from 'container/PipelinePage/tests/AddNewPipeline.test';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import i18n from 'ReactI18';
import store from 'store';
import DraggableTableRow from '..'; import DraggableTableRow from '..';
beforeAll(() => { beforeAll(() => {
Object.defineProperty(window, 'matchMedia', { matchMedia();
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
}); });
jest.mock('uplot', () => { jest.mock('uplot', () => {
@@ -41,14 +34,18 @@ jest.mock('react-dnd', () => ({
describe('DraggableTableRow Snapshot test', () => { describe('DraggableTableRow Snapshot test', () => {
it('should render DraggableTableRow', async () => { it('should render DraggableTableRow', async () => {
const { asFragment } = render( const { asFragment } = render(
<Table <Provider store={store}>
components={{ <I18nextProvider i18n={i18n}>
body: { <Table
row: DraggableTableRow, components={{
}, body: {
}} row: DraggableTableRow,
pagination={false} },
/>, }}
pagination={false}
/>
</I18nextProvider>
</Provider>,
); );
expect(asFragment()).toMatchSnapshot(); expect(asFragment()).toMatchSnapshot();
}); });

View File

@@ -99,3 +99,5 @@ exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = `
</div> </div>
</DocumentFragment> </DocumentFragment>
`; `;
exports[`PipelinePage container test should render AddNewPipeline section 1`] = `<DocumentFragment />`;

View File

@@ -127,18 +127,6 @@ function HostMetricsLogs({
data={logToRender} data={logToRender}
linesPerRow={5} linesPerRow={5}
fontSize={FontSize.MEDIUM} fontSize={FontSize.MEDIUM}
selectedFields={[
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
]}
/> />
), ),
[], [],

View File

@@ -5,16 +5,18 @@ import { Button, Typography } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { CheckCircle2, HandPlatter } from 'lucide-react'; import { CheckCircle2, HandPlatter } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
export default function WaitlistFragment({ export default function WaitlistFragment({
entityType, entityType,
}: { }: {
entityType: string; entityType: string;
}): JSX.Element { }): JSX.Element {
const { user } = useAppContext(); const { user } = useSelector<AppState, AppReducer>((state) => state.app);
const { t } = useTranslation(['infraMonitoring']); const { t } = useTranslation(['infraMonitoring']);
const { notifications } = useNotifications(); const { notifications } = useNotifications();

View File

@@ -4,7 +4,6 @@ export enum VIEWS {
TRACES = 'traces', TRACES = 'traces',
CONTAINERS = 'containers', CONTAINERS = 'containers',
PROCESSES = 'processes', PROCESSES = 'processes',
EVENTS = 'events',
} }
export const VIEW_TYPES = { export const VIEW_TYPES = {
@@ -13,5 +12,4 @@ export const VIEW_TYPES = {
TRACES: VIEWS.TRACES, TRACES: VIEWS.TRACES,
CONTAINERS: VIEWS.CONTAINERS, CONTAINERS: VIEWS.CONTAINERS,
PROCESSES: VIEWS.PROCESSES, PROCESSES: VIEWS.PROCESSES,
EVENTS: VIEWS.EVENTS,
}; };

View File

@@ -6,11 +6,12 @@ import logEvent from 'api/common/logEvent';
import cx from 'classnames'; import cx from 'classnames';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { defaultTo } from 'lodash-es'; import { defaultTo } from 'lodash-es';
import { CreditCard, HelpCircle, X } from 'lucide-react'; import { CreditCard, HelpCircle, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App'; import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useMutation } from 'react-query'; import { useMutation } from 'react-query';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
@@ -38,79 +39,31 @@ function LaunchChatSupport({
onHoverText = '', onHoverText = '',
intercomMessageDisabled = false, intercomMessageDisabled = false,
}: LaunchChatSupportProps): JSX.Element | null { }: LaunchChatSupportProps): JSX.Element | null {
const isChatSupportEnabled = useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active;
const isCloudUserVal = isCloudUser(); const isCloudUserVal = isCloudUser();
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const { const { data: licenseData, isFetching } = useLicense();
licenses,
isFetchingLicenses,
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
isLoggedIn,
} = useAppContext();
const [activeLicense, setActiveLicense] = useState<License | null>(null); const [activeLicense, setActiveLicense] = useState<License | null>(null);
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState( const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState(
false, false,
); );
const { pathname } = useLocation(); const { pathname } = useLocation();
const isPremiumChatSupportEnabled =
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
const isChatSupportEnabled = useMemo(() => { const showAddCreditCardModal =
if (!isFetchingFeatureFlags && (featureFlags || featureFlagsFetchError)) { !isPremiumChatSupportEnabled &&
let isChatSupportEnabled = false; !licenseData?.payload?.trialConvertedToSubscription;
if (featureFlags && featureFlags.length > 0) {
isChatSupportEnabled =
featureFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)
?.active || false;
}
return isChatSupportEnabled;
}
return false;
}, [featureFlags, featureFlagsFetchError, isFetchingFeatureFlags]);
const showAddCreditCardModal = useMemo(() => {
if (
!isFetchingFeatureFlags &&
(featureFlags || featureFlagsFetchError) &&
licenses
) {
let isChatSupportEnabled = false;
let isPremiumSupportEnabled = false;
const isCloudUserVal = isCloudUser();
if (featureFlags && featureFlags.length > 0) {
isChatSupportEnabled =
featureFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)
?.active || false;
isPremiumSupportEnabled =
featureFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)
?.active || false;
}
return (
isLoggedIn &&
!isPremiumSupportEnabled &&
isChatSupportEnabled &&
!licenses.trialConvertedToSubscription &&
isCloudUserVal
);
}
return false;
}, [
featureFlags,
featureFlagsFetchError,
isFetchingFeatureFlags,
isLoggedIn,
licenses,
]);
useEffect(() => { useEffect(() => {
if (!isFetchingLicenses && licenses) { const activeValidLicense =
const activeValidLicense = licenseData?.payload?.licenses?.find(
licenses.licenses?.find((license) => license.isCurrent === true) || null; (license) => license.isCurrent === true,
setActiveLicense(activeValidLicense); ) || null;
}
}, [isFetchingLicenses, licenses]); setActiveLicense(activeValidLicense);
}, [licenseData, isFetching]);
const handleFacingIssuesClick = (): void => { const handleFacingIssuesClick = (): void => {
if (showAddCreditCardModal) { if (showAddCreditCardModal) {

View File

@@ -22,13 +22,6 @@
} }
} }
.state-indicator {
width: 15px;
.log-state-indicator {
padding: 0px;
}
}
.table-timestamp { .table-timestamp {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -36,6 +29,10 @@
.ant-typography { .ant-typography {
margin-bottom: 0; margin-bottom: 0;
} }
.log-state-indicator {
padding: 0px;
}
} }
.lightMode { .lightMode {

View File

@@ -75,28 +75,12 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
} }
return [ return [
{
// We do not need any title and data index for the log state indicator
title: '',
dataIndex: '',
key: 'state-indicator',
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
children: (
<div className={cx('state-indicator', fontSize)}>
<LogStateIndicator
type={getLogIndicatorTypeForTable(item)}
fontSize={fontSize}
/>
</div>
),
}),
},
{ {
title: 'timestamp', title: 'timestamp',
dataIndex: 'timestamp', dataIndex: 'timestamp',
key: 'timestamp', key: 'timestamp',
// https://github.com/ant-design/ant-design/discussions/36886 // https://github.com/ant-design/ant-design/discussions/36886
render: (field): ColumnTypeRender<Record<string, unknown>> => { render: (field, item): ColumnTypeRender<Record<string, unknown>> => {
const date = const date =
typeof field === 'string' typeof field === 'string'
? formatTimezoneAdjustedTimestamp(field, 'YYYY-MM-DD HH:mm:ss.SSS') ? formatTimezoneAdjustedTimestamp(field, 'YYYY-MM-DD HH:mm:ss.SSS')
@@ -107,6 +91,10 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
return { return {
children: ( children: (
<div className="table-timestamp"> <div className="table-timestamp">
<LogStateIndicator
type={getLogIndicatorTypeForTable(item)}
fontSize={fontSize}
/>
<Typography.Paragraph ellipsis className={cx('text', fontSize)}> <Typography.Paragraph ellipsis className={cx('text', fontSize)}>
{date} {date}
</Typography.Paragraph> </Typography.Paragraph>

View File

@@ -1,10 +1,31 @@
import getLocalStorageKey from 'api/browser/localstorage/get';
import NotFoundImage from 'assets/NotFound'; import NotFoundImage from 'assets/NotFound';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { LOGGED_IN } from 'types/actions/app';
import { defaultText } from './constant'; import { defaultText } from './constant';
import { Button, Container, Text, TextContainer } from './styles'; import { Button, Container, Text, TextContainer } from './styles';
function NotFound({ text = defaultText }: Props): JSX.Element { function NotFound({ text = defaultText }: Props): JSX.Element {
const dispatch = useDispatch<Dispatch<AppActions>>();
const isLoggedIn = getLocalStorageKey(LOCALSTORAGE.IS_LOGGED_IN);
const onClickHandler = useCallback(() => {
if (isLoggedIn) {
dispatch({
type: LOGGED_IN,
payload: {
isLoggedIn: true,
},
});
}
}, [dispatch, isLoggedIn]);
return ( return (
<Container> <Container>
<NotFoundImage /> <NotFoundImage />
@@ -14,7 +35,7 @@ function NotFound({ text = defaultText }: Props): JSX.Element {
<Text>Page Not Found</Text> <Text>Page Not Found</Text>
</TextContainer> </TextContainer>
<Button to={ROUTES.APPLICATION} tabIndex={0}> <Button onClick={onClickHandler} to={ROUTES.APPLICATION} tabIndex={0}>
Return To Services Page Return To Services Page
</Button> </Button>
</Container> </Container>

View File

@@ -53,7 +53,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
width: calc(100% - 24px); width: 100%;
cursor: pointer; cursor: pointer;
&.filter-disabled { &.filter-disabled {

View File

@@ -8,12 +8,10 @@ import { Button, Checkbox, Input, Skeleton, Typography } from 'antd';
import cx from 'classnames'; import cx from 'classnames';
import { IQuickFiltersConfig } from 'components/QuickFilters/QuickFilters'; import { IQuickFiltersConfig } from 'components/QuickFilters/QuickFilters';
import { OPERATORS } from 'constants/queryBuilder'; import { OPERATORS } from 'constants/queryBuilder';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils'; import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues'; import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useDebouncedFn from 'hooks/useDebouncedFunction'; import { cloneDeep, isArray, isEmpty, isEqual } from 'lodash-es';
import { cloneDeep, isArray, isEmpty, isEqual, isFunction } from 'lodash-es';
import { ChevronDown, ChevronRight } from 'lucide-react'; import { ChevronDown, ChevronRight } from 'lucide-react';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
@@ -36,11 +34,10 @@ function setDefaultValues(
} }
interface ICheckboxProps { interface ICheckboxProps {
filter: IQuickFiltersConfig; filter: IQuickFiltersConfig;
onFilterChange?: (query: Query) => void;
} }
export default function CheckboxFilter(props: ICheckboxProps): JSX.Element { export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
const { filter, onFilterChange } = props; const { filter } = props;
const [searchText, setSearchText] = useState<string>(''); const [searchText, setSearchText] = useState<string>('');
const [isOpen, setIsOpen] = useState<boolean>(filter.defaultOpen); const [isOpen, setIsOpen] = useState<boolean>(filter.defaultOpen);
const [visibleItemsCount, setVisibleItemsCount] = useState<number>(10); const [visibleItemsCount, setVisibleItemsCount] = useState<number>(10);
@@ -53,9 +50,9 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
const { data, isLoading } = useGetAggregateValues( const { data, isLoading } = useGetAggregateValues(
{ {
aggregateOperator: filter.aggregateOperator || 'noop', aggregateOperator: 'noop',
dataSource: filter.dataSource || DataSource.LOGS, dataSource: DataSource.LOGS,
aggregateAttribute: filter.aggregateAttribute || '', aggregateAttribute: '',
attributeKey: filter.attributeKey.key, attributeKey: filter.attributeKey.key,
filterAttributeKeyDataType: filter.attributeKey.dataType || DataTypes.EMPTY, filterAttributeKeyDataType: filter.attributeKey.dataType || DataTypes.EMPTY,
tagType: filter.attributeKey.type || '', tagType: filter.attributeKey.type || '',
@@ -75,11 +72,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
); );
const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount); const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount);
const setSearchTextDebounced = useDebouncedFn((...args) => { // derive the state of each filter key here in the renderer itself and keep it in sync with staged query
setSearchText(args[0] as string);
}, DEBOUNCE_DELAY);
// derive the state of each filter key here in the renderer itself and keep it in sync with current query
// also we need to keep a note of last focussed query. // also we need to keep a note of last focussed query.
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
const currentFilterState = useMemo(() => { const currentFilterState = useMemo(() => {
@@ -166,12 +159,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
})), })),
}, },
}; };
redirectWithQueryBuilderData(preparedQuery);
if (onFilterChange && isFunction(onFilterChange)) {
onFilterChange(preparedQuery);
} else {
redirectWithQueryBuilderData(preparedQuery);
}
}; };
const isSomeFilterPresentForCurrentAttribute = currentQuery.builder.queryData?.[ const isSomeFilterPresentForCurrentAttribute = currentQuery.builder.queryData?.[
@@ -403,11 +391,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
}, },
}; };
if (onFilterChange && isFunction(onFilterChange)) { redirectWithQueryBuilderData(finalQuery);
onFilterChange(finalQuery);
} else {
redirectWithQueryBuilderData(finalQuery);
}
}; };
return ( return (
@@ -456,7 +440,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
<section className="search"> <section className="search">
<Input <Input
placeholder="Filter values" placeholder="Filter values"
onChange={(e): void => setSearchTextDebounced(e.target.value)} onChange={(e): void => setSearchText(e.target.value)}
disabled={isFilterDisabled} disabled={isFilterDisabled}
/> />
</section> </section>
@@ -527,7 +511,3 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
</div> </div>
); );
} }
CheckboxFilter.defaultProps = {
onFilterChange: null,
};

View File

@@ -15,8 +15,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
width: 100%;
justify-content: flex-start;
.text { .text {
color: var(--bg-vanilla-400); color: var(--bg-vanilla-400);
@@ -52,8 +50,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
width: 100%;
justify-content: flex-end;
.divider-filter { .divider-filter {
width: 1px; width: 1px;

View File

@@ -7,10 +7,9 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Tooltip, Typography } from 'antd'; import { Tooltip, Typography } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { cloneDeep, isFunction } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import Checkbox from './FilterRenderers/Checkbox/Checkbox'; import Checkbox from './FilterRenderers/Checkbox/Checkbox';
import Slider from './FilterRenderers/Slider/Slider'; import Slider from './FilterRenderers/Slider/Slider';
@@ -34,9 +33,6 @@ export interface IQuickFiltersConfig {
type: FiltersType; type: FiltersType;
title: string; title: string;
attributeKey: BaseAutocompleteData; attributeKey: BaseAutocompleteData;
aggregateOperator?: string;
aggregateAttribute?: string;
dataSource?: DataSource;
customRendererForValue?: (value: string) => JSX.Element; customRendererForValue?: (value: string) => JSX.Element;
defaultOpen: boolean; defaultOpen: boolean;
} }
@@ -44,12 +40,10 @@ export interface IQuickFiltersConfig {
interface IQuickFiltersProps { interface IQuickFiltersProps {
config: IQuickFiltersConfig[]; config: IQuickFiltersConfig[];
handleFilterVisibilityChange: () => void; handleFilterVisibilityChange: () => void;
source?: string | null;
onFilterChange?: (query: Query) => void;
} }
export default function QuickFilters(props: IQuickFiltersProps): JSX.Element { export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
const { config, handleFilterVisibilityChange, source, onFilterChange } = props; const { config, handleFilterVisibilityChange } = props;
const { const {
currentQuery, currentQuery,
@@ -84,63 +78,47 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
})), })),
}, },
}; };
redirectWithQueryBuilderData(preparedQuery);
if (onFilterChange && isFunction(onFilterChange)) {
onFilterChange(preparedQuery);
} else {
redirectWithQueryBuilderData(preparedQuery);
}
}; };
const lastQueryName = const lastQueryName =
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName; currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
const isInfraMonitoring = source === 'infra-monitoring';
return ( return (
<div className="quick-filters"> <div className="quick-filters">
{!isInfraMonitoring && ( <section className="header">
<section className="header"> <section className="left-actions">
<section className="left-actions"> <FilterOutlined />
<FilterOutlined /> <Typography.Text className="text">Filters for</Typography.Text>
<Typography.Text className="text">Filters for</Typography.Text> <Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}> <Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text> </Tooltip>
</Tooltip>
</section>
<section className="right-actions">
<Tooltip title="Reset All">
<SyncOutlined className="sync-icon" onClick={handleReset} />
</Tooltip>
<div className="divider-filter" />
<Tooltip title="Collapse Filters">
<VerticalAlignTopOutlined
rotate={270}
onClick={handleFilterVisibilityChange}
/>
</Tooltip>
</section>
</section> </section>
)} <section className="right-actions">
<Tooltip title="Reset All">
<SyncOutlined className="sync-icon" onClick={handleReset} />
</Tooltip>
<div className="divider-filter" />
<Tooltip title="Collapse Filters">
<VerticalAlignTopOutlined
rotate={270}
onClick={handleFilterVisibilityChange}
/>
</Tooltip>
</section>
</section>
<section className="filters"> <section className="filters">
{config.map((filter) => { {config.map((filter) => {
switch (filter.type) { switch (filter.type) {
case FiltersType.CHECKBOX: case FiltersType.CHECKBOX:
return <Checkbox filter={filter} onFilterChange={onFilterChange} />; return <Checkbox filter={filter} />;
case FiltersType.SLIDER: case FiltersType.SLIDER:
return <Slider filter={filter} />; return <Slider filter={filter} />;
default: default:
return <Checkbox filter={filter} onFilterChange={onFilterChange} />; return <Checkbox filter={filter} />;
} }
})} })}
</section> </section>
</div> </div>
); );
} }
QuickFilters.defaultProps = {
source: null,
onFilterChange: null,
};

View File

@@ -1,28 +1,40 @@
import { Button, Space } from 'antd'; import { Button, Space } from 'antd';
import setFlags from 'api/user/setFlags'; import setFlags from 'api/user/setFlags';
import MessageTip from 'components/MessageTip'; import MessageTip from 'components/MessageTip';
import { useAppContext } from 'providers/App/App';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_USER_FLAG } from 'types/actions/app';
import { UserFlags } from 'types/api/user/setFlags'; import { UserFlags } from 'types/api/user/setFlags';
import AppReducer from 'types/reducer/app';
import ReleaseNoteProps from '../ReleaseNoteProps'; import ReleaseNoteProps from '../ReleaseNoteProps';
export default function ReleaseNote0120({ export default function ReleaseNote0120({
release, release,
}: ReleaseNoteProps): JSX.Element | null { }: ReleaseNoteProps): JSX.Element | null {
const { user, setUserFlags } = useAppContext(); const { user } = useSelector<AppState, AppReducer>((state) => state.app);
const dispatch = useDispatch<Dispatch<AppActions>>();
const handleDontShow = useCallback(async (): Promise<void> => { const handleDontShow = useCallback(async (): Promise<void> => {
const flags: UserFlags = { ReleaseNote0120Hide: 'Y' }; const flags: UserFlags = { ReleaseNote0120Hide: 'Y' };
try { try {
setUserFlags(flags); dispatch({
type: UPDATE_USER_FLAG,
payload: {
flags,
},
});
if (!user) { if (!user) {
// no user is set, so escape the routine // no user is set, so escape the routine
return; return;
} }
const response = await setFlags({ userId: user.id, flags }); const response = await setFlags({ userId: user?.userId, flags });
if (response.statusCode !== 200) { if (response.statusCode !== 200) {
console.log('failed to complete do not show status', response.error); console.log('failed to complete do not show status', response.error);
@@ -32,7 +44,7 @@ export default function ReleaseNote0120({
// the user can switch the do no show option again in the further. // the user can switch the do no show option again in the further.
console.log('unexpected error: failed to complete do not show status', e); console.log('unexpected error: failed to complete do not show status', e);
} }
}, [setUserFlags, user]); }, [dispatch, user]);
return ( return (
<MessageTip <MessageTip

View File

@@ -1,7 +1,6 @@
import ReleaseNoteProps from 'components/ReleaseNote/ReleaseNoteProps'; import ReleaseNoteProps from 'components/ReleaseNote/ReleaseNoteProps';
import ReleaseNote0120 from 'components/ReleaseNote/Releases/ReleaseNote0120'; import ReleaseNote0120 from 'components/ReleaseNote/Releases/ReleaseNote0120';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { useAppContext } from 'providers/App/App';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { UserFlags } from 'types/api/user/setFlags'; import { UserFlags } from 'types/api/user/setFlags';
@@ -45,13 +44,12 @@ const allComponentMap: ComponentMapType[] = [
// ReleaseNote prints release specific warnings and notes that // ReleaseNote prints release specific warnings and notes that
// user needs to be aware of before using the upgraded version. // user needs to be aware of before using the upgraded version.
function ReleaseNote({ path }: ReleaseNoteProps): JSX.Element | null { function ReleaseNote({ path }: ReleaseNoteProps): JSX.Element | null {
const { user } = useAppContext(); const { userFlags, currentVersion } = useSelector<AppState, AppReducer>(
const { currentVersion } = useSelector<AppState, AppReducer>(
(state) => state.app, (state) => state.app,
); );
const c = allComponentMap.find((item) => const c = allComponentMap.find((item) =>
item.match(path, currentVersion, user.flags), item.match(path, currentVersion, userFlags),
); );
if (!c) { if (!c) {

View File

@@ -8,7 +8,7 @@ function TabLabel({
isDisabled, isDisabled,
tooltipText, tooltipText,
}: TabLabelProps): JSX.Element { }: TabLabelProps): JSX.Element {
const currentLabel = <span data-testid={`${label}`}>{label}</span>; const currentLabel = <span>{label}</span>;
if (isDisabled) { if (isDisabled) {
return ( return (

View File

@@ -1,55 +0,0 @@
.div-table {
border: 1px solid lightgray;
width: fit-content;
}
.div-tr {
display: flex;
width: fit-content;
height: 30px;
}
.div-th,
.div-td {
box-shadow: inset 0 0 0 1px lightgray;
padding: 0.25rem;
}
.div-th {
padding: 2px 4px;
position: relative;
font-weight: bold;
text-align: center;
height: 30px;
}
.div-td {
height: 30px;
}
.resizer {
position: absolute;
top: 0;
height: 100%;
right: 0;
width: 5px;
background: rgba(0, 0, 0, 0.5);
cursor: col-resize;
user-select: none;
touch-action: none;
}
.resizer.isResizing {
background: blue;
opacity: 1;
}
@media (hover: hover) {
.resizer {
opacity: 0;
}
*:hover > .resizer {
opacity: 1;
}
}

View File

@@ -1,135 +0,0 @@
import './TableV3.styles.scss';
import {
ColumnDef,
flexRender,
getCoreRowModel,
Table,
useReactTable,
} from '@tanstack/react-table';
import React, { useMemo } from 'react';
// here we are manually rendering the table body so that we can memoize the same for performant re-renders
function TableBody<T>({ table }: { table: Table<T> }): JSX.Element {
return (
<div className="div-tbody">
{table.getRowModel().rows.map((row) => (
<div key={row.id} className="div-tr">
{row.getVisibleCells().map((cell) => (
<div
key={cell.id}
className="div-td"
// we are manually setting the column width here based on the calculated column vars
style={{
width: `calc(var(--col-${cell.column.id}-size) * 1px)`,
}}
>
{cell.renderValue<any>()}
</div>
))}
</div>
))}
</div>
);
}
// memoize the table body based on the data object being passed to the table
const MemoizedTableBody = React.memo(
TableBody,
(prev, next) => prev.table.options.data === next.table.options.data,
) as typeof TableBody;
interface ITableConfig {
defaultColumnMinSize: number;
defaultColumnMaxSize: number;
}
interface ITableV3Props<T> {
columns: ColumnDef<T, any>[];
data: T[];
config: ITableConfig;
}
export function TableV3<T>(props: ITableV3Props<T>): JSX.Element {
const { data, columns, config } = props;
const table = useReactTable({
data,
columns,
defaultColumn: {
minSize: config.defaultColumnMinSize,
maxSize: config.defaultColumnMaxSize,
},
columnResizeMode: 'onChange',
getCoreRowModel: getCoreRowModel(),
debugTable: true,
debugHeaders: true,
debugColumns: true,
});
/**
* Instead of calling `column.getSize()` on every render for every header
* and especially every data cell (very expensive),
* we will calculate all column sizes at once at the root table level in a useMemo
* and pass the column sizes down as CSS variables to the <table> element.
*/
const columnSizeVars = useMemo(() => {
const headers = table.getFlatHeaders();
const colSizes: { [key: string]: number } = {};
for (let i = 0; i < headers.length; i++) {
const header = headers[i]!;
colSizes[`--header-${header.id}-size`] = header.getSize();
colSizes[`--col-${header.column.id}-size`] = header.column.getSize();
}
return colSizes;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [table.getState().columnSizingInfo, table.getState().columnSizing]);
return (
<div className="p-2">
{/* Here in the <table> equivalent element (surrounds all table head and data cells), we will define our CSS variables for column sizes */}
<div
className="div-table"
style={{
...columnSizeVars, // Define column sizes on the <table> element
width: table.getTotalSize(),
}}
>
<div className="div-thead">
{table.getHeaderGroups().map((headerGroup) => (
<div key={headerGroup.id} className="div-tr">
{headerGroup.headers.map((header) => (
<div
key={header.id}
className="div-th"
style={{
width: `calc(var(--header-${header?.id}-size) * 1px)`,
}}
>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
<div
{...{
onDoubleClick: (): void => header.column.resetSize(),
onMouseDown: header.getResizeHandler(),
onTouchStart: header.getResizeHandler(),
className: `resizer ${
header.column.getIsResizing() ? 'isResizing' : ''
}`,
}}
/>
</div>
))}
</div>
))}
</div>
{/* When resizing any column we will render this special memoized version of our table body */}
{table.getState().columnSizingInfo.isResizingColumn ? (
<MemoizedTableBody table={table} />
) : (
<TableBody table={table} />
)}
</div>
</div>
);
}

View File

@@ -1,4 +0,0 @@
.timeline-v2-container {
flex: 1;
overflow: visible;
}

View File

@@ -1,90 +0,0 @@
import './TimelineV2.styles.scss';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useEffect, useState } from 'react';
import { useMeasure } from 'react-use';
import {
getIntervals,
getMinimumIntervalsBasedOnWidth,
Interval,
} from './utils';
interface ITimelineV2Props {
startTimestamp: number;
endTimestamp: number;
timelineHeight: number;
}
function TimelineV2(props: ITimelineV2Props): JSX.Element {
const { startTimestamp, endTimestamp, timelineHeight } = props;
const [intervals, setIntervals] = useState<Interval[]>([]);
const [ref, { width }] = useMeasure<HTMLDivElement>();
const isDarkMode = useIsDarkMode();
useEffect(() => {
const spread = endTimestamp - startTimestamp;
if (spread < 0) {
return;
}
const minIntervals = getMinimumIntervalsBasedOnWidth(width);
const intervalisedSpread = (spread / minIntervals) * 1.0;
setIntervals(getIntervals(intervalisedSpread, spread));
}, [startTimestamp, endTimestamp, width]);
if (endTimestamp < startTimestamp) {
console.error(
'endTimestamp cannot be less than startTimestamp',
startTimestamp,
endTimestamp,
);
return <div />;
}
return (
<div ref={ref as never} className="timeline-v2-container">
<svg
width={width}
height={timelineHeight}
xmlns="http://www.w3.org/2000/svg"
overflow="visible"
>
<line
x1="0"
y1={timelineHeight}
x2={width}
y2={timelineHeight}
stroke={isDarkMode ? 'white' : 'black'}
strokeWidth="1"
/>
{intervals &&
intervals.length > 0 &&
intervals.map((interval, index) => (
<g
transform={`translate(${(interval.percentage * width) / 100},0)`}
key={`${interval.percentage + interval.label + index}`}
textAnchor="middle"
fontSize="0.6rem"
>
<text
x={index === intervals.length - 1 ? -10 : 0}
y={2 * Math.floor(timelineHeight / 4)}
fill={isDarkMode ? 'white' : 'black'}
>
{interval.label}
</text>
<line
y1={3 * Math.floor(timelineHeight / 4)}
y2={timelineHeight + 0.5}
stroke={isDarkMode ? 'white' : 'black'}
strokeWidth="1"
/>
</g>
))}
</svg>
</div>
);
}
export default TimelineV2;

View File

@@ -1,118 +0,0 @@
import { toFixed } from 'utils/toFixed';
type TTimeUnitName = 'ms' | 's' | 'm' | 'hr' | 'day' | 'week';
export interface IIntervalUnit {
name: TTimeUnitName;
multiplier: number;
}
export interface Interval {
label: string;
percentage: number;
}
export const INTERVAL_UNITS: IIntervalUnit[] = [
{
name: 'ms',
multiplier: 1,
},
{
name: 's',
multiplier: 1 / 1e3,
},
{
name: 'm',
multiplier: 1 / (1e3 * 60),
},
{
name: 'hr',
multiplier: 1 / (1e3 * 60 * 60),
},
{
name: 'day',
multiplier: 1 / (1e3 * 60 * 60 * 24),
},
{
name: 'week',
multiplier: 1 / (1e3 * 60 * 60 * 24 * 7),
},
];
export const getMinimumIntervalsBasedOnWidth = (width: number): number => {
// S
if (width < 640) {
return 5;
}
// M
if (width < 768) {
return 6;
}
// L
if (width < 1024) {
return 8;
}
return 10;
};
export const resolveTimeFromInterval = (
intervalTime: number,
intervalUnit: IIntervalUnit,
): number => intervalTime * intervalUnit.multiplier;
export function getIntervals(
intervalSpread: number,
baseSpread: number,
): Interval[] {
const integerPartString = intervalSpread.toString().split('.')[0];
const integerPartLength = integerPartString.length;
const intervalSpreadNormalized =
intervalSpread < 1.0
? intervalSpread
: Math.floor(Number(integerPartString) / 10 ** (integerPartLength - 1)) *
10 ** (integerPartLength - 1);
let intervalUnit = INTERVAL_UNITS[0];
for (let idx = INTERVAL_UNITS.length - 1; idx >= 0; idx -= 1) {
const standardInterval = INTERVAL_UNITS[idx];
if (intervalSpread * standardInterval.multiplier >= 1) {
intervalUnit = INTERVAL_UNITS[idx];
break;
}
}
intervalUnit = intervalUnit || INTERVAL_UNITS[0];
const intervals: Interval[] = [
{
label: `${toFixed(resolveTimeFromInterval(0, intervalUnit), 2)}${
intervalUnit.name
}`,
percentage: 0,
},
];
let tempBaseSpread = baseSpread;
let elapsedIntervals = 0;
while (tempBaseSpread && intervals.length < 20) {
let intervalTime;
if (tempBaseSpread <= 1.5 * intervalSpreadNormalized) {
intervalTime = elapsedIntervals + tempBaseSpread;
tempBaseSpread = 0;
} else {
intervalTime = elapsedIntervals + intervalSpreadNormalized;
tempBaseSpread -= intervalSpreadNormalized;
}
elapsedIntervals = intervalTime;
const interval: Interval = {
label: `${toFixed(resolveTimeFromInterval(intervalTime, intervalUnit), 2)}${
intervalUnit.name
}`,
percentage: (intervalTime / baseSpread) * 100,
};
intervals.push(interval);
}
return intervals;
}

View File

@@ -21,7 +21,5 @@ export enum LOCALSTORAGE {
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1', THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
LAST_USED_SAVED_VIEWS = 'LAST_USED_SAVED_VIEWS', LAST_USED_SAVED_VIEWS = 'LAST_USED_SAVED_VIEWS',
SHOW_LOGS_QUICK_FILTERS = 'SHOW_LOGS_QUICK_FILTERS', SHOW_LOGS_QUICK_FILTERS = 'SHOW_LOGS_QUICK_FILTERS',
USER_ID = 'USER_ID',
PREFERRED_TIMEZONE = 'PREFERRED_TIMEZONE', PREFERRED_TIMEZONE = 'PREFERRED_TIMEZONE',
UNAUTHENTICATED_ROUTE_HIT = 'UNAUTHENTICATED_ROUTE_HIT',
} }

View File

@@ -21,9 +21,4 @@ export const REACT_QUERY_KEY = {
GET_HOST_LIST: 'GET_HOST_LIST', GET_HOST_LIST: 'GET_HOST_LIST',
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE', UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3', GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3',
GET_POD_LIST: 'GET_POD_LIST',
GET_NODE_LIST: 'GET_NODE_LIST',
GET_DEPLOYMENT_LIST: 'GET_DEPLOYMENT_LIST',
GET_CLUSTER_LIST: 'GET_CLUSTER_LIST',
GET_NAMESPACE_LIST: 'GET_NAMESPACE_LIST',
}; };

View File

@@ -34,8 +34,7 @@ const ROUTES = {
MY_SETTINGS: '/my-settings', MY_SETTINGS: '/my-settings',
SETTINGS: '/settings', SETTINGS: '/settings',
ORG_SETTINGS: '/settings/org-settings', ORG_SETTINGS: '/settings/org-settings',
CUSTOM_DOMAIN_SETTINGS: '/settings/custom-domain-settings', API_KEYS: '/settings/access-tokens',
API_KEYS: '/settings/api-keys',
INGESTION_SETTINGS: '/settings/ingestion-settings', INGESTION_SETTINGS: '/settings/ingestion-settings',
SOMETHING_WENT_WRONG: '/something-went-wrong', SOMETHING_WENT_WRONG: '/something-went-wrong',
UN_AUTHORIZED: '/un-authorized', UN_AUTHORIZED: '/un-authorized',
@@ -62,7 +61,6 @@ const ROUTES = {
MESSAGING_QUEUES: '/messaging-queues', MESSAGING_QUEUES: '/messaging-queues',
MESSAGING_QUEUES_DETAIL: '/messaging-queues/detail', MESSAGING_QUEUES_DETAIL: '/messaging-queues/detail',
INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts', INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts',
INFRASTRUCTURE_MONITORING_KUBERNETES: '/infrastructure-monitoring/kubernetes',
} as const; } as const;
export default ROUTES; export default ROUTES;

View File

@@ -26,9 +26,9 @@ describe('APIKeys component', () => {
}); });
it('renders APIKeys component without crashing', () => { it('renders APIKeys component without crashing', () => {
expect(screen.getByText('API Keys')).toBeInTheDocument(); expect(screen.getByText('Access Tokens')).toBeInTheDocument();
expect( expect(
screen.getByText('Create and manage API keys for the SigNoz API'), screen.getByText('Create and manage access tokens for the SigNoz API'),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
@@ -40,16 +40,16 @@ describe('APIKeys component', () => {
); );
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('No Expiry Key')).toBeInTheDocument(); expect(screen.getByText('No Expiry Token')).toBeInTheDocument();
expect(screen.getByText('1-5 of 18 keys')).toBeInTheDocument(); expect(screen.getByText('1-5 of 18 tokens')).toBeInTheDocument();
}); });
}); });
it('opens add new key modal on button click', async () => { it('opens add new key modal on button click', async () => {
fireEvent.click(screen.getByText('New Key')); fireEvent.click(screen.getByText('New Token'));
await waitFor(() => { await waitFor(() => {
const createNewKeyBtn = screen.getByRole('button', { const createNewKeyBtn = screen.getByRole('button', {
name: /Create new key/i, name: /Create new token/i,
}); });
expect(createNewKeyBtn).toBeInTheDocument(); expect(createNewKeyBtn).toBeInTheDocument();
@@ -57,10 +57,10 @@ describe('APIKeys component', () => {
}); });
it('closes add new key modal on cancel button click', async () => { it('closes add new key modal on cancel button click', async () => {
fireEvent.click(screen.getByText('New Key')); fireEvent.click(screen.getByText('New Token'));
const createNewKeyBtn = screen.getByRole('button', { const createNewKeyBtn = screen.getByRole('button', {
name: /Create new key/i, name: /Create new token/i,
}); });
await waitFor(() => { await waitFor(() => {
@@ -79,10 +79,10 @@ describe('APIKeys component', () => {
), ),
); );
fireEvent.click(screen.getByText('New Key')); fireEvent.click(screen.getByText('New Token'));
const createNewKeyBtn = screen.getByRole('button', { const createNewKeyBtn = screen.getByRole('button', {
name: /Create new key/i, name: /Create new token/i,
}); });
await waitFor(() => { await waitFor(() => {
@@ -90,7 +90,7 @@ describe('APIKeys component', () => {
}); });
act(() => { act(() => {
const inputElement = screen.getByPlaceholderText('Enter Key Name'); const inputElement = screen.getByPlaceholderText('Enter Token Name');
fireEvent.change(inputElement, { target: { value: 'Top Secret' } }); fireEvent.change(inputElement, { target: { value: 'Top Secret' } });
fireEvent.click(screen.getByTestId('create-form-admin-role-btn')); fireEvent.click(screen.getByTestId('create-form-admin-role-btn'));
fireEvent.click(createNewKeyBtn); fireEvent.click(createNewKeyBtn);

View File

@@ -44,12 +44,14 @@ import {
View, View,
X, X,
} from 'lucide-react'; } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { ChangeEvent, useEffect, useState } from 'react'; import { ChangeEvent, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query'; import { useMutation } from 'react-query';
import { useSelector } from 'react-redux';
import { useCopyToClipboard } from 'react-use'; import { useCopyToClipboard } from 'react-use';
import { AppState } from 'store/reducers';
import { APIKeyProps } from 'types/api/pat/types'; import { APIKeyProps } from 'types/api/pat/types';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles'; import { USER_ROLES } from 'types/roles';
export const showErrorNotification = ( export const showErrorNotification = (
@@ -97,7 +99,7 @@ export const getDateDifference = (
}; };
function APIKeys(): JSX.Element { function APIKeys(): JSX.Element {
const { user } = useAppContext(); const { user } = useSelector<AppState, AppReducer>((state) => state.app);
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false);
@@ -512,15 +514,15 @@ function APIKeys(): JSX.Element {
<div className="api-key-container"> <div className="api-key-container">
<div className="api-key-content"> <div className="api-key-content">
<header> <header>
<Typography.Title className="title">API Keys</Typography.Title> <Typography.Title className="title">Access Tokens </Typography.Title>
<Typography.Text className="subtitle"> <Typography.Text className="subtitle">
Create and manage API keys for the SigNoz API Create and manage access tokens for the SigNoz API
</Typography.Text> </Typography.Text>
</header> </header>
<div className="api-keys-search-add-new"> <div className="api-keys-search-add-new">
<Input <Input
placeholder="Search for keys..." placeholder="Search for token..."
prefix={<Search size={12} color={Color.BG_VANILLA_400} />} prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
value={searchValue} value={searchValue}
onChange={handleSearch} onChange={handleSearch}
@@ -531,7 +533,7 @@ function APIKeys(): JSX.Element {
type="primary" type="primary"
onClick={showAddModal} onClick={showAddModal}
> >
<Plus size={14} /> New Key <Plus size={14} /> New Token
</Button> </Button>
</div> </div>
@@ -544,7 +546,7 @@ function APIKeys(): JSX.Element {
pageSize: 5, pageSize: 5,
hideOnSinglePage: true, hideOnSinglePage: true,
showTotal: (total: number, range: number[]): string => showTotal: (total: number, range: number[]): string =>
`${range[0]}-${range[1]} of ${total} keys`, `${range[0]}-${range[1]} of ${total} tokens`,
}} }}
/> />
</div> </div>
@@ -552,7 +554,7 @@ function APIKeys(): JSX.Element {
{/* Delete Key Modal */} {/* Delete Key Modal */}
<Modal <Modal
className="delete-api-key-modal" className="delete-api-key-modal"
title={<span className="title">Delete Key</span>} title={<span className="title">Delete Token</span>}
open={isDeleteModalOpen} open={isDeleteModalOpen}
closable closable
afterClose={handleModalClose} afterClose={handleModalClose}
@@ -574,7 +576,7 @@ function APIKeys(): JSX.Element {
onClick={onDeleteHandler} onClick={onDeleteHandler}
className="delete-btn" className="delete-btn"
> >
Delete key Delete Token
</Button>, </Button>,
]} ]}
> >
@@ -588,7 +590,7 @@ function APIKeys(): JSX.Element {
{/* Edit Key Modal */} {/* Edit Key Modal */}
<Modal <Modal
className="api-key-modal" className="api-key-modal"
title="Edit key" title="Edit token"
open={isEditModalOpen} open={isEditModalOpen}
key="edit-api-key-modal" key="edit-api-key-modal"
afterClose={handleModalClose} afterClose={handleModalClose}
@@ -612,7 +614,7 @@ function APIKeys(): JSX.Element {
icon={<Check size={14} />} icon={<Check size={14} />}
onClick={onUpdateApiKey} onClick={onUpdateApiKey}
> >
Update key Update Token
</Button>, </Button>,
]} ]}
> >
@@ -632,7 +634,7 @@ function APIKeys(): JSX.Element {
label="Name" label="Name"
rules={[{ required: true }, { type: 'string', min: 6 }]} rules={[{ required: true }, { type: 'string', min: 6 }]}
> >
<Input placeholder="Enter Key Name" autoFocus /> <Input placeholder="Enter Token Name" autoFocus />
</Form.Item> </Form.Item>
<Form.Item name="role" label="Role"> <Form.Item name="role" label="Role">
@@ -666,7 +668,7 @@ function APIKeys(): JSX.Element {
{/* Create New Key Modal */} {/* Create New Key Modal */}
<Modal <Modal
className="api-key-modal" className="api-key-modal"
title="Create new key" title="Create new token"
open={isAddModalOpen} open={isAddModalOpen}
key="create-api-key-modal" key="create-api-key-modal"
closable closable
@@ -683,7 +685,7 @@ function APIKeys(): JSX.Element {
onClick={handleCopyClose} onClick={handleCopyClose}
icon={<Check size={12} />} icon={<Check size={12} />}
> >
Copy key and close Copy token and close
</Button>, </Button>,
] ]
: [ : [
@@ -704,7 +706,7 @@ function APIKeys(): JSX.Element {
loading={isLoadingCreateAPIKey} loading={isLoadingCreateAPIKey}
onClick={onCreateAPIKey} onClick={onCreateAPIKey}
> >
Create new key Create new token
</Button>, </Button>,
] ]
} }
@@ -728,7 +730,7 @@ function APIKeys(): JSX.Element {
rules={[{ required: true }, { type: 'string', min: 6 }]} rules={[{ required: true }, { type: 'string', min: 6 }]}
validateTrigger="onFinish" validateTrigger="onFinish"
> >
<Input placeholder="Enter Key Name" autoFocus /> <Input placeholder="Enter Token Name" autoFocus />
</Form.Item> </Form.Item>
<Form.Item name="role" label="Role"> <Form.Item name="role" label="Role">
@@ -769,7 +771,7 @@ function APIKeys(): JSX.Element {
{showNewAPIKeyDetails && ( {showNewAPIKeyDetails && (
<div className="api-key-info-container"> <div className="api-key-info-container">
<Row> <Row>
<Col span={8}>Key</Col> <Col span={8}>Token</Col>
<Col span={16}> <Col span={16}>
<span className="copyable-text"> <span className="copyable-text">
<Typography.Text> <Typography.Text>

View File

@@ -6,11 +6,13 @@ import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission'; import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history'; import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { generatePath } from 'react-router-dom'; import { generatePath } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Channels, PayloadProps } from 'types/api/channels/getAll'; import { Channels, PayloadProps } from 'types/api/channels/getAll';
import AppReducer from 'types/reducer/app';
import Delete from './Delete'; import Delete from './Delete';
@@ -18,8 +20,8 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
const { t } = useTranslation(['channels']); const { t } = useTranslation(['channels']);
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const [channels, setChannels] = useState<Channels[]>(allChannels); const [channels, setChannels] = useState<Channels[]>(allChannels);
const { user } = useAppContext(); const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [action] = useComponentPermission(['new_alert_action'], user.role); const [action] = useComponentPermission(['new_alert_action'], role);
const onClickEditHandler = useCallback((id: string) => { const onClickEditHandler = useCallback((id: string) => {
history.replace( history.replace(

View File

@@ -31,6 +31,13 @@ jest.mock('hooks/useNotifications', () => ({
})), })),
})); }));
jest.mock('hooks/useFeatureFlag', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
active: true,
})),
}));
describe('Create Alert Channel', () => { describe('Create Alert Channel', () => {
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
@@ -355,7 +362,7 @@ describe('Create Alert Channel', () => {
expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue); expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue);
}); });
}); });
describe('Email', () => { describe('Opsgenie', () => {
beforeEach(() => { beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.Email} />); render(<CreateAlertChannels preType={ChannelType.Email} />);
}); });
@@ -378,9 +385,7 @@ describe('Create Alert Channel', () => {
}); });
it('Should check if the selected item in the type dropdown has text "msteams"', () => { it('Should check if the selected item in the type dropdown has text "msteams"', () => {
expect( expect(screen.getByText('msteams')).toBeInTheDocument();
screen.getByText('Microsoft Teams (Supported in Paid Plans Only)'),
).toBeInTheDocument();
}); });
it('Should check if Webhook URL label and input are displayed properly ', () => { it('Should check if Webhook URL label and input are displayed properly ', () => {

View File

@@ -286,7 +286,7 @@ describe('Create Alert Channel (Normal User)', () => {
expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue); expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue);
}); });
}); });
describe('Email', () => { describe('Opsgenie', () => {
beforeEach(() => { beforeEach(() => {
render(<CreateAlertChannels preType={ChannelType.Email} />); render(<CreateAlertChannels preType={ChannelType.Email} />);
}); });
@@ -314,8 +314,7 @@ describe('Create Alert Channel (Normal User)', () => {
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
// TODO[vikrantgupta25]: check with Shaheer it('Should check if the upgrade plan message is shown', () => {
it.skip('Should check if the upgrade plan message is shown', () => {
expect(screen.getByText('Upgrade to a Paid Plan')).toBeInTheDocument(); expect(screen.getByText('Upgrade to a Paid Plan')).toBeInTheDocument();
expect( expect(
screen.getByText(/This feature is available for paid plans only./), screen.getByText(/This feature is available for paid plans only./),
@@ -336,7 +335,7 @@ describe('Create Alert Channel (Normal User)', () => {
screen.getByRole('button', { name: 'button_return' }), screen.getByRole('button', { name: 'button_return' }),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it.skip('Should check if save and test buttons are disabled', () => { it('Should check if save and test buttons are disabled', () => {
expect( expect(
screen.getByRole('button', { name: 'button_save_channel' }), screen.getByRole('button', { name: 'button_save_channel' }),
).toBeDisabled(); ).toBeDisabled();

View File

@@ -20,6 +20,13 @@ jest.mock('hooks/useNotifications', () => ({
})), })),
})); }));
jest.mock('hooks/useFeatureFlag', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
active: true,
})),
}));
describe('Should check if the edit alert channel is properly displayed ', () => { describe('Should check if the edit alert channel is properly displayed ', () => {
beforeEach(() => { beforeEach(() => {
render(<EditAlertChannels initialValue={editAlertChannelInitialValue} />); render(<EditAlertChannels initialValue={editAlertChannelInitialValue} />);

View File

@@ -9,9 +9,11 @@ import useComponentPermission from 'hooks/useComponentPermission';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import history from 'lib/history'; import history from 'lib/history';
import { isUndefined } from 'lodash-es'; import { isUndefined } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import AlertChannelsComponent from './AlertChannels'; import AlertChannelsComponent from './AlertChannels';
import { Button, ButtonContainer, RightActionContainer } from './styles'; import { Button, ButtonContainer, RightActionContainer } from './styles';
@@ -20,10 +22,10 @@ const { Paragraph } = Typography;
function AlertChannels(): JSX.Element { function AlertChannels(): JSX.Element {
const { t } = useTranslation(['channels']); const { t } = useTranslation(['channels']);
const { user } = useAppContext(); const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [addNewChannelPermission] = useComponentPermission( const [addNewChannelPermission] = useComponentPermission(
['add_new_channel'], ['add_new_channel'],
user.role, role,
); );
const onToggleHandler = useCallback(() => { const onToggleHandler = useCallback(() => {
history.push(ROUTES.CHANNELS_NEW); history.push(ROUTES.CHANNELS_NEW);

View File

@@ -1,7 +1,8 @@
import 'uplot/dist/uPlot.min.css'; import 'uplot/dist/uPlot.min.css';
import './AnomalyAlertEvaluationView.styles.scss'; import './AnomalyAlertEvaluationView.styles.scss';
import { Checkbox, Input, Typography } from 'antd'; import { Checkbox, Typography } from 'antd';
import Search from 'antd/es/input/Search';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebouncedFn from 'hooks/useDebouncedFunction'; import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useResizeObserver } from 'hooks/useDimensions'; import { useResizeObserver } from 'hooks/useDimensions';
@@ -15,8 +16,6 @@ import uPlot from 'uplot';
import tooltipPlugin from './tooltipPlugin'; import tooltipPlugin from './tooltipPlugin';
const { Search } = Input;
function UplotChart({ function UplotChart({
data, data,
options, options,

View File

@@ -18,6 +18,8 @@ import SideNav from 'container/SideNav';
import TopNav from 'container/TopNav'; import TopNav from 'container/TopNav';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history'; import history from 'lib/history';
import { isNull } from 'lodash-es'; import { isNull } from 'lodash-es';
@@ -27,9 +29,10 @@ import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { Helmet } from 'react-helmet-async'; import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useMutation, useQueries } from 'react-query'; import { useMutation, useQueries } from 'react-query';
import { useDispatch } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions'; import AppActions from 'types/actions';
import { import {
UPDATE_CURRENT_ERROR, UPDATE_CURRENT_ERROR,
@@ -40,6 +43,7 @@ import {
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout'; import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { LicenseEvent } from 'types/api/licensesV3/getActive'; import { LicenseEvent } from 'types/api/licensesV3/getActive';
import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app'; import { isCloudUser } from 'utils/app';
import { import {
getFormattedDate, getFormattedDate,
@@ -52,18 +56,11 @@ import { getRouteKey } from './utils';
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
function AppLayout(props: AppLayoutProps): JSX.Element { function AppLayout(props: AppLayoutProps): JSX.Element {
const { const { isLoggedIn, user, role } = useSelector<AppState, AppReducer>(
isLoggedIn, (state) => state.app,
user, );
licenses,
isFetchingLicenses,
activeLicenseV3,
isFetchingActiveLicenseV3,
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
} = useAppContext();
const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext();
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const [ const [
@@ -101,6 +98,23 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
const { data: licenseData, isFetching } = useLicense();
const isPremiumChatSupportEnabled =
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
const isChatSupportEnabled =
useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active || false;
const isCloudUserVal = isCloudUser();
const showAddCreditCardModal =
isLoggedIn &&
isChatSupportEnabled &&
isCloudUserVal &&
!isPremiumChatSupportEnabled &&
!licenseData?.payload?.trialConvertedToSubscription;
const { pathname } = useLocation(); const { pathname } = useLocation();
const { t } = useTranslation(['titles']); const { t } = useTranslation(['titles']);
@@ -234,16 +248,15 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
useEffect(() => { useEffect(() => {
if ( if (
!isFetchingLicenses && !isFetching &&
licenses && licenseData?.payload?.onTrial &&
licenses.onTrial && !licenseData?.payload?.trialConvertedToSubscription &&
!licenses.trialConvertedToSubscription && !licenseData?.payload?.workSpaceBlock &&
!licenses.workSpaceBlock && getRemainingDays(licenseData?.payload.trialEnd) < 7
getRemainingDays(licenses.trialEnd) < 7
) { ) {
setShowTrialExpiryBanner(true); setShowTrialExpiryBanner(true);
} }
}, [isFetchingLicenses, licenses]); }, [licenseData, isFetching]);
useEffect(() => { useEffect(() => {
if ( if (
@@ -259,12 +272,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
// after logging out hide the trial expiry banner // after logging out hide the trial expiry banner
if (!isLoggedIn) { if (!isLoggedIn) {
setShowTrialExpiryBanner(false); setShowTrialExpiryBanner(false);
setShowPaymentFailedWarning(false);
} }
}, [isLoggedIn]); }, [isLoggedIn]);
const handleUpgrade = (): void => { const handleUpgrade = (): void => {
if (user.role === 'ADMIN') { if (role === 'ADMIN') {
history.push(ROUTES.BILLING); history.push(ROUTES.BILLING);
} }
}; };
@@ -272,8 +284,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const handleFailedPayment = (): void => { const handleFailedPayment = (): void => {
manageCreditCard({ manageCreditCard({
licenseKey: activeLicenseV3?.key || '', licenseKey: activeLicenseV3?.key || '',
successURL: window.location.origin, successURL: window.location.href,
cancelURL: window.location.origin, cancelURL: window.location.href,
}); });
}; };
@@ -292,9 +304,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD'; const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY'; const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';
const isAlertOverview = (): boolean => routeKey === 'ALERT_OVERVIEW'; const isAlertOverview = (): boolean => routeKey === 'ALERT_OVERVIEW';
const isInfraMonitoring = (): boolean => const isInfraMonitoringHosts = (): boolean =>
routeKey === 'INFRASTRUCTURE_MONITORING_HOSTS' || routeKey === 'INFRASTRUCTURE_MONITORING_HOSTS';
routeKey === 'INFRASTRUCTURE_MONITORING_KUBERNETES';
const isPathMatch = (regex: RegExp): boolean => regex.test(pathname); const isPathMatch = (regex: RegExp): boolean => regex.test(pathname);
const isDashboardView = (): boolean => const isDashboardView = (): boolean =>
@@ -316,41 +327,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
} }
}, [isDarkMode]); }, [isDarkMode]);
const showAddCreditCardModal = useMemo(() => {
if (
!isFetchingFeatureFlags &&
(featureFlags || featureFlagsFetchError) &&
licenses
) {
let isChatSupportEnabled = false;
let isPremiumSupportEnabled = false;
const isCloudUserVal = isCloudUser();
if (featureFlags && featureFlags.length > 0) {
isChatSupportEnabled =
featureFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)
?.active || false;
isPremiumSupportEnabled =
featureFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)
?.active || false;
}
return (
isLoggedIn &&
!isPremiumSupportEnabled &&
isChatSupportEnabled &&
!licenses.trialConvertedToSubscription &&
isCloudUserVal
);
}
return false;
}, [
featureFlags,
featureFlagsFetchError,
isFetchingFeatureFlags,
isLoggedIn,
licenses,
]);
return ( return (
<Layout className={cx(isDarkMode ? 'darkMode' : 'lightMode')}> <Layout className={cx(isDarkMode ? 'darkMode' : 'lightMode')}>
<Helmet> <Helmet>
@@ -360,8 +336,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
{showTrialExpiryBanner && !showPaymentFailedWarning && ( {showTrialExpiryBanner && !showPaymentFailedWarning && (
<div className="trial-expiry-banner"> <div className="trial-expiry-banner">
You are in free trial period. Your free trial will end on{' '} You are in free trial period. Your free trial will end on{' '}
<span>{getFormattedDate(licenses?.trialEnd || Date.now())}.</span> <span>
{user.role === 'ADMIN' ? ( {getFormattedDate(licenseData?.payload?.trialEnd || Date.now())}.
</span>
{role === 'ADMIN' ? (
<span> <span>
{' '} {' '}
Please{' '} Please{' '}
@@ -384,7 +362,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
)} )}
. .
</span> </span>
{user.role === 'ADMIN' ? ( {role === 'ADMIN' ? (
<span> <span>
{' '} {' '}
Please{' '} Please{' '}
@@ -407,7 +385,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
)} )}
<Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}> <Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}>
{isToDisplayLayout && !renderFullScreen && <SideNav />} {isToDisplayLayout && !renderFullScreen && (
<SideNav licenseData={licenseData} isFetching={isFetching} />
)}
<div className="app-content" data-overlayscrollbars-initialize> <div className="app-content" data-overlayscrollbars-initialize>
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}> <Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<LayoutContent data-overlayscrollbars-initialize> <LayoutContent data-overlayscrollbars-initialize>
@@ -423,7 +403,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isAlertHistory() || isAlertHistory() ||
isAlertOverview() || isAlertOverview() ||
isMessagingQueues() || isMessagingQueues() ||
isInfraMonitoring() isInfraMonitoringHosts()
? 0 ? 0
: '0 1rem', : '0 1rem',

View File

@@ -1,14 +1,17 @@
import { billingSuccessResponse } from 'mocks-server/__mockdata__/billing'; import { billingSuccessResponse } from 'mocks-server/__mockdata__/billing';
import { import {
licensesSuccessResponse,
notOfTrailResponse, notOfTrailResponse,
trialConvertedToSubscriptionResponse, trialConvertedToSubscriptionResponse,
} from 'mocks-server/__mockdata__/licenses'; } from 'mocks-server/__mockdata__/licenses';
import { act, render, screen, waitFor } from 'tests/test-utils'; import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { act, render, screen } from 'tests/test-utils';
import { getFormattedDate } from 'utils/timeUtils'; import { getFormattedDate } from 'utils/timeUtils';
import BillingContainer from './BillingContainer'; import BillingContainer from './BillingContainer';
const lisenceUrl = 'http://localhost/api/v2/licenses';
jest.mock('uplot', () => { jest.mock('uplot', () => {
const paths = { const paths = {
spline: jest.fn(), spline: jest.fn(),
@@ -35,7 +38,9 @@ window.ResizeObserver =
describe('BillingContainer', () => { describe('BillingContainer', () => {
test('Component should render', async () => { test('Component should render', async () => {
render(<BillingContainer />); act(() => {
render(<BillingContainer />);
});
const dataInjection = screen.getByRole('columnheader', { const dataInjection = screen.getByRole('columnheader', {
name: /data ingested/i, name: /data ingested/i,
@@ -50,18 +55,13 @@ describe('BillingContainer', () => {
}); });
expect(cost).toBeInTheDocument(); expect(cost).toBeInTheDocument();
const dayRemainingInBillingPeriod = await screen.findByText(
/11 days_remaining/i,
);
expect(dayRemainingInBillingPeriod).toBeInTheDocument();
const manageBilling = screen.getByRole('button', { const manageBilling = screen.getByRole('button', {
name: 'manage_billing', name: 'manage_billing',
}); });
expect(manageBilling).toBeInTheDocument(); expect(manageBilling).toBeInTheDocument();
const dollar = screen.getByText(/\$1,278.3/i); const dollar = screen.getByText(/\$0/i);
await waitFor(() => expect(dollar).toBeInTheDocument()); expect(dollar).toBeInTheDocument();
const currentBill = screen.getByText('billing'); const currentBill = screen.getByText('billing');
expect(currentBill).toBeInTheDocument(); expect(currentBill).toBeInTheDocument();
@@ -69,9 +69,7 @@ describe('BillingContainer', () => {
test('OnTrail', async () => { test('OnTrail', async () => {
act(() => { act(() => {
render(<BillingContainer />, undefined, undefined, { render(<BillingContainer />);
licenses: licensesSuccessResponse.data,
});
}); });
const freeTrailText = await screen.findByText('Free Trial'); const freeTrailText = await screen.findByText('Free Trial');
@@ -102,10 +100,14 @@ describe('BillingContainer', () => {
}); });
test('OnTrail but trialConvertedToSubscription', async () => { test('OnTrail but trialConvertedToSubscription', async () => {
server.use(
rest.get(lisenceUrl, (req, res, ctx) =>
res(ctx.status(200), ctx.json(trialConvertedToSubscriptionResponse)),
),
);
act(() => { act(() => {
render(<BillingContainer />, undefined, undefined, { render(<BillingContainer />);
licenses: trialConvertedToSubscriptionResponse.data,
});
}); });
const currentBill = screen.getByText('billing'); const currentBill = screen.getByText('billing');
@@ -136,9 +138,12 @@ describe('BillingContainer', () => {
}); });
test('Not on ontrail', async () => { test('Not on ontrail', async () => {
const { findByText } = render(<BillingContainer />, undefined, undefined, { server.use(
licenses: notOfTrailResponse.data, rest.get(lisenceUrl, (req, res, ctx) =>
}); res(ctx.status(200), ctx.json(notOfTrailResponse)),
),
);
const { findByText } = render(<BillingContainer />);
const billingPeriodText = `Your current billing period is from ${getFormattedDate( const billingPeriodText = `Your current billing period is from ${getFormattedDate(
billingSuccessResponse.data.billingPeriodStart, billingSuccessResponse.data.billingPeriodStart,
@@ -163,4 +168,17 @@ describe('BillingContainer', () => {
}); });
expect(logRow).toBeInTheDocument(); expect(logRow).toBeInTheDocument();
}); });
test('Should render corrent day remaining in billing period', async () => {
server.use(
rest.get(lisenceUrl, (req, res, ctx) =>
res(ctx.status(200), ctx.json(notOfTrailResponse)),
),
);
render(<BillingContainer />);
const dayRemainingInBillingPeriod = await screen.findByText(
/11 days_remaining/i,
);
expect(dayRemainingInBillingPeriod).toBeInTheDocument();
});
}); });

View File

@@ -24,15 +24,18 @@ import Spinner from 'components/Spinner';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useAxiosError from 'hooks/useAxiosError'; import useAxiosError from 'hooks/useAxiosError';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { isEmpty, pick } from 'lodash-es'; import { isEmpty, pick } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useMutation, useQuery } from 'react-query'; import { useMutation, useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout'; import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { License } from 'types/api/licenses/def'; import { License } from 'types/api/licenses/def';
import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app'; import { isCloudUser } from 'utils/app';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils'; import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
@@ -134,13 +137,9 @@ export default function BillingContainer(): JSX.Element {
Partial<UsageResponsePayloadProps> Partial<UsageResponsePayloadProps>
>({}); >({});
const { const { isFetching, data: licensesData, error: licenseError } = useLicense();
user,
org, const { user, org } = useSelector<AppState, AppReducer>((state) => state.app);
licenses,
isFetchingLicenses,
licensesFetchError,
} = useAppContext();
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const handleError = useAxiosError(); const handleError = useAxiosError();
@@ -182,7 +181,7 @@ export default function BillingContainer(): JSX.Element {
setData(formattedUsageData); setData(formattedUsageData);
if (!licenses?.onTrial) { if (!licensesData?.payload?.onTrial) {
const remainingDays = getRemainingDays(billingPeriodEnd) - 1; const remainingDays = getRemainingDays(billingPeriodEnd) - 1;
setHeaderText( setHeaderText(
@@ -196,14 +195,14 @@ export default function BillingContainer(): JSX.Element {
setApiResponse(data?.payload || {}); setApiResponse(data?.payload || {});
}, },
[licenses?.onTrial], [licensesData?.payload?.onTrial],
); );
const isSubscriptionPastDue = const isSubscriptionPastDue =
apiResponse.subscriptionStatus === SubscriptionStatus.PastDue; apiResponse.subscriptionStatus === SubscriptionStatus.PastDue;
const { isLoading, isFetching: isFetchingBillingData } = useQuery( const { isLoading, isFetching: isFetchingBillingData } = useQuery(
[REACT_QUERY_KEY.GET_BILLING_USAGE, user?.id], [REACT_QUERY_KEY.GET_BILLING_USAGE, user?.userId],
{ {
queryFn: () => getUsage(activeLicense?.key || ''), queryFn: () => getUsage(activeLicense?.key || ''),
onError: handleError, onError: handleError,
@@ -214,29 +213,25 @@ export default function BillingContainer(): JSX.Element {
useEffect(() => { useEffect(() => {
const activeValidLicense = const activeValidLicense =
licenses?.licenses?.find((license) => license.isCurrent === true) || null; licensesData?.payload?.licenses?.find(
(license) => license.isCurrent === true,
) || null;
setActiveLicense(activeValidLicense); setActiveLicense(activeValidLicense);
if (!isFetchingLicenses && licenses?.onTrial && !licensesFetchError) { if (!isFetching && licensesData?.payload?.onTrial && !licenseError) {
const remainingDays = getRemainingDays(licenses?.trialEnd); const remainingDays = getRemainingDays(licensesData?.payload?.trialEnd);
setIsFreeTrial(true); setIsFreeTrial(true);
setBillAmount(0); setBillAmount(0);
setDaysRemaining(remainingDays > 0 ? remainingDays : 0); setDaysRemaining(remainingDays > 0 ? remainingDays : 0);
setHeaderText( setHeaderText(
`You are in free trial period. Your free trial will end on ${getFormattedDate( `You are in free trial period. Your free trial will end on ${getFormattedDate(
licenses?.trialEnd, licensesData?.payload?.trialEnd,
)}`, )}`,
); );
} }
}, [ }, [isFetching, licensesData?.payload, licenseError]);
licenses?.licenses,
licenses?.onTrial,
licenses?.trialEnd,
isFetchingLicenses,
licensesFetchError,
]);
const columns: ColumnsType<DataType> = [ const columns: ColumnsType<DataType> = [
{ {
@@ -318,7 +313,7 @@ export default function BillingContainer(): JSX.Element {
}); });
const handleBilling = useCallback(async () => { const handleBilling = useCallback(async () => {
if (isFreeTrial && !licenses?.trialConvertedToSubscription) { if (isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription) {
logEvent('Billing : Upgrade Plan', { logEvent('Billing : Upgrade Plan', {
user: pick(user, ['email', 'userId', 'name']), user: pick(user, ['email', 'userId', 'name']),
org, org,
@@ -345,7 +340,7 @@ export default function BillingContainer(): JSX.Element {
}, [ }, [
activeLicense?.key, activeLicense?.key,
isFreeTrial, isFreeTrial,
licenses?.trialConvertedToSubscription, licensesData?.payload?.trialConvertedToSubscription,
manageCreditCard, manageCreditCard,
updateCreditCard, updateCreditCard,
]); ]);
@@ -430,7 +425,7 @@ export default function BillingContainer(): JSX.Element {
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Flex vertical> <Flex vertical>
<Typography.Title level={5} style={{ marginTop: 2, fontWeight: 500 }}> <Typography.Title level={5} style={{ marginTop: 2, fontWeight: 500 }}>
{isCloudUserVal ? t('teams_cloud') : t('teams')}{' '} {isCloudUserVal ? t('enterprise_cloud') : t('enterprise')}{' '}
{isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''} {isFreeTrial ? <Tag color="success"> Free Trial </Tag> : ''}
</Typography.Title> </Typography.Title>
{!isLoading && !isFetchingBillingData ? ( {!isLoading && !isFetchingBillingData ? (
@@ -457,21 +452,22 @@ export default function BillingContainer(): JSX.Element {
disabled={isLoading} disabled={isLoading}
onClick={handleBilling} onClick={handleBilling}
> >
{isFreeTrial && !licenses?.trialConvertedToSubscription {isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription
? t('upgrade_plan') ? t('upgrade_plan')
: t('manage_billing')} : t('manage_billing')}
</Button> </Button>
</Flex> </Flex>
</Flex> </Flex>
{licenses?.onTrial && licenses?.trialConvertedToSubscription && ( {licensesData?.payload?.onTrial &&
<Typography.Text licensesData?.payload?.trialConvertedToSubscription && (
ellipsis <Typography.Text
style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }} ellipsis
> style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }}
{t('card_details_recieved_and_billing_info')} >
</Typography.Text> {t('card_details_recieved_and_billing_info')}
)} </Typography.Text>
)}
{!isLoading && !isFetchingBillingData ? ( {!isLoading && !isFetchingBillingData ? (
headerText && ( headerText && (
@@ -514,7 +510,7 @@ export default function BillingContainer(): JSX.Element {
{(isLoading || isFetchingBillingData) && renderTableSkeleton()} {(isLoading || isFetchingBillingData) && renderTableSkeleton()}
</div> </div>
{isFreeTrial && !licenses?.trialConvertedToSubscription && ( {isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription && (
<div className="upgrade-plan-benefits"> <div className="upgrade-plan-benefits">
<Row <Row
justify="space-between" justify="space-between"

View File

@@ -1,146 +0,0 @@
import ROUTES from 'constants/routes';
import CreateAlertPage from 'pages/CreateAlert';
import { MemoryRouter, Route } from 'react-router-dom';
import { act, fireEvent, render } from 'tests/test-utils';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { ALERT_TYPE_TO_TITLE, ALERT_TYPE_URL_MAP } from './constants';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALERTS_NEW}`,
}),
}));
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
let mockWindowOpen: jest.Mock;
window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
unobserve: jest.fn(),
}));
function findLinkForAlertType(
links: HTMLElement[],
alertType: AlertTypes,
): HTMLElement {
const link = links.find(
(el) =>
el.closest('[data-testid]')?.getAttribute('data-testid') ===
`alert-type-card-${alertType}`,
);
expect(link).toBeTruthy();
return link as HTMLElement;
}
function clickLinkAndVerifyRedirect(
link: HTMLElement,
expectedUrl: string,
): void {
fireEvent.click(link);
expect(mockWindowOpen).toHaveBeenCalledWith(expectedUrl, '_blank');
}
describe('Alert rule documentation redirection', () => {
let renderResult: ReturnType<typeof render>;
beforeAll(() => {
mockWindowOpen = jest.fn();
window.open = mockWindowOpen;
});
beforeEach(() => {
act(() => {
renderResult = render(
<MemoryRouter initialEntries={['/alerts/new']}>
<Route path={ROUTES.ALERTS_NEW}>
<CreateAlertPage />
</Route>
</MemoryRouter>,
);
});
});
it('should render alert type cards', () => {
const { getByText, getAllByText } = renderResult;
// Check for the heading
expect(getByText('choose_alert_type')).toBeInTheDocument();
// Check for alert type titles and descriptions
Object.values(AlertTypes).forEach((alertType) => {
const title = ALERT_TYPE_TO_TITLE[alertType];
expect(getByText(title)).toBeInTheDocument();
expect(getByText(`${title}_desc`)).toBeInTheDocument();
});
const clickHereLinks = getAllByText(
'Click here to see how to create a sample alert.',
);
expect(clickHereLinks).toHaveLength(5);
});
it('should redirect to correct documentation for each alert type', () => {
const { getAllByText } = renderResult;
const clickHereLinks = getAllByText(
'Click here to see how to create a sample alert.',
);
const alertTypeCount = Object.keys(AlertTypes).length;
expect(clickHereLinks).toHaveLength(alertTypeCount);
Object.values(AlertTypes).forEach((alertType) => {
const linkForAlertType = findLinkForAlertType(clickHereLinks, alertType);
const expectedUrl = ALERT_TYPE_URL_MAP[alertType];
clickLinkAndVerifyRedirect(linkForAlertType, expectedUrl.selection);
});
expect(mockWindowOpen).toHaveBeenCalledTimes(alertTypeCount);
});
Object.values(AlertTypes)
.filter((type) => type !== AlertTypes.ANOMALY_BASED_ALERT)
.forEach((alertType) => {
it(`should redirect to create alert page for ${alertType} and "Check an example alert" should redirect to the correct documentation`, () => {
const { getByTestId, getByRole } = renderResult;
const alertTypeLink = getByTestId(`alert-type-card-${alertType}`);
act(() => {
fireEvent.click(alertTypeLink);
});
act(() => {
fireEvent.click(
getByRole('button', {
name: /alert setup guide/i,
}),
);
});
expect(mockWindowOpen).toHaveBeenCalledWith(
ALERT_TYPE_URL_MAP[alertType].creation,
'_blank',
);
});
});
});

View File

@@ -1,71 +0,0 @@
import ROUTES from 'constants/routes';
import CreateAlertPage from 'pages/CreateAlert';
import { MemoryRouter, Route } from 'react-router-dom';
import { act, fireEvent, render } from 'tests/test-utils';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { ALERT_TYPE_URL_MAP } from './constants';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string; search: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALERTS_NEW}`,
search: 'ruleType=anomaly_rule',
}),
}));
jest.mock('uplot', () => {
const paths = {
spline: jest.fn(),
bars: jest.fn(),
};
const uplotMock = jest.fn(() => ({
paths,
}));
return {
paths,
default: uplotMock,
};
});
window.ResizeObserver =
window.ResizeObserver ||
jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
unobserve: jest.fn(),
}));
describe('Anomaly Alert Documentation Redirection', () => {
let mockWindowOpen: jest.Mock;
beforeAll(() => {
mockWindowOpen = jest.fn();
window.open = mockWindowOpen;
});
it('should handle anomaly alert documentation redirection correctly', () => {
const { getByRole } = render(
<MemoryRouter initialEntries={['/alerts/new']}>
<Route path={ROUTES.ALERTS_NEW}>
<CreateAlertPage />
</Route>
</MemoryRouter>,
);
const alertType = AlertTypes.ANOMALY_BASED_ALERT;
act(() => {
fireEvent.click(
getByRole('button', {
name: /alert setup guide/i,
}),
);
});
expect(mockWindowOpen).toHaveBeenCalledWith(
ALERT_TYPE_URL_MAP[alertType].creation,
'_blank',
);
});
});

View File

@@ -2,7 +2,7 @@ import { Row, Tag, Typography } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts'; import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App'; import useFeatureFlags from 'hooks/useFeatureFlag';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AlertTypes } from 'types/api/alerts/alertTypes'; import { AlertTypes } from 'types/api/alerts/alertTypes';
@@ -13,11 +13,9 @@ import { OptionType } from './types';
function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element { function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
const { t } = useTranslation(['alerts']); const { t } = useTranslation(['alerts']);
const { featureFlags } = useAppContext();
const isAnomalyDetectionEnabled = const isAnomalyDetectionEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION) useFeatureFlags(FeatureKeys.ANOMALY_DETECTION)?.active || false;
?.active || false;
const optionList = getOptionList(t, isAnomalyDetectionEnabled); const optionList = getOptionList(t, isAnomalyDetectionEnabled);

View File

@@ -1,47 +0,0 @@
import { AlertTypes } from 'types/api/alerts/alertTypes';
// since we don't have a card in alert creation for anomaly based alert
export const ALERT_TYPE_URL_MAP: Record<
AlertTypes,
{ selection: string; creation: string }
> = {
[AlertTypes.METRICS_BASED_ALERT]: {
selection:
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples',
creation:
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
},
[AlertTypes.LOGS_BASED_ALERT]: {
selection:
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples',
creation:
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
},
[AlertTypes.TRACES_BASED_ALERT]: {
selection:
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples',
creation:
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
},
[AlertTypes.EXCEPTIONS_BASED_ALERT]: {
selection:
'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples',
creation:
'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
},
[AlertTypes.ANOMALY_BASED_ALERT]: {
selection:
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples',
creation:
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
},
};
export const ALERT_TYPE_TO_TITLE: Record<AlertTypes, string> = {
[AlertTypes.METRICS_BASED_ALERT]: 'metric_based_alert',
[AlertTypes.LOGS_BASED_ALERT]: 'log_based_alert',
[AlertTypes.TRACES_BASED_ALERT]: 'traces_based_alert',
[AlertTypes.EXCEPTIONS_BASED_ALERT]: 'exceptions_based_alert',
[AlertTypes.ANOMALY_BASED_ALERT]: 'anomaly_based_alert',
};

View File

@@ -1,262 +0,0 @@
.custom-domain-settings-container {
margin-top: 24px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 24px;
width: 100%;
.custom-domain-settings-content {
width: calc(100% - 30px);
max-width: 736px;
.title {
color: var(--bg-vanilla-100);
font-size: var(--font-size-lg);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 28px; /* 155.556% */
letter-spacing: -0.09px;
}
.subtitle {
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.custom-domain-settings-card {
border-radius: 4px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-400);
.ant-card-body {
padding: 12px;
display: flex;
flex-direction: column;
.custom-domain-settings-content-header {
color: var(--bg-vanilla-100);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.custom-domain-settings-content-body {
margin-top: 12px;
display: flex;
gap: 12px;
align-items: flex-end;
justify-content: space-between;
.custom-domain-url-edit-btn {
.periscope-btn {
border-radius: 2px;
border: 1px solid var(--Slate-200, #2c3140);
background: var(--Ink-200, #23262e);
}
}
}
.custom-domain-urls {
display: flex;
flex-direction: column;
flex: 1;
}
.custom-domain-url {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
line-height: 24px;
padding: 4px 0;
}
.custom-domain-update-status {
margin-top: 12px;
color: var(--bg-robin-400);
font-size: 13px;
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px;
letter-spacing: -0.07px;
border-radius: 4px;
border: 1px solid rgba(78, 116, 248, 0.1);
background: rgba(78, 116, 248, 0.1);
}
}
}
}
.custom-domain-settings-modal {
.ant-modal-content {
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
padding: 0;
.ant-modal-header {
background: none;
border-bottom: 1px solid var(--bg-slate-500);
padding: 16px;
margin-bottom: 0;
}
.ant-modal-close-x {
font-size: 12px;
}
.ant-modal-body {
padding: 12px 16px;
.custom-domain-settings-modal-body {
margin-bottom: 48px;
font-size: 13px;
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.custom-domain-settings-modal-error {
display: flex;
flex-direction: column;
gap: 24px;
.update-limit-reached-error {
display: flex;
padding: 20px 24px 24px 24px;
flex-direction: column;
align-items: center;
gap: 24px;
align-self: stretch;
border-radius: 4px;
border: 1px solid rgba(255, 205, 86, 0.2);
background: rgba(255, 205, 86, 0.1);
color: var(--bg-amber-400);
font-size: 13px;
font-style: normal;
line-height: 20px; /* 142.857% */
}
.ant-alert-message::first-letter {
text-transform: capitalize;
}
}
.custom-domain-settings-modal-footer {
padding: 16px 0;
margin-top: 0;
display: flex;
justify-content: flex-end;
.apply-changes-btn {
width: 100%;
}
.facing-issue-button {
width: 100%;
.periscope-btn {
width: 100%;
border-radius: 2px;
background: var(--bg-robin-500);
border: none;
color: var(--bg-vanilla-100);
line-height: 20px;
.ant-btn-icon {
display: none;
}
&:hover {
background: var(--bg-robin-500) !important;
border: none !important;
color: var(--bg-vanilla-100) !important;
line-height: 20px !important;
}
}
}
}
}
}
.lightMode {
.custom-domain-settings-container {
.custom-domain-settings-content {
.title {
color: var(--bg-ink-400);
}
.subtitle {
color: var(--bg-vanilla-400);
}
}
.custom-domain-settings-card {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.ant-card-body {
.custom-domain-settings-content-header {
color: var(--bg-ink-100);
}
.custom-domain-update-status {
color: var(--bg-robin-400);
border: 1px solid rgba(78, 116, 248, 0.1);
background: rgba(78, 116, 248, 0.1);
}
.custom-domain-url-edit-btn {
.periscope-btn {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
box-shadow: none;
}
}
}
}
}
.custom-domain-settings-modal {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-header {
border-bottom: 1px solid var(--bg-vanilla-300);
}
.custom-domain-settings-modal-error {
.update-limit-reached-error {
border: 1px solid rgba(255, 205, 86, 0.2);
background: rgba(255, 205, 86, 0.1);
color: var(--bg-amber-500);
}
}
}
}
}

View File

@@ -1,309 +0,0 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './CustomDomainSettings.styles.scss';
import { Color } from '@signozhq/design-tokens';
import {
Alert,
Button,
Card,
Form,
Input,
Modal,
Skeleton,
Tag,
Typography,
} from 'antd';
import updateSubDomainAPI from 'api/customDomain/updateSubDomain';
import { AxiosError } from 'axios';
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
import { useGetDeploymentsData } from 'hooks/CustomDomain/useGetDeploymentsData';
import { useNotifications } from 'hooks/useNotifications';
import { InfoIcon, Link2, Pencil } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { useCopyToClipboard } from 'react-use';
import { HostsProps } from 'types/api/customDomain/types';
interface CustomDomainSettingsProps {
subdomain: string;
}
export default function CustomDomainSettings(): JSX.Element {
const { org } = useAppContext();
const { notifications } = useNotifications();
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isPollingEnabled, setIsPollingEnabled] = useState(false);
const [hosts, setHosts] = useState<HostsProps[] | null>(null);
const [updateDomainError, setUpdateDomainError] = useState<AxiosError | null>(
null,
);
const [, setCopyUrl] = useCopyToClipboard();
const [
customDomainDetails,
setCustomDomainDetails,
] = useState<CustomDomainSettingsProps | null>();
const [editForm] = Form.useForm();
const handleModalClose = (): void => {
setIsEditModalOpen(false);
editForm.resetFields();
setUpdateDomainError(null);
};
const {
data: deploymentsData,
isLoading: isLoadingDeploymentsData,
isFetching: isFetchingDeploymentsData,
refetch: refetchDeploymentsData,
} = useGetDeploymentsData();
const {
mutate: updateSubDomain,
isLoading: isLoadingUpdateCustomDomain,
} = useMutation(updateSubDomainAPI, {
onSuccess: () => {
setIsPollingEnabled(true);
refetchDeploymentsData();
setIsEditModalOpen(false);
},
onError: (error: AxiosError) => {
setUpdateDomainError(error);
setIsPollingEnabled(false);
},
});
useEffect(() => {
if (isFetchingDeploymentsData) {
return;
}
if (deploymentsData?.data?.status === 'success') {
setHosts(deploymentsData.data.data.hosts);
const activeCustomDomain = deploymentsData.data.data.hosts.find(
(host) => !host.is_default,
);
if (activeCustomDomain) {
setCustomDomainDetails({
subdomain: activeCustomDomain?.name || '',
});
}
}
if (deploymentsData?.data?.data?.state !== 'HEALTHY' && isPollingEnabled) {
setTimeout(() => {
refetchDeploymentsData();
}, 3000);
}
if (deploymentsData?.data?.data.state === 'HEALTHY') {
setIsPollingEnabled(false);
}
}, [
deploymentsData,
refetchDeploymentsData,
isPollingEnabled,
isFetchingDeploymentsData,
]);
const onUpdateCustomDomainSettings = (): void => {
editForm
.validateFields()
.then((values) => {
if (values.subdomain) {
updateSubDomain({
data: {
name: values.subdomain,
},
});
setCustomDomainDetails({
subdomain: values.subdomain,
});
}
})
.catch((errorInfo) => {
console.error('error info', errorInfo);
});
};
const onCopyUrlHandler = (host: string): void => {
const url = `${host}.${deploymentsData?.data.data.cluster.region.dns}`;
setCopyUrl(url);
notifications.success({
message: 'Copied to clipboard',
});
};
return (
<div className="custom-domain-settings-container">
<div className="custom-domain-settings-content">
<header>
<Typography.Title className="title">
Custom Domain Settings
</Typography.Title>
<Typography.Text className="subtitle">
Personalize your workspace domain effortlessly.
</Typography.Text>
</header>
</div>
<div className="custom-domain-settings-content">
{!isLoadingDeploymentsData && (
<Card className="custom-domain-settings-card">
<div className="custom-domain-settings-content-header">
Team {org?.[0]?.name} Information
</div>
<div className="custom-domain-settings-content-body">
<div className="custom-domain-urls">
{hosts?.map((host) => (
<div
className="custom-domain-url"
key={host.name}
onClick={(): void => onCopyUrlHandler(host.name)}
>
<Link2 size={12} /> {host.name}.
{deploymentsData?.data.data.cluster.region.dns}
{host.is_default && <Tag color={Color.BG_ROBIN_500}>Default</Tag>}
</div>
))}
</div>
<div className="custom-domain-url-edit-btn">
<Button
className="periscope-btn"
disabled={
isLoadingDeploymentsData ||
isFetchingDeploymentsData ||
isPollingEnabled
}
type="default"
icon={<Pencil size={10} />}
onClick={(): void => setIsEditModalOpen(true)}
>
Customize teams URL
</Button>
</div>
</div>
{isPollingEnabled && (
<Alert
className="custom-domain-update-status"
message={`Updating your URL to ⎯ ${customDomainDetails?.subdomain}.${deploymentsData?.data.data.cluster.region.dns}. This may take a few mins.`}
type="info"
icon={<InfoIcon size={12} />}
/>
)}
</Card>
)}
{isLoadingDeploymentsData && (
<Card className="custom-domain-settings-card">
<Skeleton
className="custom-domain-settings-skeleton"
active
paragraph={{ rows: 2 }}
/>
</Card>
)}
</div>
{/* Update Custom Domain Modal */}
<Modal
className="custom-domain-settings-modal"
title="Customize your teams URL"
open={isEditModalOpen}
key="edit-custom-domain-settings-modal"
afterClose={handleModalClose}
// closable
onCancel={handleModalClose}
destroyOnClose
footer={null}
>
<Form
name="edit-custom-domain-settings-form"
key={customDomainDetails?.subdomain}
form={editForm}
layout="vertical"
autoComplete="off"
initialValues={{
subdomain: customDomainDetails?.subdomain,
}}
>
{updateDomainError?.status !== 409 && (
<>
<div className="custom-domain-settings-modal-body">
Enter your preferred subdomain to create a unique URL for your team.
Need help? Contact support.
</div>
<Form.Item
name="subdomain"
label="Teams URL subdomain"
rules={[{ required: true }, { type: 'string', min: 3 }]}
>
<Input
addonBefore={updateDomainError && <InfoIcon size={12} color="red" />}
placeholder="Enter Domain"
onChange={(): void => setUpdateDomainError(null)}
addonAfter={deploymentsData?.data.data.cluster.region.dns}
autoFocus
/>
</Form.Item>
</>
)}
{updateDomainError && (
<div className="custom-domain-settings-modal-error">
{updateDomainError.status === 409 ? (
<Alert
message="Youve already updated the custom domain once today. To make further changes, please contact our support team for assistance."
type="warning"
className="update-limit-reached-error"
/>
) : (
<Typography.Text type="danger">
{(updateDomainError.response?.data as { error: string })?.error}
</Typography.Text>
)}
</div>
)}
{updateDomainError?.status !== 409 && (
<div className="custom-domain-settings-modal-footer">
<Button
className="periscope-btn primary apply-changes-btn"
onClick={onUpdateCustomDomainSettings}
loading={isLoadingUpdateCustomDomain}
>
Apply Changes
</Button>
</div>
)}
{updateDomainError?.status === 409 && (
<div className="custom-domain-settings-modal-footer">
<LaunchChatSupport
attributes={{
screen: 'Custom Domain Settings',
}}
eventName="Custom Domain Settings: Facing Issues Updating Custom Domain"
message="Hi Team, I need help with updating custom domain"
buttonText="Contact Support"
/>
</div>
)}
</Form>
</Modal>
</div>
);
}

View File

@@ -1,3 +0,0 @@
import CustomDomainSettings from './CustomDomainSettings';
export default CustomDomainSettings;

View File

@@ -46,7 +46,6 @@ import {
Plus, Plus,
X, X,
} from 'lucide-react'; } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { import {
CSSProperties, CSSProperties,
Dispatch, Dispatch,
@@ -57,12 +56,15 @@ import {
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { Dashboard } from 'types/api/dashboard/getAll'; import { Dashboard } from 'types/api/dashboard/getAll';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { ViewProps } from 'types/api/saveViews/types'; import { ViewProps } from 'types/api/saveViews/types';
import { DataSource, StringOperators } from 'types/common/queryBuilder'; import { DataSource, StringOperators } from 'types/common/queryBuilder';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles'; import { USER_ROLES } from 'types/roles';
import { PreservedViewsTypes } from './constants'; import { PreservedViewsTypes } from './constants';
@@ -112,7 +114,6 @@ function ExplorerOptions({
panelType, panelType,
isStagedQueryUpdated, isStagedQueryUpdated,
redirectWithQueryBuilderData, redirectWithQueryBuilderData,
isDefaultQuery,
} = useQueryBuilder(); } = useQueryBuilder();
const handleSaveViewModalToggle = (): void => { const handleSaveViewModalToggle = (): void => {
@@ -132,7 +133,7 @@ function ExplorerOptions({
setIsSaveModalOpen(false); setIsSaveModalOpen(false);
}; };
const { user } = useAppContext(); const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const handleConditionalQueryModification = useCallback((): string => { const handleConditionalQueryModification = useCallback((): string => {
if ( if (
@@ -471,7 +472,7 @@ function ExplorerOptions({
} }
}; };
const isEditDeleteSupported = allowedRoles.includes(user.role as string); const isEditDeleteSupported = allowedRoles.includes(role as string);
const [ const [
isRecentlyUsedSavedViewSelected, isRecentlyUsedSavedViewSelected,
@@ -479,11 +480,6 @@ function ExplorerOptions({
] = useState(false); ] = useState(false);
useEffect(() => { useEffect(() => {
// If the query is not the default query, don't set the recently used saved view
if (!isDefaultQuery({ currentQuery, sourcePage: sourcepage })) {
return;
}
const parsedPreservedView = JSON.parse( const parsedPreservedView = JSON.parse(
localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY) || '{}', localStorage.getItem(PRESERVED_VIEW_LOCAL_STORAGE_KEY) || '{}',
); );
@@ -505,18 +501,12 @@ function ExplorerOptions({
setIsRecentlyUsedSavedViewSelected(false); setIsRecentlyUsedSavedViewSelected(false);
} }
// eslint-disable-next-line consistent-return return (): void => clearTimeout(timeoutId);
return (): void => {
clearTimeout(timeoutId);
};
}, [ }, [
PRESERVED_VIEW_LOCAL_STORAGE_KEY, PRESERVED_VIEW_LOCAL_STORAGE_KEY,
PRESERVED_VIEW_TYPE, PRESERVED_VIEW_TYPE,
currentQuery,
isDefaultQuery,
isRecentlyUsedSavedViewSelected, isRecentlyUsedSavedViewSelected,
onMenuItemSelectHandler, onMenuItemSelectHandler,
sourcepage,
viewKey, viewKey,
viewName, viewName,
viewsData?.data?.data, viewsData?.data?.data,

View File

@@ -11,11 +11,11 @@ import {
SlackChannel, SlackChannel,
WebhookChannel, WebhookChannel,
} from 'container/CreateAlertChannels/config'; } from 'container/CreateAlertChannels/config';
import useFeatureFlags from 'hooks/useFeatureFlag';
import { isFeatureKeys } from 'hooks/useFeatureFlag/utils';
import history from 'lib/history'; import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { Dispatch, ReactElement, SetStateAction } from 'react'; import { Dispatch, ReactElement, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { isFeatureKeys } from 'utils/app';
import EmailSettings from './Settings/Email'; import EmailSettings from './Settings/Email';
import MsTeamsSettings from './Settings/MsTeams'; import MsTeamsSettings from './Settings/MsTeams';
@@ -39,22 +39,16 @@ function FormAlertChannels({
editing = false, editing = false,
}: FormAlertChannelsProps): JSX.Element { }: FormAlertChannelsProps): JSX.Element {
const { t } = useTranslation('channels'); const { t } = useTranslation('channels');
const { featureFlags } = useAppContext(); const isUserOnEEPlan = useFeatureFlags(FeatureKeys.ENTERPRISE_PLAN);
const isUserOnEEPlan =
featureFlags?.find((flag) => flag.name === FeatureKeys.ENTERPRISE_PLAN)
?.active || false;
const feature = `ALERT_CHANNEL_${type.toUpperCase()}`; const feature = `ALERT_CHANNEL_${type.toUpperCase()}`;
const featureKey = isFeatureKeys(feature) const hasFeature = useFeatureFlags(
? feature isFeatureKeys(feature) ? feature : FeatureKeys.ALERT_CHANNEL_SLACK,
: FeatureKeys.ALERT_CHANNEL_SLACK;
const hasFeature = featureFlags?.find((flag) => flag.name === featureKey);
const isOssFeature = featureFlags?.find(
(flag) => flag.name === FeatureKeys.OSS,
); );
const isOssFeature = useFeatureFlags(FeatureKeys.OSS);
const renderSettings = (): ReactElement | null => { const renderSettings = (): ReactElement | null => {
if ( if (
// for ee plan // for ee plan

View File

@@ -8,11 +8,13 @@ import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission'; import useComponentPermission from 'hooks/useComponentPermission';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { AlertTypes } from 'types/api/alerts/alertTypes'; import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertDef, Labels } from 'types/api/alerts/def'; import { AlertDef, Labels } from 'types/api/alerts/def';
import AppReducer from 'types/reducer/app';
import { requireErrorMessage } from 'utils/form/requireErrorMessage'; import { requireErrorMessage } from 'utils/form/requireErrorMessage';
import { popupContainer } from 'utils/selectPopupContainer'; import { popupContainer } from 'utils/selectPopupContainer';
@@ -43,10 +45,10 @@ function BasicInfo({
const { t } = useTranslation('alerts'); const { t } = useTranslation('alerts');
const channels = useFetch(getChannels); const channels = useFetch(getChannels);
const { user } = useAppContext(); const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [addNewChannelPermission] = useComponentPermission( const [addNewChannelPermission] = useComponentPermission(
['add_new_channel'], ['add_new_channel'],
user.role, role,
); );
const [ const [

View File

@@ -3,10 +3,12 @@ import { Select, Spin } from 'antd';
import useComponentPermission from 'hooks/useComponentPermission'; import useComponentPermission from 'hooks/useComponentPermission';
import { State } from 'hooks/useFetch'; import { State } from 'hooks/useFetch';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { useAppContext } from 'providers/App/App';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { PayloadProps } from 'types/api/channels/getAll'; import { PayloadProps } from 'types/api/channels/getAll';
import AppReducer from 'types/reducer/app';
import { StyledCreateChannelOption, StyledSelect } from './styles'; import { StyledCreateChannelOption, StyledSelect } from './styles';
@@ -47,10 +49,10 @@ function ChannelSelect({
}); });
} }
const { user } = useAppContext(); const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const [addNewChannelPermission] = useComponentPermission( const [addNewChannelPermission] = useComponentPermission(
['add_new_channel'], ['add_new_channel'],
user.role, role,
); );
const renderOptions = (): ReactNode[] => { const renderOptions = (): ReactNode[] => {

View File

@@ -18,13 +18,13 @@ import {
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions'; import { useResizeObserver } from 'hooks/useDimensions';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax'; import GetMinMax from 'lib/getMinMax';
import getTimeString from 'lib/getTimeString'; import getTimeString from 'lib/getTimeString';
import history from 'lib/history'; import history from 'lib/history';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useAppContext } from 'providers/App/App';
import { useTimezone } from 'providers/Timezone'; import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -84,8 +84,6 @@ function ChartPreview({
GlobalReducer GlobalReducer
>((state) => state.globalTime); >((state) => state.globalTime);
const { featureFlags } = useAppContext();
const handleBackNavigation = (): void => { const handleBackNavigation = (): void => {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
const startTime = searchParams.get(QueryParams.startTime); const startTime = searchParams.get(QueryParams.startTime);
@@ -272,8 +270,7 @@ function ChartPreview({
chartData && !queryResponse.isError && !queryResponse.isLoading; chartData && !queryResponse.isError && !queryResponse.isLoading;
const isAnomalyDetectionEnabled = const isAnomalyDetectionEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION) useFeatureFlags(FeatureKeys.ANOMALY_DETECTION)?.active || false;
?.active || false;
return ( return (
<div className="alert-chart-container" ref={graphRef}> <div className="alert-chart-container" ref={graphRef}>

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