Compare commits
77 Commits
v0.62.0-cl
...
v0.65.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f6419c2f8 | ||
|
|
421879cf7a | ||
|
|
00abadd429 | ||
|
|
14096f8d53 | ||
|
|
d2aa1cf06e | ||
|
|
838192cf5c | ||
|
|
5dfe245f2d | ||
|
|
53b86e4b5c | ||
|
|
5d9a2571df | ||
|
|
bef6cc945a | ||
|
|
2c2e248c95 | ||
|
|
2f62a9d36d | ||
|
|
04778b9641 | ||
|
|
26fe5e49e7 | ||
|
|
accafbc3ec | ||
|
|
8e7c78e1b1 | ||
|
|
53ebd39f41 | ||
|
|
b36ef944cc | ||
|
|
fa90fad373 | ||
|
|
77420b9d3a | ||
|
|
cecc57e72d | ||
|
|
512adc6471 | ||
|
|
42fefc65be | ||
|
|
dcc659907a | ||
|
|
b90ed375c2 | ||
|
|
a8a3bd3f7d | ||
|
|
7405bfbbee | ||
|
|
67e822e23e | ||
|
|
60dc479a19 | ||
|
|
85cf4f4e2e | ||
|
|
83aa48c721 | ||
|
|
823f84f857 | ||
|
|
8a4d45084d | ||
|
|
5bc6c33899 | ||
|
|
83f6dea2db | ||
|
|
7031c866e8 | ||
|
|
46bc7c7a21 | ||
|
|
6d9741c3a4 | ||
|
|
610a8ec704 | ||
|
|
cd9f27ab08 | ||
|
|
14fbb1fcda | ||
|
|
96da21df05 | ||
|
|
8608f02263 | ||
|
|
2701ae5c34 | ||
|
|
951593b0a3 | ||
|
|
e6766023dd | ||
|
|
bef5b96c5c | ||
|
|
b29359dee0 | ||
|
|
9a1cd65b73 | ||
|
|
8ab0c066d6 | ||
|
|
b333aa3775 | ||
|
|
8a3319cdf5 | ||
|
|
d09c4d947e | ||
|
|
2508e6f9f1 | ||
|
|
1b8213653a | ||
|
|
b499b10333 | ||
|
|
b35b975798 | ||
|
|
715f8a2363 | ||
|
|
8d1c4491b7 | ||
|
|
e3caa6a8f5 | ||
|
|
a1059ed949 | ||
|
|
8c46de8eac | ||
|
|
2b5a0ec496 | ||
|
|
a9440c010c | ||
|
|
f9e7eff357 | ||
|
|
0fbfb6b22b | ||
|
|
b25df66381 | ||
|
|
32fa5a403c | ||
|
|
f9d4cf19e9 | ||
|
|
81775c7d55 | ||
|
|
8d2666004b | ||
|
|
51baf7f8d3 | ||
|
|
31a2926375 | ||
|
|
8c6225185d | ||
|
|
d4458d65ad | ||
|
|
02d8fdb212 | ||
|
|
47d8c9e3e7 |
1
.github/workflows/build.yaml
vendored
1
.github/workflows/build.yaml
vendored
@@ -3,7 +3,6 @@ name: build-pipeline
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
|
||||||
- main
|
- main
|
||||||
- release/v*
|
- release/v*
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -3,7 +3,7 @@ name: "Update PR labels and Block PR until related docs are shipped for the feat
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- main
|
||||||
types: [opened, edited, labeled, unlabeled]
|
types: [opened, edited, labeled, unlabeled]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
2
.github/workflows/e2e-k3s.yaml
vendored
2
.github/workflows/e2e-k3s.yaml
vendored
@@ -42,7 +42,7 @@ jobs:
|
|||||||
kubectl create ns sample-application
|
kubectl create ns sample-application
|
||||||
|
|
||||||
# apply hotrod k8s manifest file
|
# apply hotrod k8s manifest file
|
||||||
kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/develop/sample-apps/hotrod/hotrod.yaml
|
kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/main/sample-apps/hotrod/hotrod.yaml
|
||||||
|
|
||||||
# wait for all deployments in sample-application namespace to be READY
|
# wait for all deployments in sample-application namespace to be READY
|
||||||
kubectl -n sample-application get deploy --output name | xargs -r -n1 -t kubectl -n sample-application rollout status --timeout=300s
|
kubectl -n sample-application get deploy --output name | xargs -r -n1 -t kubectl -n sample-application rollout status --timeout=300s
|
||||||
|
|||||||
5
.github/workflows/jest-coverage-changes.yml
vendored
5
.github/workflows/jest-coverage-changes.yml
vendored
@@ -2,7 +2,8 @@ name: Jest Coverage - changed files
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: develop
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -11,7 +12,7 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: "refs/heads/develop"
|
ref: "refs/heads/main"
|
||||||
token: ${{ secrets.GITHUB_TOKEN }} # Provide the GitHub token for authentication
|
token: ${{ secrets.GITHUB_TOKEN }} # Provide the GitHub token for authentication
|
||||||
|
|
||||||
- name: Fetch branch
|
- name: Fetch branch
|
||||||
|
|||||||
12
.github/workflows/push.yaml
vendored
12
.github/workflows/push.yaml
vendored
@@ -4,7 +4,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- develop
|
|
||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
|
|
||||||
@@ -58,6 +57,17 @@ 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:
|
||||||
|
|||||||
16
.github/workflows/releaser.yaml
vendored
Normal file
16
.github/workflows/releaser.yaml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: releaser
|
||||||
|
|
||||||
|
on:
|
||||||
|
# schedule every wednesday 9:30 AM UTC (3pm IST)
|
||||||
|
schedule:
|
||||||
|
- cron: '30 9 * * 3'
|
||||||
|
|
||||||
|
# allow manual triggering of the workflow by a maintainer with no inputs
|
||||||
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
releaser:
|
||||||
|
uses: signoz/primus.workflows/.github/workflows/releaser-signoz.yaml@main
|
||||||
|
secrets: inherit
|
||||||
|
with:
|
||||||
|
PRIMUS_REF: main
|
||||||
1
.github/workflows/sonar.yml
vendored
1
.github/workflows/sonar.yml
vendored
@@ -3,7 +3,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- develop
|
|
||||||
paths:
|
paths:
|
||||||
- 'frontend/**'
|
- 'frontend/**'
|
||||||
defaults:
|
defaults:
|
||||||
|
|||||||
6
.github/workflows/staging-deployment.yaml
vendored
6
.github/workflows/staging-deployment.yaml
vendored
@@ -1,12 +1,12 @@
|
|||||||
name: staging-deployment
|
name: staging-deployment
|
||||||
# Trigger deployment only on push to develop branch
|
# Trigger deployment only on push to main branch
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- main
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
name: Deploy latest develop branch to staging
|
name: Deploy latest main branch to staging
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: staging
|
environment: staging
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
2
.github/workflows/testing-deployment.yaml
vendored
2
.github/workflows/testing-deployment.yaml
vendored
@@ -44,7 +44,7 @@ jobs:
|
|||||||
git add .
|
git add .
|
||||||
git stash push -m "stashed on $(date --iso-8601=seconds)"
|
git stash push -m "stashed on $(date --iso-8601=seconds)"
|
||||||
git fetch origin
|
git fetch origin
|
||||||
git checkout develop
|
git checkout main
|
||||||
git pull
|
git pull
|
||||||
# This is added to include the scenerio when new commit in PR is force-pushed
|
# This is added to include the scenerio when new commit in PR is force-pushed
|
||||||
git branch -D ${GITHUB_BRANCH}
|
git branch -D ${GITHUB_BRANCH}
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ to make SigNoz UI available at [localhost:3301](http://localhost:3301)
|
|||||||
**5.1.1 To install the HotROD sample app:**
|
**5.1.1 To install the HotROD sample app:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-install.sh \
|
curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-install.sh \
|
||||||
| HELM_RELEASE=my-release SIGNOZ_NAMESPACE=platform bash
|
| HELM_RELEASE=my-release SIGNOZ_NAMESPACE=platform bash
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -362,7 +362,7 @@ kubectl -n sample-application run strzal --image=djbingham/curl \
|
|||||||
**5.1.4 To delete the HotROD sample app:**
|
**5.1.4 To delete the HotROD sample app:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-delete.sh \
|
curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-delete.sh \
|
||||||
| HOTROD_NAMESPACE=sample-application bash
|
| HOTROD_NAMESPACE=sample-application bash
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -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-query-service-static-all: build-query-service-static-amd64 build-query-service-static-arm64 build-frontend-static
|
||||||
|
|
||||||
# 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-query-service-amd64: build-query-service-static-amd64 build-frontend-static
|
||||||
@echo "------------------"
|
@echo "------------------"
|
||||||
@echo "--> Building query-service docker image for amd64"
|
@echo "--> Building query-service docker image for amd64"
|
||||||
@echo "------------------"
|
@echo "------------------"
|
||||||
|
|||||||
11
conf/defaults.yaml
Normal file
11
conf/defaults.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
##################### SigNoz Configuration Defaults #####################
|
||||||
|
#
|
||||||
|
# Do not modify this file
|
||||||
|
#
|
||||||
|
|
||||||
|
##################### Web #####################
|
||||||
|
web:
|
||||||
|
# The prefix to serve web on
|
||||||
|
prefix: /
|
||||||
|
# The directory containing the static build files.
|
||||||
|
directory: /etc/signoz/web
|
||||||
@@ -58,7 +58,7 @@ from the HotROD application, you should see the data generated from hotrod in Si
|
|||||||
```sh
|
```sh
|
||||||
kubectl create ns sample-application
|
kubectl create ns sample-application
|
||||||
|
|
||||||
kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/develop/sample-apps/hotrod/hotrod.yaml
|
kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/main/sample-apps/hotrod/hotrod.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
To generate load:
|
To generate load:
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
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
|
||||||
@@ -16,14 +15,7 @@ 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:
|
test: ["CMD", "wget", "--spider", "-q", "0.0.0.0:8123/ping"]
|
||||||
[
|
|
||||||
"CMD",
|
|
||||||
"wget",
|
|
||||||
"--spider",
|
|
||||||
"-q",
|
|
||||||
"0.0.0.0:8123/ping"
|
|
||||||
]
|
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
@@ -32,15 +24,12 @@ 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
|
||||||
@@ -57,7 +46,6 @@ 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
|
||||||
@@ -89,9 +77,8 @@ 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:
|
||||||
<<: *clickhouse-defaults
|
!!merge <<: *clickhouse-defaults
|
||||||
hostname: clickhouse
|
hostname: clickhouse
|
||||||
# ports:
|
# ports:
|
||||||
# - "9000:9000"
|
# - "9000:9000"
|
||||||
@@ -103,7 +90,6 @@ 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
|
||||||
@@ -131,7 +117,6 @@ 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:
|
||||||
@@ -144,14 +129,9 @@ services:
|
|||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.61.0
|
image: signoz/query-service:0.65.0
|
||||||
command:
|
command: ["-config=/root/config/prometheus.yml", "--use-logs-new-schema=true", "--use-trace-new-schema=true"]
|
||||||
[
|
|
||||||
"-config=/root/config/prometheus.yml",
|
|
||||||
"--use-logs-new-schema=true"
|
|
||||||
]
|
|
||||||
# ports:
|
# ports:
|
||||||
# - "6060:6060" # pprof port
|
# - "6060:6060" # pprof port
|
||||||
# - "8080:8080" # query-service port
|
# - "8080:8080" # query-service port
|
||||||
@@ -169,24 +149,16 @@ services:
|
|||||||
- TELEMETRY_ENABLED=true
|
- TELEMETRY_ENABLED=true
|
||||||
- DEPLOYMENT_TYPE=docker-swarm
|
- DEPLOYMENT_TYPE=docker-swarm
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/health"]
|
||||||
[
|
|
||||||
"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
|
||||||
<<: *db-depend
|
!!merge <<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:0.61.0
|
image: signoz/frontend:0.65.0
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
@@ -197,15 +169,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:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:0.111.14
|
image: signoz/signoz-otel-collector:0.111.16
|
||||||
command:
|
command: ["--config=/etc/otel-collector-config.yaml", "--manager-config=/etc/manager-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
|
||||||
[
|
|
||||||
"--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
|
||||||
@@ -235,22 +201,20 @@ 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.14
|
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:
|
||||||
@@ -263,17 +227,15 @@ 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
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ exporters:
|
|||||||
clickhousetraces:
|
clickhousetraces:
|
||||||
datasource: tcp://clickhouse:9000/signoz_traces
|
datasource: tcp://clickhouse:9000/signoz_traces
|
||||||
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
|
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||||
|
use_new_schema: true
|
||||||
clickhousemetricswrite:
|
clickhousemetricswrite:
|
||||||
endpoint: tcp://clickhouse:9000/signoz_metrics
|
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||||
resource_to_telemetry_conversion:
|
resource_to_telemetry_conversion:
|
||||||
|
|||||||
@@ -69,10 +69,12 @@ services:
|
|||||||
- --storage.path=/data
|
- --storage.path=/data
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.14}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.16}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
|
- "sync"
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
|
- "--up="
|
||||||
depends_on:
|
depends_on:
|
||||||
clickhouse:
|
clickhouse:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -84,7 +86,7 @@ services:
|
|||||||
# 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:0.111.14
|
image: signoz/signoz-otel-collector:0.111.16
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ services:
|
|||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"-config=/root/config/prometheus.yml",
|
"-config=/root/config/prometheus.yml",
|
||||||
"--use-logs-new-schema=true"
|
"--use-logs-new-schema=true",
|
||||||
|
"--use-trace-new-schema=true"
|
||||||
]
|
]
|
||||||
ports:
|
ports:
|
||||||
- "6060:6060"
|
- "6060:6060"
|
||||||
|
|||||||
@@ -13,14 +13,7 @@ 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:
|
test: ["CMD", "wget", "--spider", "-q", "0.0.0.0:8123/ping"]
|
||||||
[
|
|
||||||
"CMD",
|
|
||||||
"wget",
|
|
||||||
"--spider",
|
|
||||||
"-q",
|
|
||||||
"0.0.0.0:8123/ping"
|
|
||||||
]
|
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
@@ -29,20 +22,17 @@ 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
|
||||||
@@ -59,7 +49,6 @@ 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
|
||||||
@@ -93,9 +82,8 @@ 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:
|
||||||
<<: *clickhouse-defaults
|
!!merge <<: *clickhouse-defaults
|
||||||
container_name: signoz-clickhouse
|
container_name: signoz-clickhouse
|
||||||
hostname: clickhouse
|
hostname: clickhouse
|
||||||
ports:
|
ports:
|
||||||
@@ -110,7 +98,6 @@ 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
|
||||||
@@ -128,7 +115,6 @@ 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
|
||||||
@@ -145,7 +131,6 @@ 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
|
||||||
@@ -158,17 +143,11 @@ 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.61.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.65.0}
|
||||||
container_name: signoz-query-service
|
container_name: signoz-query-service
|
||||||
command:
|
command: ["-config=/root/config/prometheus.yml", "--use-logs-new-schema=true", "--use-trace-new-schema=true"]
|
||||||
[
|
|
||||||
"-config=/root/config/prometheus.yml",
|
|
||||||
"--use-logs-new-schema=true"
|
|
||||||
]
|
|
||||||
# ports:
|
# ports:
|
||||||
# - "6060:6060" # pprof port
|
# - "6060:6060" # pprof port
|
||||||
# - "8080:8080" # query-service port
|
# - "8080:8080" # query-service port
|
||||||
@@ -187,21 +166,13 @@ services:
|
|||||||
- DEPLOYMENT_TYPE=docker-standalone-amd
|
- DEPLOYMENT_TYPE=docker-standalone-amd
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/health"]
|
||||||
[
|
|
||||||
"CMD",
|
|
||||||
"wget",
|
|
||||||
"--spider",
|
|
||||||
"-q",
|
|
||||||
"localhost:8080/api/v1/health"
|
|
||||||
]
|
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
<<: *db-depend
|
!!merge <<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.61.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.65.0}
|
||||||
container_name: signoz-frontend
|
container_name: signoz-frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -211,9 +182,8 @@ 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.14}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.16}
|
||||||
container_name: otel-migrator-sync
|
container_name: otel-migrator-sync
|
||||||
command:
|
command:
|
||||||
- "sync"
|
- "sync"
|
||||||
@@ -222,13 +192,12 @@ 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.14}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.16}
|
||||||
container_name: otel-migrator-async
|
container_name: otel-migrator-async
|
||||||
command:
|
command:
|
||||||
- "async"
|
- "async"
|
||||||
@@ -239,21 +208,14 @@ 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.14}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.16}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
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"]
|
||||||
[
|
|
||||||
"--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
|
||||||
@@ -283,7 +245,6 @@ 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
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
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
|
||||||
@@ -18,14 +16,7 @@ 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:
|
test: ["CMD", "wget", "--spider", "-q", "0.0.0.0:8123/ping"]
|
||||||
[
|
|
||||||
"CMD",
|
|
||||||
"wget",
|
|
||||||
"--spider",
|
|
||||||
"-q",
|
|
||||||
"0.0.0.0:8123/ping"
|
|
||||||
]
|
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
@@ -34,20 +25,17 @@ 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
|
||||||
@@ -64,7 +52,6 @@ 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
|
||||||
@@ -98,9 +85,8 @@ 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:
|
||||||
<<: *clickhouse-defaults
|
!!merge <<: *clickhouse-defaults
|
||||||
container_name: signoz-clickhouse
|
container_name: signoz-clickhouse
|
||||||
hostname: clickhouse
|
hostname: clickhouse
|
||||||
ports:
|
ports:
|
||||||
@@ -115,7 +101,6 @@ 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
|
||||||
@@ -133,7 +118,6 @@ 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
|
||||||
@@ -150,7 +134,6 @@ 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
|
||||||
@@ -163,18 +146,11 @@ 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.61.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.65.0}
|
||||||
container_name: signoz-query-service
|
container_name: signoz-query-service
|
||||||
command:
|
command: ["-config=/root/config/prometheus.yml", "-gateway-url=https://api.staging.signoz.cloud", "--use-logs-new-schema=true", "--use-trace-new-schema=true"]
|
||||||
[
|
|
||||||
"-config=/root/config/prometheus.yml",
|
|
||||||
"-gateway-url=https://api.staging.signoz.cloud",
|
|
||||||
"--use-logs-new-schema=true"
|
|
||||||
]
|
|
||||||
# ports:
|
# ports:
|
||||||
# - "6060:6060" # pprof port
|
# - "6060:6060" # pprof port
|
||||||
# - "8080:8080" # query-service port
|
# - "8080:8080" # query-service port
|
||||||
@@ -194,21 +170,13 @@ services:
|
|||||||
- KAFKA_SPAN_EVAL=${KAFKA_SPAN_EVAL:-false}
|
- KAFKA_SPAN_EVAL=${KAFKA_SPAN_EVAL:-false}
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/health"]
|
||||||
[
|
|
||||||
"CMD",
|
|
||||||
"wget",
|
|
||||||
"--spider",
|
|
||||||
"-q",
|
|
||||||
"localhost:8080/api/v1/health"
|
|
||||||
]
|
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
<<: *db-depend
|
!!merge <<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.61.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.65.0}
|
||||||
container_name: signoz-frontend
|
container_name: signoz-frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -218,31 +186,22 @@ 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.14}
|
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.14}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.16}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
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"]
|
||||||
[
|
|
||||||
"--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
|
||||||
@@ -272,7 +231,6 @@ 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
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ exporters:
|
|||||||
clickhousetraces:
|
clickhousetraces:
|
||||||
datasource: tcp://clickhouse:9000/signoz_traces
|
datasource: tcp://clickhouse:9000/signoz_traces
|
||||||
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
|
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
|
||||||
|
use_new_schema: true
|
||||||
clickhousemetricswrite:
|
clickhousemetricswrite:
|
||||||
endpoint: tcp://clickhouse:9000/signoz_metrics
|
endpoint: tcp://clickhouse:9000/signoz_metrics
|
||||||
resource_to_telemetry_conversion:
|
resource_to_telemetry_conversion:
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ 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"]
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ type APIHandlerOptions struct {
|
|||||||
FluxInterval time.Duration
|
FluxInterval time.Duration
|
||||||
UseLogsNewSchema bool
|
UseLogsNewSchema bool
|
||||||
UseTraceNewSchema bool
|
UseTraceNewSchema bool
|
||||||
UseLicensesV3 bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type APIHandler struct {
|
type APIHandler struct {
|
||||||
@@ -68,7 +67,6 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
|
|||||||
FluxInterval: opts.FluxInterval,
|
FluxInterval: opts.FluxInterval,
|
||||||
UseLogsNewSchema: opts.UseLogsNewSchema,
|
UseLogsNewSchema: opts.UseLogsNewSchema,
|
||||||
UseTraceNewSchema: opts.UseTraceNewSchema,
|
UseTraceNewSchema: opts.UseTraceNewSchema,
|
||||||
UseLicensesV3: opts.UseLicensesV3,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -84,13 +84,6 @@ func (ah *APIHandler) listLicenses(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
|
func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
|
||||||
if ah.UseLicensesV3 {
|
|
||||||
// if the licenses v3 is toggled on then do not apply license in v2 and run the validator!
|
|
||||||
// TODO: remove after migration to v3 and deprecation from zeus
|
|
||||||
zap.L().Info("early return from apply license v2 call")
|
|
||||||
render.Success(w, http.StatusOK, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var l model.License
|
var l model.License
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&l); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&l); err != nil {
|
||||||
@@ -102,7 +95,7 @@ func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
|
|||||||
RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
|
RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
license, apiError := ah.LM().Activate(r.Context(), l.Key)
|
license, apiError := ah.LM().ActivateV3(r.Context(), l.Key)
|
||||||
if apiError != nil {
|
if apiError != nil {
|
||||||
RespondError(w, apiError, nil)
|
RespondError(w, apiError, nil)
|
||||||
return
|
return
|
||||||
@@ -265,24 +258,12 @@ func convertLicenseV3ToLicenseV2(licenses []*model.LicenseV3) []model.License {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
|
func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
|
||||||
|
licensesV3, apierr := ah.LM().GetLicensesV3(r.Context())
|
||||||
var licenses []model.License
|
if apierr != nil {
|
||||||
|
RespondError(w, apierr, nil)
|
||||||
if ah.UseLicensesV3 {
|
return
|
||||||
licensesV3, err := ah.LM().GetLicensesV3(r.Context())
|
|
||||||
if err != nil {
|
|
||||||
RespondError(w, err, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
licenses = convertLicenseV3ToLicenseV2(licensesV3)
|
|
||||||
} else {
|
|
||||||
_licenses, apiError := ah.LM().GetLicenses(r.Context())
|
|
||||||
if apiError != nil {
|
|
||||||
RespondError(w, apiError, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
licenses = _licenses
|
|
||||||
}
|
}
|
||||||
|
licenses := convertLicenseV3ToLicenseV2(licensesV3)
|
||||||
|
|
||||||
resp := model.Licenses{
|
resp := model.Licenses{
|
||||||
TrialStart: -1,
|
TrialStart: -1,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import (
|
|||||||
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"
|
"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/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"
|
||||||
@@ -78,7 +79,6 @@ type ServerOptions struct {
|
|||||||
GatewayUrl string
|
GatewayUrl string
|
||||||
UseLogsNewSchema bool
|
UseLogsNewSchema bool
|
||||||
UseTraceNewSchema bool
|
UseTraceNewSchema bool
|
||||||
UseLicensesV3 bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server runs HTTP api service
|
// Server runs HTTP api service
|
||||||
@@ -108,7 +108,7 @@ 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, web *web.Web) (*Server, error) {
|
||||||
|
|
||||||
modelDao, err := dao.InitDao("sqlite", baseconst.RELATIONAL_DATASOURCE_PATH)
|
modelDao, err := dao.InitDao("sqlite", baseconst.RELATIONAL_DATASOURCE_PATH)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -135,7 +135,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// initiate license manager
|
// initiate license manager
|
||||||
lm, err := licensepkg.StartManager("sqlite", localDB, serverOptions.UseLicensesV3)
|
lm, err := licensepkg.StartManager("sqlite", localDB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -274,7 +274,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
Gateway: gatewayProxy,
|
Gateway: gatewayProxy,
|
||||||
UseLogsNewSchema: serverOptions.UseLogsNewSchema,
|
UseLogsNewSchema: serverOptions.UseLogsNewSchema,
|
||||||
UseTraceNewSchema: serverOptions.UseTraceNewSchema,
|
UseTraceNewSchema: serverOptions.UseTraceNewSchema,
|
||||||
UseLicensesV3: serverOptions.UseLicensesV3,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apiHandler, err := api.NewAPIHandler(apiOpts)
|
apiHandler, err := api.NewAPIHandler(apiOpts)
|
||||||
@@ -291,7 +290,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
usageManager: usageManager,
|
usageManager: usageManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
httpServer, err := s.createPublicServer(apiHandler)
|
httpServer, err := s.createPublicServer(apiHandler, web)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -340,7 +339,7 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, error) {
|
func (s *Server) createPublicServer(apiHandler *api.APIHandler, web *web.Web) (*http.Server, error) {
|
||||||
|
|
||||||
r := baseapp.NewRouter()
|
r := baseapp.NewRouter()
|
||||||
|
|
||||||
@@ -384,6 +383,11 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
|
|||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -2,18 +2,6 @@ package signozio
|
|||||||
|
|
||||||
type status string
|
type status string
|
||||||
|
|
||||||
type ActivationResult struct {
|
|
||||||
Status status `json:"status"`
|
|
||||||
Data *ActivationResponse `json:"data,omitempty"`
|
|
||||||
ErrorType string `json:"errorType,omitempty"`
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ActivationResponse struct {
|
|
||||||
ActivationId string `json:"ActivationId"`
|
|
||||||
PlanDetails string `json:"PlanDetails"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ValidateLicenseResponse struct {
|
type ValidateLicenseResponse struct {
|
||||||
Status status `json:"status"`
|
Status status `json:"status"`
|
||||||
Data map[string]interface{} `json:"data"`
|
Data map[string]interface{} `json:"data"`
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"go.signoz.io/signoz/ee/query-service/constants"
|
"go.signoz.io/signoz/ee/query-service/constants"
|
||||||
"go.signoz.io/signoz/ee/query-service/model"
|
"go.signoz.io/signoz/ee/query-service/model"
|
||||||
@@ -39,86 +38,6 @@ func init() {
|
|||||||
C = New()
|
C = New()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActivateLicense sends key to license.signoz.io and gets activation data
|
|
||||||
func ActivateLicense(key, siteId string) (*ActivationResponse, *model.ApiError) {
|
|
||||||
licenseReq := map[string]string{
|
|
||||||
"key": key,
|
|
||||||
"siteId": siteId,
|
|
||||||
}
|
|
||||||
|
|
||||||
reqString, _ := json.Marshal(licenseReq)
|
|
||||||
httpResponse, err := http.Post(C.Prefix+"/licenses/activate", APPLICATION_JSON, bytes.NewBuffer(reqString))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("failed to connect to license.signoz.io", zap.Error(err))
|
|
||||||
return nil, model.BadRequest(fmt.Errorf("unable to connect with license.signoz.io, please check your network connection"))
|
|
||||||
}
|
|
||||||
|
|
||||||
httpBody, err := io.ReadAll(httpResponse.Body)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("failed to read activation response from license.signoz.io", zap.Error(err))
|
|
||||||
return nil, model.BadRequest(fmt.Errorf("failed to read activation response from license.signoz.io"))
|
|
||||||
}
|
|
||||||
|
|
||||||
defer httpResponse.Body.Close()
|
|
||||||
|
|
||||||
// read api request result
|
|
||||||
result := ActivationResult{}
|
|
||||||
err = json.Unmarshal(httpBody, &result)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("failed to marshal activation response from license.signoz.io", zap.Error(err))
|
|
||||||
return nil, model.InternalError(errors.Wrap(err, "failed to marshal license activation response"))
|
|
||||||
}
|
|
||||||
|
|
||||||
switch httpResponse.StatusCode {
|
|
||||||
case 200, 201:
|
|
||||||
return result.Data, nil
|
|
||||||
case 400, 401:
|
|
||||||
return nil, model.BadRequest(fmt.Errorf(fmt.Sprintf("failed to activate: %s", result.Error)))
|
|
||||||
default:
|
|
||||||
return nil, model.InternalError(fmt.Errorf(fmt.Sprintf("failed to activate: %s", result.Error)))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateLicense validates the license key
|
|
||||||
func ValidateLicense(activationId string) (*ActivationResponse, *model.ApiError) {
|
|
||||||
validReq := map[string]string{
|
|
||||||
"activationId": activationId,
|
|
||||||
}
|
|
||||||
|
|
||||||
reqString, _ := json.Marshal(validReq)
|
|
||||||
response, err := http.Post(C.Prefix+"/licenses/validate", APPLICATION_JSON, bytes.NewBuffer(reqString))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, model.BadRequest(errors.Wrap(err, "unable to connect with license.signoz.io, please check your network connection"))
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, model.BadRequest(errors.Wrap(err, "failed to read validation response from license.signoz.io"))
|
|
||||||
}
|
|
||||||
|
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
switch response.StatusCode {
|
|
||||||
case 200, 201:
|
|
||||||
a := ActivationResult{}
|
|
||||||
err = json.Unmarshal(body, &a)
|
|
||||||
if err != nil {
|
|
||||||
return nil, model.BadRequest(errors.Wrap(err, "failed to marshal license validation response"))
|
|
||||||
}
|
|
||||||
return a.Data, nil
|
|
||||||
case 400, 401:
|
|
||||||
return nil, model.BadRequest(errors.Wrap(fmt.Errorf(string(body)),
|
|
||||||
"bad request error received from license.signoz.io"))
|
|
||||||
default:
|
|
||||||
return nil, model.InternalError(errors.Wrap(fmt.Errorf(string(body)),
|
|
||||||
"internal error received from license.signoz.io"))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func ValidateLicenseV3(licenseKey string) (*model.LicenseV3, *model.ApiError) {
|
func ValidateLicenseV3(licenseKey string) (*model.LicenseV3, *model.ApiError) {
|
||||||
|
|
||||||
// Creating an HTTP client with a timeout for better control
|
// Creating an HTTP client with a timeout for better control
|
||||||
|
|||||||
@@ -18,15 +18,13 @@ import (
|
|||||||
|
|
||||||
// Repo is license repo. stores license keys in a secured DB
|
// Repo is license repo. stores license keys in a secured DB
|
||||||
type Repo struct {
|
type Repo struct {
|
||||||
db *sqlx.DB
|
db *sqlx.DB
|
||||||
useLicensesV3 bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLicenseRepo initiates a new license repo
|
// NewLicenseRepo initiates a new license repo
|
||||||
func NewLicenseRepo(db *sqlx.DB, useLicensesV3 bool) Repo {
|
func NewLicenseRepo(db *sqlx.DB) Repo {
|
||||||
return Repo{
|
return Repo{
|
||||||
db: db,
|
db: db,
|
||||||
useLicensesV3: useLicensesV3,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,26 +110,16 @@ func (r *Repo) GetActiveLicenseV2(ctx context.Context) (*model.License, *basemod
|
|||||||
// GetActiveLicense fetches the latest active license from DB.
|
// GetActiveLicense fetches the latest active license from DB.
|
||||||
// If the license is not present, expect a nil license and a nil error in the output.
|
// If the license is not present, expect a nil license and a nil error in the output.
|
||||||
func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel.ApiError) {
|
func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel.ApiError) {
|
||||||
if r.useLicensesV3 {
|
activeLicenseV3, err := r.GetActiveLicenseV3(ctx)
|
||||||
zap.L().Info("Using licenses v3 for GetActiveLicense")
|
|
||||||
activeLicenseV3, err := r.GetActiveLicenseV3(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if activeLicenseV3 == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
activeLicenseV2 := model.ConvertLicenseV3ToLicenseV2(activeLicenseV3)
|
|
||||||
return activeLicenseV2, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
active, err := r.GetActiveLicenseV2(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
|
||||||
}
|
}
|
||||||
return active, nil
|
|
||||||
|
if activeLicenseV3 == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
activeLicenseV2 := model.ConvertLicenseV3ToLicenseV2(activeLicenseV3)
|
||||||
|
return activeLicenseV2, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error) {
|
func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error) {
|
||||||
|
|||||||
@@ -51,12 +51,12 @@ type Manager struct {
|
|||||||
activeFeatures basemodel.FeatureSet
|
activeFeatures basemodel.FeatureSet
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartManager(dbType string, db *sqlx.DB, useLicensesV3 bool, 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, useLicensesV3)
|
repo := NewLicenseRepo(db)
|
||||||
err := repo.InitDB(dbType)
|
err := repo.InitDB(dbType)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -67,32 +67,7 @@ func StartManager(dbType string, db *sqlx.DB, useLicensesV3 bool, features ...ba
|
|||||||
repo: &repo,
|
repo: &repo,
|
||||||
}
|
}
|
||||||
|
|
||||||
if useLicensesV3 {
|
if err := m.start(features...); err != nil {
|
||||||
// get active license from the db
|
|
||||||
active, err := m.repo.GetActiveLicenseV2(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return m, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we have an active license then need to fetch the complete details
|
|
||||||
if active != nil {
|
|
||||||
// fetch the new license structure from control plane
|
|
||||||
licenseV3, apiError := validate.ValidateLicenseV3(active.Key)
|
|
||||||
if apiError != nil {
|
|
||||||
return m, apiError
|
|
||||||
}
|
|
||||||
|
|
||||||
// insert the licenseV3 in sqlite db
|
|
||||||
apiError = m.repo.InsertLicenseV3(context.Background(), licenseV3)
|
|
||||||
// if the license already exists move ahead.
|
|
||||||
if apiError != nil && apiError.Typ != model.ErrorConflict {
|
|
||||||
return m, apiError
|
|
||||||
}
|
|
||||||
zap.L().Info("Successfully inserted license from v2 to v3 table")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.start(useLicensesV3, features...); err != nil {
|
|
||||||
return m, err
|
return m, err
|
||||||
}
|
}
|
||||||
LM = m
|
LM = m
|
||||||
@@ -100,16 +75,8 @@ func StartManager(dbType string, db *sqlx.DB, useLicensesV3 bool, features ...ba
|
|||||||
}
|
}
|
||||||
|
|
||||||
// start loads active license in memory and initiates validator
|
// start loads active license in memory and initiates validator
|
||||||
func (lm *Manager) start(useLicensesV3 bool, features ...basemodel.Feature) error {
|
func (lm *Manager) start(features ...basemodel.Feature) error {
|
||||||
|
return lm.LoadActiveLicenseV3(features...)
|
||||||
var err error
|
|
||||||
if useLicensesV3 {
|
|
||||||
err = lm.LoadActiveLicenseV3(features...)
|
|
||||||
} else {
|
|
||||||
err = lm.LoadActiveLicense(features...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lm *Manager) Stop() {
|
func (lm *Manager) Stop() {
|
||||||
@@ -117,31 +84,6 @@ func (lm *Manager) Stop() {
|
|||||||
<-lm.terminated
|
<-lm.terminated
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lm *Manager) SetActive(l *model.License, features ...basemodel.Feature) {
|
|
||||||
lm.mutex.Lock()
|
|
||||||
defer lm.mutex.Unlock()
|
|
||||||
|
|
||||||
if l == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lm.activeLicense = l
|
|
||||||
lm.activeFeatures = append(l.FeatureSet, features...)
|
|
||||||
// set default features
|
|
||||||
setDefaultFeatures(lm)
|
|
||||||
|
|
||||||
err := lm.InitFeatures(lm.activeFeatures)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Panic("Couldn't activate features", zap.Error(err))
|
|
||||||
}
|
|
||||||
if !lm.validatorRunning {
|
|
||||||
// we want to make sure only one validator runs,
|
|
||||||
// we already have lock() so good to go
|
|
||||||
lm.validatorRunning = true
|
|
||||||
go lm.Validator(context.Background())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
func (lm *Manager) SetActiveV3(l *model.LicenseV3, features ...basemodel.Feature) {
|
func (lm *Manager) SetActiveV3(l *model.LicenseV3, features ...basemodel.Feature) {
|
||||||
lm.mutex.Lock()
|
lm.mutex.Lock()
|
||||||
defer lm.mutex.Unlock()
|
defer lm.mutex.Unlock()
|
||||||
@@ -172,29 +114,6 @@ func setDefaultFeatures(lm *Manager) {
|
|||||||
lm.activeFeatures = append(lm.activeFeatures, baseconstants.DEFAULT_FEATURE_SET...)
|
lm.activeFeatures = append(lm.activeFeatures, baseconstants.DEFAULT_FEATURE_SET...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadActiveLicense loads the most recent active license
|
|
||||||
func (lm *Manager) LoadActiveLicense(features ...basemodel.Feature) error {
|
|
||||||
active, err := lm.repo.GetActiveLicense(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if active != nil {
|
|
||||||
lm.SetActive(active, features...)
|
|
||||||
} else {
|
|
||||||
zap.L().Info("No active license found, defaulting to basic plan")
|
|
||||||
// if no active license is found, we default to basic(free) plan with all default features
|
|
||||||
lm.activeFeatures = model.BasicPlan
|
|
||||||
setDefaultFeatures(lm)
|
|
||||||
err := lm.InitFeatures(lm.activeFeatures)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("Couldn't initialize features", zap.Error(err))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error {
|
func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error {
|
||||||
active, err := lm.repo.GetActiveLicenseV3(context.Background())
|
active, err := lm.repo.GetActiveLicenseV3(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -265,31 +184,6 @@ func (lm *Manager) GetLicensesV3(ctx context.Context) (response []*model.License
|
|||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validator validates license after an epoch of time
|
|
||||||
func (lm *Manager) Validator(ctx context.Context) {
|
|
||||||
zap.L().Info("Validator started!")
|
|
||||||
defer close(lm.terminated)
|
|
||||||
tick := time.NewTicker(validationFrequency)
|
|
||||||
defer tick.Stop()
|
|
||||||
|
|
||||||
lm.Validate(ctx)
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-lm.done:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
select {
|
|
||||||
case <-lm.done:
|
|
||||||
return
|
|
||||||
case <-tick.C:
|
|
||||||
lm.Validate(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validator validates license after an epoch of time
|
// Validator validates license after an epoch of time
|
||||||
func (lm *Manager) ValidatorV3(ctx context.Context) {
|
func (lm *Manager) ValidatorV3(ctx context.Context) {
|
||||||
zap.L().Info("ValidatorV3 started!")
|
zap.L().Info("ValidatorV3 started!")
|
||||||
@@ -315,73 +209,6 @@ func (lm *Manager) ValidatorV3(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the current active license
|
|
||||||
func (lm *Manager) Validate(ctx context.Context) (reterr error) {
|
|
||||||
zap.L().Info("License validation started")
|
|
||||||
if lm.activeLicense == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
lm.mutex.Lock()
|
|
||||||
|
|
||||||
lm.lastValidated = time.Now().Unix()
|
|
||||||
if reterr != nil {
|
|
||||||
zap.L().Error("License validation completed with error", zap.Error(reterr))
|
|
||||||
atomic.AddUint64(&lm.failedAttempts, 1)
|
|
||||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED,
|
|
||||||
map[string]interface{}{"err": reterr.Error()}, "", true, false)
|
|
||||||
} else {
|
|
||||||
zap.L().Info("License validation completed with no errors")
|
|
||||||
}
|
|
||||||
|
|
||||||
lm.mutex.Unlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
response, apiError := validate.ValidateLicense(lm.activeLicense.ActivationId)
|
|
||||||
if apiError != nil {
|
|
||||||
zap.L().Error("failed to validate license", zap.Error(apiError.Err))
|
|
||||||
return apiError.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.PlanDetails == lm.activeLicense.PlanDetails {
|
|
||||||
// license plan hasnt changed, nothing to do
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.PlanDetails != "" {
|
|
||||||
|
|
||||||
// copy and replace the active license record
|
|
||||||
l := model.License{
|
|
||||||
Key: lm.activeLicense.Key,
|
|
||||||
CreatedAt: lm.activeLicense.CreatedAt,
|
|
||||||
PlanDetails: response.PlanDetails,
|
|
||||||
ValidationMessage: lm.activeLicense.ValidationMessage,
|
|
||||||
ActivationId: lm.activeLicense.ActivationId,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := l.ParsePlan(); err != nil {
|
|
||||||
zap.L().Error("failed to parse updated license", zap.Error(err))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// updated plan is parsable, check if plan has changed
|
|
||||||
if lm.activeLicense.PlanDetails != response.PlanDetails {
|
|
||||||
err := lm.repo.UpdatePlanDetails(ctx, lm.activeLicense.Key, response.PlanDetails)
|
|
||||||
if err != nil {
|
|
||||||
// unexpected db write issue but we can let the user continue
|
|
||||||
// and wait for update to work in next cycle.
|
|
||||||
zap.L().Error("failed to validate license", zap.Error(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// activate the update license plan
|
|
||||||
lm.SetActive(&l)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lm *Manager) RefreshLicense(ctx context.Context) *model.ApiError {
|
func (lm *Manager) RefreshLicense(ctx context.Context) *model.ApiError {
|
||||||
|
|
||||||
license, apiError := validate.ValidateLicenseV3(lm.activeLicenseV3.Key)
|
license, apiError := validate.ValidateLicenseV3(lm.activeLicenseV3.Key)
|
||||||
@@ -429,50 +256,6 @@ func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activate activates a license key with signoz server
|
|
||||||
func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *model.License, errResponse *model.ApiError) {
|
|
||||||
defer func() {
|
|
||||||
if errResponse != nil {
|
|
||||||
userEmail, err := auth.GetEmailFromJwt(ctx)
|
|
||||||
if err == nil {
|
|
||||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED,
|
|
||||||
map[string]interface{}{"err": errResponse.Err.Error()}, userEmail, true, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
response, apiError := validate.ActivateLicense(key, "")
|
|
||||||
if apiError != nil {
|
|
||||||
zap.L().Error("failed to activate license", zap.Error(apiError.Err))
|
|
||||||
return nil, apiError
|
|
||||||
}
|
|
||||||
|
|
||||||
l := &model.License{
|
|
||||||
Key: key,
|
|
||||||
ActivationId: response.ActivationId,
|
|
||||||
PlanDetails: response.PlanDetails,
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse validity and features from the plan details
|
|
||||||
err := l.ParsePlan()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("failed to activate license", zap.Error(err))
|
|
||||||
return nil, model.InternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// store the license before activating it
|
|
||||||
err = lm.repo.InsertLicense(ctx, l)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("failed to activate license", zap.Error(err))
|
|
||||||
return nil, model.InternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// license is valid, activate it
|
|
||||||
lm.SetActive(l)
|
|
||||||
return l, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (licenseResponse *model.LicenseV3, errResponse *model.ApiError) {
|
func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (licenseResponse *model.LicenseV3, errResponse *model.ApiError) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if errResponse != nil {
|
if errResponse != nil {
|
||||||
|
|||||||
@@ -10,13 +10,17 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/collector/confmap"
|
||||||
"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"
|
||||||
|
signozconfig "go.signoz.io/signoz/pkg/config"
|
||||||
|
"go.signoz.io/signoz/pkg/confmap/provider/signozenvprovider"
|
||||||
"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"
|
||||||
|
signozweb "go.signoz.io/signoz/pkg/web"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
|
||||||
@@ -95,7 +99,6 @@ func main() {
|
|||||||
|
|
||||||
var useLogsNewSchema bool
|
var useLogsNewSchema bool
|
||||||
var useTraceNewSchema bool
|
var useTraceNewSchema bool
|
||||||
var useLicensesV3 bool
|
|
||||||
var cacheConfigPath, fluxInterval string
|
var cacheConfigPath, fluxInterval string
|
||||||
var enableQueryServiceLogOTLPExport bool
|
var enableQueryServiceLogOTLPExport bool
|
||||||
var preferSpanMetrics bool
|
var preferSpanMetrics bool
|
||||||
@@ -104,10 +107,10 @@ func main() {
|
|||||||
var maxOpenConns int
|
var maxOpenConns int
|
||||||
var dialTimeout time.Duration
|
var dialTimeout time.Duration
|
||||||
var gatewayUrl string
|
var gatewayUrl string
|
||||||
|
var useLicensesV3 bool
|
||||||
|
|
||||||
flag.BoolVar(&useLogsNewSchema, "use-logs-new-schema", false, "use logs_v2 schema for logs")
|
flag.BoolVar(&useLogsNewSchema, "use-logs-new-schema", false, "use logs_v2 schema for logs")
|
||||||
flag.BoolVar(&useTraceNewSchema, "use-trace-new-schema", false, "use new schema for traces")
|
flag.BoolVar(&useTraceNewSchema, "use-trace-new-schema", false, "use new schema for traces")
|
||||||
flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses")
|
|
||||||
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
|
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
|
||||||
flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)")
|
flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)")
|
||||||
flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
|
flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
|
||||||
@@ -121,6 +124,7 @@ func main() {
|
|||||||
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.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@@ -131,6 +135,23 @@ func main() {
|
|||||||
|
|
||||||
version.PrintVersion()
|
version.PrintVersion()
|
||||||
|
|
||||||
|
config, err := signozconfig.New(context.Background(), signozconfig.ProviderSettings{
|
||||||
|
ResolverSettings: confmap.ResolverSettings{
|
||||||
|
URIs: []string{"signozenv:"},
|
||||||
|
ProviderFactories: []confmap.ProviderFactory{
|
||||||
|
signozenvprovider.NewFactory(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Fatal("Failed to create config", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
web, err := signozweb.New(zap.L(), config.Web)
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Fatal("Failed to create web", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
serverOptions := &app.ServerOptions{
|
serverOptions := &app.ServerOptions{
|
||||||
HTTPHostPort: baseconst.HTTPHostPort,
|
HTTPHostPort: baseconst.HTTPHostPort,
|
||||||
PromConfigPath: promConfigPath,
|
PromConfigPath: promConfigPath,
|
||||||
@@ -148,7 +169,6 @@ func main() {
|
|||||||
GatewayUrl: gatewayUrl,
|
GatewayUrl: gatewayUrl,
|
||||||
UseLogsNewSchema: useLogsNewSchema,
|
UseLogsNewSchema: useLogsNewSchema,
|
||||||
UseTraceNewSchema: useTraceNewSchema,
|
UseTraceNewSchema: useTraceNewSchema,
|
||||||
UseLicensesV3: useLicensesV3,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the jwt secret key
|
// Read the jwt secret key
|
||||||
@@ -166,7 +186,7 @@ func main() {
|
|||||||
zap.L().Info("Migration successful")
|
zap.L().Info("Migration successful")
|
||||||
}
|
}
|
||||||
|
|
||||||
server, err := app.NewServer(serverOptions)
|
server, err := app.NewServer(serverOptions, web)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Fatal("Failed to create server", zap.Error(err))
|
zap.L().Fatal("Failed to create server", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,3 @@ if [ "$branch" = "main" ]; then
|
|||||||
echo "${color_red}${bold}You can't commit directly to the main branch${reset}"
|
echo "${color_red}${bold}You can't commit directly to the main branch${reset}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$branch" = "develop" ]; then
|
|
||||||
echo "${color_red}${bold}You can't commit directly to the develop branch${reset}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -242,6 +242,7 @@
|
|||||||
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"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": "Access Tokens",
|
"api_keys": "API Keys",
|
||||||
"my_settings": "My Settings",
|
"my_settings": "My Settings",
|
||||||
"overview_metrics": "Overview Metrics",
|
"overview_metrics": "Overview Metrics",
|
||||||
"dbcall_metrics": "Database Calls",
|
"dbcall_metrics": "Database Calls",
|
||||||
|
|||||||
@@ -26,7 +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 | Access Tokens",
|
"API_KEYS": "SigNoz | API Keys",
|
||||||
"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",
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"delete_confirm_message": "Are you sure you want to delete {{keyName}} token? Deleting a token is irreversible and cannot be undone."
|
"delete_confirm_message": "Are you sure you want to delete {{keyName}} key? Deleting a key is irreversible and cannot be undone."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"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": "Access Tokens",
|
"api_keys": "API Keys",
|
||||||
"my_settings": "My Settings",
|
"my_settings": "My Settings",
|
||||||
"overview_metrics": "Overview Metrics",
|
"overview_metrics": "Overview Metrics",
|
||||||
"dbcall_metrics": "Database Calls",
|
"dbcall_metrics": "Database Calls",
|
||||||
|
|||||||
@@ -32,7 +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 | Access Tokens",
|
"API_KEYS": "SigNoz | API Keys",
|
||||||
"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",
|
||||||
|
|||||||
@@ -1,29 +1,16 @@
|
|||||||
/* 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, isNull } from 'lodash-es';
|
import { isEmpty } from 'lodash-es';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { ReactChild, useEffect, useMemo, useState } from 'react';
|
import { ReactChild, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { matchPath, useLocation } from 'react-router-dom';
|
||||||
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 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';
|
||||||
|
|
||||||
@@ -31,32 +18,28 @@ 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,
|
||||||
} = useSelector<AppState, AppReducer>((state) => state.app);
|
licenses,
|
||||||
|
isFetchingLicenses,
|
||||||
const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext();
|
activeLicenseV3,
|
||||||
|
isFetchingActiveLicenseV3,
|
||||||
|
} = useAppContext();
|
||||||
const mapRoutes = useMemo(
|
const mapRoutes = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new Map(
|
new Map(
|
||||||
[...routes, LIST_LICENSES].map((e) => {
|
[...routes, LIST_LICENSES, SUPPORT_ROUTE].map((e) => {
|
||||||
const currentPath = matchPath(pathname, {
|
const currentPath = matchPath(pathname, {
|
||||||
path: e.path,
|
path: e.path,
|
||||||
});
|
});
|
||||||
@@ -65,52 +48,13 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
),
|
),
|
||||||
[pathname],
|
[pathname],
|
||||||
);
|
);
|
||||||
|
|
||||||
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 localStorageUserAuthToken = getInitialUserTokenRefreshToken();
|
|
||||||
|
|
||||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
|
||||||
|
|
||||||
const currentRoute = mapRoutes.get('current');
|
|
||||||
|
|
||||||
const isOldRoute = oldRoutes.indexOf(pathname) > -1;
|
const isOldRoute = oldRoutes.indexOf(pathname) > -1;
|
||||||
|
const currentRoute = mapRoutes.get('current');
|
||||||
|
const isCloudUserVal = isCloudUser();
|
||||||
|
|
||||||
const [orgData, setOrgData] = useState<Organization | undefined>(undefined);
|
const [orgData, setOrgData] = useState<Organization | undefined>(undefined);
|
||||||
|
|
||||||
const isLocalStorageLoggedIn =
|
const { data: orgUsers, isFetching: isFetchingOrgUsers } = useQuery({
|
||||||
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({
|
||||||
@@ -120,10 +64,10 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
return undefined;
|
return undefined;
|
||||||
},
|
},
|
||||||
queryKey: ['getOrgUser'],
|
queryKey: ['getOrgUser'],
|
||||||
enabled: !isEmpty(orgData),
|
enabled: !isEmpty(orgData) && user.role === 'ADMIN',
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkFirstTimeUser = (): boolean => {
|
const checkFirstTimeUser = useCallback((): boolean => {
|
||||||
const users = orgUsers?.payload || [];
|
const users = orgUsers?.payload || [];
|
||||||
|
|
||||||
const remainingUsers = users.filter(
|
const remainingUsers = users.filter(
|
||||||
@@ -131,154 +75,80 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return remainingUsers.length === 1;
|
return remainingUsers.length === 1;
|
||||||
};
|
}, [orgUsers?.payload]);
|
||||||
|
|
||||||
// Check if the onboarding should be shown based on the org users and onboarding completion status, wait for org users and preferences to load
|
useEffect(() => {
|
||||||
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 &&
|
||||||
!isLoadingOrgUsers &&
|
orgPreferences &&
|
||||||
!isEmpty(orgUsers?.payload) &&
|
!isFetchingOrgUsers &&
|
||||||
!isNull(orgPreferences)
|
orgUsers &&
|
||||||
|
orgUsers.payload
|
||||||
) {
|
) {
|
||||||
if (key === 'ONBOARDING' && isOnboardingComplete) {
|
const isOnboardingComplete = orgPreferences?.find(
|
||||||
history.push(ROUTES.APPLICATION);
|
(preference: Record<string, any>) => preference.key === 'ORG_ONBOARDING',
|
||||||
}
|
)?.value;
|
||||||
|
|
||||||
const isFirstTimeUser = checkFirstTimeUser();
|
const isFirstUser = checkFirstTimeUser();
|
||||||
|
if (
|
||||||
if (isFirstTimeUser && !isOnboardingComplete) {
|
isFirstUser &&
|
||||||
|
!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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}, [
|
||||||
if (!isCloudUserVal && key === 'ONBOARDING') {
|
checkFirstTimeUser,
|
||||||
history.push(ROUTES.APPLICATION);
|
isCloudUserVal,
|
||||||
}
|
isFetchingOrgPreferences,
|
||||||
};
|
isFetchingOrgUsers,
|
||||||
|
orgPreferences,
|
||||||
const handleUserLoginIfTokenPresent = async (
|
orgUsers,
|
||||||
key: keyof typeof ROUTES,
|
pathname,
|
||||||
): 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;
|
||||||
|
|
||||||
if (path && path !== ROUTES.WORKSPACE_LOCKED) {
|
if (path && path !== ROUTES.WORKSPACE_LOCKED) {
|
||||||
history.push(ROUTES.WORKSPACE_LOCKED);
|
history.push(ROUTES.WORKSPACE_LOCKED);
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: UPDATE_USER_IS_FETCH,
|
|
||||||
payload: {
|
|
||||||
isUserFetching: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isFetchingLicensesData) {
|
if (!isFetchingLicenses) {
|
||||||
const shouldBlockWorkspace = licensesData?.payload?.workSpaceBlock;
|
const currentRoute = mapRoutes.get('current');
|
||||||
|
const shouldBlockWorkspace = licenses?.workSpaceBlock;
|
||||||
|
|
||||||
if (shouldBlockWorkspace) {
|
if (shouldBlockWorkspace && currentRoute) {
|
||||||
navigateToWorkSpaceBlocked(currentRoute);
|
navigateToWorkSpaceBlocked(currentRoute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [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) {
|
if (shouldSuspendWorkspace && currentRoute) {
|
||||||
navigateToWorkSpaceSuspended(currentRoute);
|
navigateToWorkSpaceSuspended(currentRoute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isFetchingActiveLicenseV3, activeLicenseV3]);
|
}, [isFetchingActiveLicenseV3, activeLicenseV3, mapRoutes, pathname]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (org && org.length > 0 && org[0].id !== undefined) {
|
if (org && org.length > 0 && org[0].id !== undefined) {
|
||||||
@@ -286,103 +156,70 @@ 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(() => {
|
||||||
(async (): Promise<void> => {
|
// if it is an old route navigate to the new route
|
||||||
try {
|
if (isOldRoute) {
|
||||||
if (isOldRoute) {
|
const redirectUrl = oldNewRoutesMapping[pathname];
|
||||||
const redirectUrl = oldNewRoutesMapping[pathname];
|
|
||||||
|
|
||||||
const newLocation = {
|
const newLocation = {
|
||||||
...location,
|
...location,
|
||||||
pathname: redirectUrl,
|
pathname: redirectUrl,
|
||||||
};
|
};
|
||||||
history.replace(newLocation);
|
history.replace(newLocation);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
if (currentRoute) {
|
// if the current route
|
||||||
const { isPrivate, key } = currentRoute;
|
if (currentRoute) {
|
||||||
|
const { isPrivate, key } = currentRoute;
|
||||||
if (isPrivate && key !== String(ROUTES.WORKSPACE_LOCKED)) {
|
if (isPrivate) {
|
||||||
handlePrivateRoutes(key);
|
if (isLoggedInState) {
|
||||||
} else {
|
const route = routePermission[key];
|
||||||
// no need to fetch the user and make user fetching false
|
if (route && route.find((e) => e === user.role) === undefined) {
|
||||||
if (getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true') {
|
history.push(ROUTES.UN_AUTHORIZED);
|
||||||
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 {
|
||||||
// not found
|
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, pathname);
|
||||||
navigateToLoginIfNotLoggedIn(isLocalStorageLoggedIn);
|
history.push(ROUTES.LOGIN);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} else if (isLoggedInState) {
|
||||||
// something went wrong
|
const fromPathname = getLocalStorageApi(
|
||||||
history.push(ROUTES.SOMETHING_WENT_WRONG);
|
LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT,
|
||||||
|
);
|
||||||
|
if (fromPathname) {
|
||||||
|
history.push(fromPathname);
|
||||||
|
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, '');
|
||||||
|
} else {
|
||||||
|
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);
|
||||||
|
}
|
||||||
}, [
|
}, [
|
||||||
dispatch,
|
licenses,
|
||||||
isLoggedInState,
|
isLoggedInState,
|
||||||
|
pathname,
|
||||||
|
user,
|
||||||
|
isOldRoute,
|
||||||
currentRoute,
|
currentRoute,
|
||||||
licensesData,
|
location,
|
||||||
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}</>;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
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';
|
||||||
@@ -11,35 +9,21 @@ 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 { useIsDarkMode, useThemeConfig } from 'hooks/useDarkMode';
|
import { useThemeConfig } from 'hooks/useDarkMode';
|
||||||
import { THEME_MODE } from 'hooks/useDarkMode/constant';
|
import { LICENSE_PLAN_KEY } from 'hooks/useLicense';
|
||||||
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, pick, pickBy } from 'lodash-es';
|
import { identity, 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 { AppProvider } from 'providers/App/App';
|
import { useAppContext } 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, useEffect, useState } from 'react';
|
import { Suspense, useCallback, useEffect, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { Redirect, Route, Router, Switch } from 'react-router-dom';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
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';
|
||||||
@@ -51,14 +35,20 @@ import defaultRoutes, {
|
|||||||
|
|
||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
const themeConfig = useThemeConfig();
|
const themeConfig = useThemeConfig();
|
||||||
const { data: licenseData } = useLicense();
|
const {
|
||||||
|
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();
|
||||||
|
|
||||||
@@ -66,164 +56,114 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
const isCloudUserVal = isCloudUser();
|
const isCloudUserVal = isCloudUser();
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const enableAnalytics = useCallback(
|
||||||
|
(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 isChatSupportEnabled =
|
const { name, email, role } = user;
|
||||||
useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active || false;
|
|
||||||
|
|
||||||
const isPremiumSupportEnabled =
|
const identifyPayload = {
|
||||||
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
|
email,
|
||||||
|
name,
|
||||||
|
company_name: orgName,
|
||||||
|
role,
|
||||||
|
source: 'signoz-ui',
|
||||||
|
};
|
||||||
|
|
||||||
const { data: orgPreferences, isLoading: isLoadingOrgPreferences } = useQuery({
|
const sanitizedIdentifyPayload = pickBy(identifyPayload, identity);
|
||||||
queryFn: () => getAllOrgPreferences(),
|
const domain = extractDomain(email);
|
||||||
queryKey: ['getOrgPreferences'],
|
const hostNameParts = hostname.split('.');
|
||||||
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 (orgPreferences && !isLoadingOrgPreferences) {
|
|
||||||
dispatch({
|
|
||||||
type: UPDATE_IS_FETCHING_ORG_PREFERENCES,
|
|
||||||
payload: {
|
|
||||||
isFetchingOrgPreferences: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: UPDATE_ORG_PREFERENCES,
|
|
||||||
payload: {
|
|
||||||
orgPreferences: orgPreferences.payload?.data || null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [orgPreferences, dispatch, isLoadingOrgPreferences]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isLoggedInState && role !== USER_ROLES.ADMIN) {
|
|
||||||
dispatch({
|
|
||||||
type: UPDATE_IS_FETCHING_ORG_PREFERENCES,
|
|
||||||
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 (
|
if (
|
||||||
isLoggedInState &&
|
!isFetchingLicenses &&
|
||||||
|
licenses &&
|
||||||
|
!isFetchingUser &&
|
||||||
user &&
|
user &&
|
||||||
user.userId &&
|
!!user.email
|
||||||
user.email &&
|
|
||||||
!isIdentifiedUser
|
|
||||||
) {
|
) {
|
||||||
setLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER, 'true');
|
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);
|
||||||
|
|
||||||
|
if (isLoggedInState && user && user.id && user.email && !isIdentifiedUser) {
|
||||||
|
setLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER, 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
}, [
|
||||||
if (
|
isLoggedInState,
|
||||||
isOnBasicPlan ||
|
user,
|
||||||
(isLoggedInState && role && role !== 'ADMIN') ||
|
licenses,
|
||||||
!(isCloudUserVal || isEECloudUser())
|
isCloudUserVal,
|
||||||
) {
|
isFetchingLicenses,
|
||||||
const newRoutes = routes.filter((route) => route?.path !== ROUTES.BILLING);
|
isFetchingUser,
|
||||||
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) {
|
||||||
@@ -237,99 +177,116 @@ function App(): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
trackPageView(pathname);
|
trackPageView(pathname);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [pathname, trackPageView]);
|
||||||
}, [pathname]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const showAddCreditCardModal =
|
// feature flag shouldn't be loading and featureFlags or fetchError any one of this should be true indicating that req is complete
|
||||||
!isPremiumSupportEnabled &&
|
// licenses should also be present. there is no check for licenses for loading and error as that is mandatory if not present then routing
|
||||||
!licenseData?.payload?.trialConvertedToSubscription;
|
// to something went wrong which would ideally need a reload.
|
||||||
|
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;
|
||||||
|
|
||||||
if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) {
|
isPremiumSupportEnabled =
|
||||||
window.Intercom('boot', {
|
featureFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)
|
||||||
app_id: process.env.INTERCOM_APP_ID,
|
?.active || false;
|
||||||
email: user?.email || '',
|
}
|
||||||
name: user?.name || '',
|
const showAddCreditCardModal =
|
||||||
});
|
!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 (user && user?.email && user?.userId && user?.name) {
|
if (!isFetchingUser && isCloudUserVal && user && user.email) {
|
||||||
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]);
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// if the user is in logged in state
|
||||||
}, [user]);
|
if (isLoggedInState) {
|
||||||
|
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..." />;
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
// if the required calls fails then return a something went wrong error
|
||||||
console.info('We are hiring! https://jobs.gem.com/signoz');
|
// this needs to be on top of data missing error because if there is an error, data will never be loaded and it will
|
||||||
}, []);
|
// move to indefinitive loading
|
||||||
|
if (userFetchError || licensesFetchError) {
|
||||||
|
return <Redirect to={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) {
|
||||||
|
return <Spinner tip="Loading..." />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppProvider>
|
<ConfigProvider theme={themeConfig}>
|
||||||
<ConfigProvider theme={themeConfig}>
|
<Router history={history}>
|
||||||
<Router history={history}>
|
<CompatRouter>
|
||||||
<CompatRouter>
|
<NotificationProvider>
|
||||||
<NotificationProvider>
|
<PrivateRoute>
|
||||||
<PrivateRoute>
|
<ResourceProvider>
|
||||||
<ResourceProvider>
|
<QueryBuilderProvider>
|
||||||
<QueryBuilderProvider>
|
<DashboardProvider>
|
||||||
<DashboardProvider>
|
<KeyboardHotkeysProvider>
|
||||||
<KeyboardHotkeysProvider>
|
<AlertRuleProvider>
|
||||||
<AlertRuleProvider>
|
<AppLayout>
|
||||||
<AppLayout>
|
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
<Switch>
|
||||||
<Switch>
|
{routes.map(({ path, component, exact }) => (
|
||||||
{routes.map(({ path, component, exact }) => (
|
<Route
|
||||||
<Route
|
key={`${path}`}
|
||||||
key={`${path}`}
|
exact={exact}
|
||||||
exact={exact}
|
path={path}
|
||||||
path={path}
|
component={component}
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -427,24 +427,27 @@ 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/api-keys',
|
'/settings/access-tokens',
|
||||||
];
|
];
|
||||||
|
|
||||||
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/api-keys': '/settings/access-tokens',
|
'/settings/access-tokens': '/settings/api-keys',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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'];
|
||||||
|
|||||||
@@ -1,92 +1,28 @@
|
|||||||
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 = async (
|
const afterLogin = (
|
||||||
userId: string,
|
userId: string,
|
||||||
authToken: string,
|
authToken: string,
|
||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
): Promise<SuccessResponse<PayloadProps> | undefined> => {
|
interceptorRejected?: boolean,
|
||||||
|
): 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');
|
||||||
|
|
||||||
store.dispatch<AppActions>({
|
if (!interceptorRejected) {
|
||||||
type: UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN,
|
window.dispatchEvent(
|
||||||
payload: {
|
new CustomEvent('AFTER_LOGIN', {
|
||||||
accessJwt: authToken,
|
detail: {
|
||||||
refreshJwt: refreshToken,
|
accessJWT: authToken,
|
||||||
},
|
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;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ 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,
|
||||||
@@ -26,10 +25,7 @@ const interceptorsResponse = (
|
|||||||
const interceptorsRequestResponse = (
|
const interceptorsRequestResponse = (
|
||||||
value: InternalAxiosRequestConfig,
|
value: InternalAxiosRequestConfig,
|
||||||
): InternalAxiosRequestConfig => {
|
): InternalAxiosRequestConfig => {
|
||||||
const token =
|
const token = getLocalStorageApi(LOCALSTORAGE.AUTH_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}` : '';
|
||||||
@@ -47,41 +43,36 @@ 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: store.getState().app.user?.refreshJwt,
|
refreshToken: getLocalStorageApi(LOCALSTORAGE.REFRESH_AUTH_TOKEN) || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
if (response.statusCode === 200) {
|
||||||
const user = await afterLogin(
|
afterLogin(
|
||||||
response.payload.userId,
|
response.payload.userId,
|
||||||
response.payload.accessJwt,
|
response.payload.accessJwt,
|
||||||
response.payload.refreshJwt,
|
response.payload.refreshJwt,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (user) {
|
const reResponse = await axios(
|
||||||
const reResponse = await axios(
|
`${value.config.baseURL}${value.config.url?.substring(1)}`,
|
||||||
`${value.config.baseURL}${value.config.url?.substring(1)}`,
|
{
|
||||||
{
|
method: value.config.method,
|
||||||
method: value.config.method,
|
headers: {
|
||||||
headers: {
|
...value.config.headers,
|
||||||
...value.config.headers,
|
Authorization: `Bearer ${response.payload.accessJwt}`,
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,18 @@
|
|||||||
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
|
||||||
> => {
|
> => {
|
||||||
try {
|
const response = await axios.get('/licenses');
|
||||||
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;
|
||||||
|
|||||||
@@ -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'>,
|
||||||
@@ -1,28 +1,18 @@
|
|||||||
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> => {
|
||||||
try {
|
const response = await axios.get(`/user/${props.userId}`);
|
||||||
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;
|
||||||
|
|||||||
@@ -2,14 +2,6 @@ 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);
|
||||||
@@ -19,50 +11,9 @@ 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);
|
||||||
|
|
||||||
store.dispatch({
|
window.dispatchEvent(new CustomEvent('LOGOUT'));
|
||||||
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
|
||||||
|
|||||||
@@ -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 { data: licenseData, isFetching } = useLicense();
|
const { licenses, isFetchingLicenses } = useAppContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const activeValidLicense =
|
if (!isFetchingLicenses && licenses) {
|
||||||
licenseData?.payload?.licenses?.find(
|
const activeValidLicense =
|
||||||
(license) => license.isCurrent === true,
|
licenses.licenses?.find((license) => license.isCurrent === true) || null;
|
||||||
) || null;
|
|
||||||
|
|
||||||
setActiveLicense(activeValidLicense);
|
setActiveLicense(activeValidLicense);
|
||||||
}, [licenseData, isFetching]);
|
}
|
||||||
|
}, [licenses, isFetchingLicenses]);
|
||||||
|
|
||||||
const handleBillingOnSuccess = (
|
const handleBillingOnSuccess = (
|
||||||
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
|
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
&.custom-time {
|
&.custom-time {
|
||||||
input:not(:focus) {
|
input:not(:focus) {
|
||||||
min-width: 240px;
|
min-width: 280px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,3 +119,69 @@
|
|||||||
color: var(--bg-slate-400) !important;
|
color: var(--bg-slate-400) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-time-popover__footer {
|
||||||
|
border-top: 1px solid var(--bg-ink-200);
|
||||||
|
padding: 8px 14px;
|
||||||
|
.timezone-container {
|
||||||
|
&,
|
||||||
|
.timezone {
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
letter-spacing: -0.06px;
|
||||||
|
}
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
gap: 6px;
|
||||||
|
.timezone {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: rgba(171, 189, 255, 0.04);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0px 4px;
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.timezone-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: rgba(171, 189, 255, 0.04);
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px;
|
||||||
|
letter-spacing: -0.06px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.date-time-popover__footer {
|
||||||
|
border-color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
.timezone-container {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
&__clock-icon {
|
||||||
|
stroke: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
.timezone {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
background: rgb(179 179 179 / 15%);
|
||||||
|
&__icon {
|
||||||
|
stroke: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.timezone-badge {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
background: rgb(179 179 179 / 15%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
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 {
|
||||||
@@ -15,11 +16,14 @@ import { isValidTimeFormat } from 'lib/getMinMax';
|
|||||||
import { defaultTo, isFunction, noop } from 'lodash-es';
|
import { defaultTo, isFunction, noop } from 'lodash-es';
|
||||||
import debounce from 'lodash-es/debounce';
|
import debounce from 'lodash-es/debounce';
|
||||||
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
|
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import {
|
import {
|
||||||
ChangeEvent,
|
ChangeEvent,
|
||||||
Dispatch,
|
Dispatch,
|
||||||
SetStateAction,
|
SetStateAction,
|
||||||
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
@@ -28,6 +32,8 @@ import { popupContainer } from 'utils/selectPopupContainer';
|
|||||||
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
|
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
|
||||||
|
|
||||||
const maxAllowedMinTimeInMonths = 6;
|
const maxAllowedMinTimeInMonths = 6;
|
||||||
|
type ViewType = 'datetime' | 'timezone';
|
||||||
|
const DEFAULT_VIEW: ViewType = 'datetime';
|
||||||
|
|
||||||
interface CustomTimePickerProps {
|
interface CustomTimePickerProps {
|
||||||
onSelect: (value: string) => void;
|
onSelect: (value: string) => void;
|
||||||
@@ -81,11 +87,42 @@ function CustomTimePicker({
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||||
|
|
||||||
|
const [activeView, setActiveView] = useState<ViewType>(DEFAULT_VIEW);
|
||||||
|
|
||||||
|
const { timezone, browserTimezone } = useTimezone();
|
||||||
|
const activeTimezoneOffset = timezone.offset;
|
||||||
|
const isTimezoneOverridden = useMemo(
|
||||||
|
() => timezone.offset !== browserTimezone.offset,
|
||||||
|
[timezone, browserTimezone],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleViewChange = useCallback(
|
||||||
|
(newView: 'timezone' | 'datetime'): void => {
|
||||||
|
if (activeView !== newView) {
|
||||||
|
setActiveView(newView);
|
||||||
|
}
|
||||||
|
setOpen(true);
|
||||||
|
},
|
||||||
|
[activeView, setOpen],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isOpenedFromFooter, setIsOpenedFromFooter] = useState(false);
|
||||||
|
|
||||||
const getSelectedTimeRangeLabel = (
|
const getSelectedTimeRangeLabel = (
|
||||||
selectedTime: string,
|
selectedTime: string,
|
||||||
selectedTimeValue: string,
|
selectedTimeValue: string,
|
||||||
): string => {
|
): string => {
|
||||||
if (selectedTime === 'custom') {
|
if (selectedTime === 'custom') {
|
||||||
|
// Convert the date range string to 12-hour format
|
||||||
|
const dates = selectedTimeValue.split(' - ');
|
||||||
|
if (dates.length === 2) {
|
||||||
|
const startDate = dayjs(dates[0], 'DD/MM/YYYY HH:mm');
|
||||||
|
const endDate = dayjs(dates[1], 'DD/MM/YYYY HH:mm');
|
||||||
|
|
||||||
|
return `${startDate.format('DD/MM/YYYY hh:mm A')} - ${endDate.format(
|
||||||
|
'DD/MM/YYYY hh:mm A',
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
return selectedTimeValue;
|
return selectedTimeValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +168,7 @@ function CustomTimePicker({
|
|||||||
setOpen(newOpen);
|
setOpen(newOpen);
|
||||||
if (!newOpen) {
|
if (!newOpen) {
|
||||||
setCustomDTPickerVisible?.(false);
|
setCustomDTPickerVisible?.(false);
|
||||||
|
setActiveView('datetime');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -244,6 +282,7 @@ function CustomTimePicker({
|
|||||||
|
|
||||||
const handleFocus = (): void => {
|
const handleFocus = (): void => {
|
||||||
setIsInputFocused(true);
|
setIsInputFocused(true);
|
||||||
|
setActiveView('datetime');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlur = (): void => {
|
const handleBlur = (): void => {
|
||||||
@@ -259,6 +298,18 @@ 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
|
||||||
@@ -280,6 +331,10 @@ function CustomTimePicker({
|
|||||||
handleGoLive={defaultTo(handleGoLive, noop)}
|
handleGoLive={defaultTo(handleGoLive, noop)}
|
||||||
options={items}
|
options={items}
|
||||||
selectedTime={selectedTime}
|
selectedTime={selectedTime}
|
||||||
|
activeView={activeView}
|
||||||
|
setActiveView={setActiveView}
|
||||||
|
setIsOpenedFromFooter={setIsOpenedFromFooter}
|
||||||
|
isOpenedFromFooter={isOpenedFromFooter}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
content
|
content
|
||||||
@@ -316,12 +371,17 @@ function CustomTimePicker({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
suffix={
|
suffix={
|
||||||
<ChevronDown
|
<>
|
||||||
size={14}
|
{!!isTimezoneOverridden && activeTimezoneOffset && (
|
||||||
onClick={(): void => {
|
<div className="timezone-badge" onClick={handleTimezoneHintClick}>
|
||||||
setOpen(!open);
|
<span>{activeTimezoneOffset}</span>
|
||||||
}}
|
</div>
|
||||||
/>
|
)}
|
||||||
|
<ChevronDown
|
||||||
|
size={14}
|
||||||
|
onClick={(): void => handleViewChange('datetime')}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import './CustomTimePicker.styles.scss';
|
import './CustomTimePicker.styles.scss';
|
||||||
|
|
||||||
|
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';
|
||||||
@@ -9,10 +11,13 @@ import {
|
|||||||
Option,
|
Option,
|
||||||
RelativeDurationSuggestionOptions,
|
RelativeDurationSuggestionOptions,
|
||||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
|
import { Clock, PenLine } from 'lucide-react';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { Dispatch, SetStateAction, useMemo } from 'react';
|
import { Dispatch, SetStateAction, useMemo } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import RangePickerModal from './RangePickerModal';
|
import RangePickerModal from './RangePickerModal';
|
||||||
|
import TimezonePicker from './TimezonePicker';
|
||||||
|
|
||||||
interface CustomTimePickerPopoverContentProps {
|
interface CustomTimePickerPopoverContentProps {
|
||||||
options: any[];
|
options: any[];
|
||||||
@@ -26,8 +31,13 @@ interface CustomTimePickerPopoverContentProps {
|
|||||||
onSelectHandler: (label: string, value: string) => void;
|
onSelectHandler: (label: string, value: string) => void;
|
||||||
handleGoLive: () => void;
|
handleGoLive: () => void;
|
||||||
selectedTime: string;
|
selectedTime: string;
|
||||||
|
activeView: 'datetime' | 'timezone';
|
||||||
|
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
|
||||||
|
isOpenedFromFooter: boolean;
|
||||||
|
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
function CustomTimePickerPopoverContent({
|
function CustomTimePickerPopoverContent({
|
||||||
options,
|
options,
|
||||||
setIsOpen,
|
setIsOpen,
|
||||||
@@ -37,12 +47,18 @@ function CustomTimePickerPopoverContent({
|
|||||||
onSelectHandler,
|
onSelectHandler,
|
||||||
handleGoLive,
|
handleGoLive,
|
||||||
selectedTime,
|
selectedTime,
|
||||||
|
activeView,
|
||||||
|
setActiveView,
|
||||||
|
isOpenedFromFooter,
|
||||||
|
setIsOpenedFromFooter,
|
||||||
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
|
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
|
||||||
pathname,
|
pathname,
|
||||||
]);
|
]);
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
const activeTimezoneOffset = timezone.offset;
|
||||||
|
|
||||||
function getTimeChips(options: Option[]): JSX.Element {
|
function getTimeChips(options: Option[]): JSX.Element {
|
||||||
return (
|
return (
|
||||||
@@ -63,55 +79,105 @@ function CustomTimePickerPopoverContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleTimezoneHintClick = (): void => {
|
||||||
|
setActiveView('timezone');
|
||||||
|
setIsOpenedFromFooter(true);
|
||||||
|
logEvent(
|
||||||
|
'DateTimePicker: Timezone picker opened from time range picker footer',
|
||||||
|
{
|
||||||
|
page: pathname,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (activeView === 'timezone') {
|
||||||
|
return (
|
||||||
|
<div className="date-time-popover">
|
||||||
|
<TimezonePicker
|
||||||
|
setActiveView={setActiveView}
|
||||||
|
setIsOpen={setIsOpen}
|
||||||
|
isOpenedFromFooter={isOpenedFromFooter}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="date-time-popover">
|
<>
|
||||||
<div className="date-time-options">
|
<div className="date-time-popover">
|
||||||
{isLogsExplorerPage && (
|
<div className="date-time-options">
|
||||||
<Button className="data-time-live" type="text" onClick={handleGoLive}>
|
{isLogsExplorerPage && (
|
||||||
Live
|
<Button className="data-time-live" type="text" onClick={handleGoLive}>
|
||||||
</Button>
|
Live
|
||||||
)}
|
</Button>
|
||||||
{options.map((option) => (
|
)}
|
||||||
<Button
|
{options.map((option) => (
|
||||||
type="text"
|
<Button
|
||||||
key={option.label + option.value}
|
type="text"
|
||||||
onClick={(): void => {
|
key={option.label + option.value}
|
||||||
onSelectHandler(option.label, option.value);
|
onClick={(): void => {
|
||||||
}}
|
onSelectHandler(option.label, option.value);
|
||||||
className={cx(
|
}}
|
||||||
'date-time-options-btn',
|
className={cx(
|
||||||
customDateTimeVisible
|
'date-time-options-btn',
|
||||||
? option.value === 'custom' && 'active'
|
customDateTimeVisible
|
||||||
: selectedTime === option.value && 'active',
|
? option.value === 'custom' && 'active'
|
||||||
)}
|
: selectedTime === option.value && 'active',
|
||||||
>
|
)}
|
||||||
{option.label}
|
>
|
||||||
</Button>
|
{option.label}
|
||||||
))}
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
'relative-date-time',
|
||||||
|
selectedTime === 'custom' || customDateTimeVisible
|
||||||
|
? 'date-picker'
|
||||||
|
: 'relative-times',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{selectedTime === 'custom' || customDateTimeVisible ? (
|
||||||
|
<RangePickerModal
|
||||||
|
setCustomDTPickerVisible={setCustomDTPickerVisible}
|
||||||
|
setIsOpen={setIsOpen}
|
||||||
|
onCustomDateHandler={onCustomDateHandler}
|
||||||
|
selectedTime={selectedTime}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="relative-times-container">
|
||||||
|
<div className="time-heading">RELATIVE TIMES</div>
|
||||||
|
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
className={cx(
|
<div className="date-time-popover__footer">
|
||||||
'relative-date-time',
|
<div className="timezone-container">
|
||||||
selectedTime === 'custom' || customDateTimeVisible
|
<Clock
|
||||||
? 'date-picker'
|
color={Color.BG_VANILLA_400}
|
||||||
: 'relative-times',
|
className="timezone-container__clock-icon"
|
||||||
)}
|
height={12}
|
||||||
>
|
width={12}
|
||||||
{selectedTime === 'custom' || customDateTimeVisible ? (
|
|
||||||
<RangePickerModal
|
|
||||||
setCustomDTPickerVisible={setCustomDTPickerVisible}
|
|
||||||
setIsOpen={setIsOpen}
|
|
||||||
onCustomDateHandler={onCustomDateHandler}
|
|
||||||
selectedTime={selectedTime}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
<span className="timezone__icon">Current timezone</span>
|
||||||
<div className="relative-times-container">
|
<div>⎯</div>
|
||||||
<div className="time-heading">RELATIVE TIMES</div>
|
<button
|
||||||
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
|
type="button"
|
||||||
</div>
|
className="timezone"
|
||||||
)}
|
onClick={handleTimezoneHintClick}
|
||||||
|
>
|
||||||
|
<span>{activeTimezoneOffset}</span>
|
||||||
|
<PenLine
|
||||||
|
color={Color.BG_VANILLA_100}
|
||||||
|
className="timezone__icon"
|
||||||
|
size={10}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ 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, { Dayjs } from 'dayjs';
|
||||||
import { Dispatch, SetStateAction } from 'react';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
|
import { Dispatch, SetStateAction, useMemo } 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';
|
||||||
@@ -31,7 +32,10 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
|
|||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
const disabledDate = (current: Dayjs): boolean => {
|
// Using any type here because antd's DatePicker expects its own internal Dayjs type
|
||||||
|
// which conflicts with our project's Dayjs type that has additional plugins (tz, utc etc).
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
|
||||||
|
const disabledDate = (current: any): boolean => {
|
||||||
const currentDay = dayjs(current);
|
const currentDay = dayjs(current);
|
||||||
return currentDay.isAfter(dayjs());
|
return currentDay.isAfter(dayjs());
|
||||||
};
|
};
|
||||||
@@ -49,16 +53,32 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
|
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
|
||||||
};
|
};
|
||||||
|
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: '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' && {
|
||||||
defaultValue: [dayjs(minTime / 1000000), dayjs(maxTime / 1000000)],
|
value: rangeValue,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
// Variables
|
||||||
|
$font-family: 'Inter';
|
||||||
|
$item-spacing: 8px;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--border-color: var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
--border-color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mixins
|
||||||
|
@mixin text-style-base {
|
||||||
|
font-family: $font-family;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin flex-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timezone-picker {
|
||||||
|
width: 532px;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: $font-family;
|
||||||
|
|
||||||
|
&__search {
|
||||||
|
@include flex-center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input-container {
|
||||||
|
@include flex-center;
|
||||||
|
gap: 6px;
|
||||||
|
width: -webkit-fill-available;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
@include text-style-base;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
padding: 0;
|
||||||
|
&.ant-input:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__esc-key {
|
||||||
|
@include text-style-base;
|
||||||
|
font-size: 8px;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
letter-spacing: -0.04px;
|
||||||
|
border-radius: 2.286px;
|
||||||
|
border: 1.143px solid var(--bg-ink-200);
|
||||||
|
border-bottom-width: 2.286px;
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
padding: 0 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
max-height: 310px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__item {
|
||||||
|
@include flex-center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 7.5px 6px 7.5px $item-spacing;
|
||||||
|
margin: 4px $item-spacing;
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
width: -webkit-fill-available;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: $font-family;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.selected {
|
||||||
|
border-radius: 2px;
|
||||||
|
background: rgba(171, 189, 255, 0.04);
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-divider {
|
||||||
|
position: relative;
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2px;
|
||||||
|
left: -$item-spacing;
|
||||||
|
right: -$item-spacing;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
@include text-style-base;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__offset {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
letter-spacing: -0.06px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timezone-name-wrapper {
|
||||||
|
@include flex-center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
&__selected-icon {
|
||||||
|
height: 15px;
|
||||||
|
width: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.timezone-picker {
|
||||||
|
&__search {
|
||||||
|
.search-icon {
|
||||||
|
stroke: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__input {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
&__esc-key {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
border-color: var(--bg-vanilla-400);
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
&__item {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
&__offset {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.timezone-name-wrapper {
|
||||||
|
&__selected-icon {
|
||||||
|
.check-icon {
|
||||||
|
stroke: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
208
frontend/src/components/CustomTimePicker/TimezonePicker.tsx
Normal file
208
frontend/src/components/CustomTimePicker/TimezonePicker.tsx
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import './TimezonePicker.styles.scss';
|
||||||
|
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Input } from 'antd';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { TimezonePickerShortcuts } from 'constants/shortcuts/TimezonePickerShortcuts';
|
||||||
|
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||||
|
import { Check, Search } from 'lucide-react';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
|
import {
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { Timezone, TIMEZONE_DATA } from './timezoneUtils';
|
||||||
|
|
||||||
|
interface SearchBarProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
|
||||||
|
isOpenedFromFooter: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TimezoneItemProps {
|
||||||
|
timezone: Timezone;
|
||||||
|
isSelected?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ICON_SIZE = 14;
|
||||||
|
|
||||||
|
function SearchBar({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
setIsOpen,
|
||||||
|
setActiveView,
|
||||||
|
isOpenedFromFooter = false,
|
||||||
|
}: SearchBarProps): JSX.Element {
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent): void => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
if (isOpenedFromFooter) {
|
||||||
|
setActiveView('datetime');
|
||||||
|
} else {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setActiveView, setIsOpen, isOpenedFromFooter],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="timezone-picker__search">
|
||||||
|
<div className="timezone-picker__input-container">
|
||||||
|
<Search
|
||||||
|
color={Color.BG_VANILLA_400}
|
||||||
|
className="search-icon"
|
||||||
|
height={ICON_SIZE}
|
||||||
|
width={ICON_SIZE}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
className="timezone-picker__input"
|
||||||
|
placeholder="Search timezones..."
|
||||||
|
value={value}
|
||||||
|
onChange={(e): void => onChange(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
tabIndex={0}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<kbd className="timezone-picker__esc-key">esc</kbd>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TimezoneItem({
|
||||||
|
timezone,
|
||||||
|
isSelected = false,
|
||||||
|
onClick,
|
||||||
|
}: TimezoneItemProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cx('timezone-picker__item', {
|
||||||
|
selected: isSelected,
|
||||||
|
'has-divider': timezone.hasDivider,
|
||||||
|
})}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<div className="timezone-name-wrapper">
|
||||||
|
<div className="timezone-name-wrapper__selected-icon">
|
||||||
|
{isSelected && (
|
||||||
|
<Check
|
||||||
|
className="check-icon"
|
||||||
|
color={Color.BG_VANILLA_100}
|
||||||
|
height={ICON_SIZE}
|
||||||
|
width={ICON_SIZE}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="timezone-picker__name">{timezone.name}</div>
|
||||||
|
</div>
|
||||||
|
<div className="timezone-picker__offset">{timezone.offset}</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TimezoneItem.defaultProps = {
|
||||||
|
isSelected: false,
|
||||||
|
onClick: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TimezonePickerProps {
|
||||||
|
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
|
||||||
|
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
isOpenedFromFooter: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TimezonePicker({
|
||||||
|
setActiveView,
|
||||||
|
setIsOpen,
|
||||||
|
isOpenedFromFooter,
|
||||||
|
}: TimezonePickerProps): JSX.Element {
|
||||||
|
console.log({ isOpenedFromFooter });
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const { timezone, updateTimezone } = useTimezone();
|
||||||
|
const [selectedTimezone, setSelectedTimezone] = useState<string>(
|
||||||
|
timezone.name ?? TIMEZONE_DATA[0].name,
|
||||||
|
);
|
||||||
|
|
||||||
|
const getFilteredTimezones = useCallback((searchTerm: string): Timezone[] => {
|
||||||
|
const normalizedSearch = searchTerm.toLowerCase();
|
||||||
|
return TIMEZONE_DATA.filter(
|
||||||
|
(tz) =>
|
||||||
|
tz.name.toLowerCase().includes(normalizedSearch) ||
|
||||||
|
tz.offset.toLowerCase().includes(normalizedSearch) ||
|
||||||
|
tz.searchIndex.toLowerCase().includes(normalizedSearch),
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCloseTimezonePicker = useCallback(() => {
|
||||||
|
if (isOpenedFromFooter) {
|
||||||
|
setActiveView('datetime');
|
||||||
|
} else {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
}, [isOpenedFromFooter, setActiveView, setIsOpen]);
|
||||||
|
|
||||||
|
const handleTimezoneSelect = useCallback(
|
||||||
|
(timezone: Timezone) => {
|
||||||
|
setSelectedTimezone(timezone.name);
|
||||||
|
updateTimezone(timezone);
|
||||||
|
handleCloseTimezonePicker();
|
||||||
|
setIsOpen(false);
|
||||||
|
logEvent('DateTimePicker: New Timezone Selected', {
|
||||||
|
timezone: {
|
||||||
|
name: timezone.name,
|
||||||
|
offset: timezone.offset,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[handleCloseTimezonePicker, setIsOpen, updateTimezone],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register keyboard shortcuts
|
||||||
|
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
registerShortcut(
|
||||||
|
TimezonePickerShortcuts.CloseTimezonePicker,
|
||||||
|
handleCloseTimezonePicker,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (): void => {
|
||||||
|
deregisterShortcut(TimezonePickerShortcuts.CloseTimezonePicker);
|
||||||
|
};
|
||||||
|
}, [deregisterShortcut, handleCloseTimezonePicker, registerShortcut]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="timezone-picker">
|
||||||
|
<SearchBar
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={setSearchTerm}
|
||||||
|
setIsOpen={setIsOpen}
|
||||||
|
setActiveView={setActiveView}
|
||||||
|
isOpenedFromFooter={isOpenedFromFooter}
|
||||||
|
/>
|
||||||
|
<div className="timezone-picker__list">
|
||||||
|
{getFilteredTimezones(searchTerm).map((timezone) => (
|
||||||
|
<TimezoneItem
|
||||||
|
key={timezone.value}
|
||||||
|
timezone={timezone}
|
||||||
|
isSelected={timezone.name === selectedTimezone}
|
||||||
|
onClick={(): void => handleTimezoneSelect(timezone)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TimezonePicker;
|
||||||
152
frontend/src/components/CustomTimePicker/timezoneUtils.ts
Normal file
152
frontend/src/components/CustomTimePicker/timezoneUtils.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import dayjs from 'dayjs';
|
||||||
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
|
import utc from 'dayjs/plugin/utc';
|
||||||
|
|
||||||
|
dayjs.extend(utc);
|
||||||
|
dayjs.extend(timezone);
|
||||||
|
|
||||||
|
export interface Timezone {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
offset: string;
|
||||||
|
searchIndex: string;
|
||||||
|
hasDivider?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TIMEZONE_TYPES = {
|
||||||
|
BROWSER: 'BROWSER',
|
||||||
|
UTC: 'UTC',
|
||||||
|
STANDARD: 'STANDARD',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type TimezoneType = typeof TIMEZONE_TYPES[keyof typeof TIMEZONE_TYPES];
|
||||||
|
|
||||||
|
export const UTC_TIMEZONE: Timezone = {
|
||||||
|
name: 'Coordinated Universal Time — UTC, GMT',
|
||||||
|
value: 'UTC',
|
||||||
|
offset: 'UTC',
|
||||||
|
searchIndex: 'UTC',
|
||||||
|
hasDivider: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeTimezoneName = (timezone: string): string => {
|
||||||
|
// https://github.com/tc39/proposal-temporal/issues/1076
|
||||||
|
if (timezone === 'Asia/Calcutta') {
|
||||||
|
return 'Asia/Kolkata';
|
||||||
|
}
|
||||||
|
return timezone;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatOffset = (offsetMinutes: number): string => {
|
||||||
|
if (offsetMinutes === 0) return 'UTC';
|
||||||
|
|
||||||
|
const hours = Math.floor(Math.abs(offsetMinutes) / 60);
|
||||||
|
const minutes = Math.abs(offsetMinutes) % 60;
|
||||||
|
const sign = offsetMinutes > 0 ? '+' : '-';
|
||||||
|
|
||||||
|
return `UTC ${sign} ${hours}${
|
||||||
|
minutes ? `:${minutes.toString().padStart(2, '0')}` : ':00'
|
||||||
|
}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTimezoneEntry = (
|
||||||
|
name: string,
|
||||||
|
offsetMinutes: number,
|
||||||
|
type: TimezoneType = TIMEZONE_TYPES.STANDARD,
|
||||||
|
hasDivider = false,
|
||||||
|
): Timezone => {
|
||||||
|
const offset = formatOffset(offsetMinutes);
|
||||||
|
let value = name;
|
||||||
|
let displayName = name;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case TIMEZONE_TYPES.BROWSER:
|
||||||
|
displayName = `Browser time — ${name}`;
|
||||||
|
value = name;
|
||||||
|
break;
|
||||||
|
case TIMEZONE_TYPES.UTC:
|
||||||
|
displayName = 'Coordinated Universal Time — UTC, GMT';
|
||||||
|
value = 'UTC';
|
||||||
|
break;
|
||||||
|
case TIMEZONE_TYPES.STANDARD:
|
||||||
|
displayName = name;
|
||||||
|
value = name;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error(`Invalid timezone type: ${type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: displayName,
|
||||||
|
value,
|
||||||
|
offset,
|
||||||
|
searchIndex: offset.replace(/ /g, ''),
|
||||||
|
...(hasDivider && { hasDivider }),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOffsetByTimezone = (timezone: string): number => {
|
||||||
|
const dayjsTimezone = dayjs().tz(timezone);
|
||||||
|
return dayjsTimezone.utcOffset();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBrowserTimezone = (): Timezone => {
|
||||||
|
const browserTz = dayjs.tz.guess();
|
||||||
|
const normalizedTz = normalizeTimezoneName(browserTz);
|
||||||
|
const browserOffset = getOffsetByTimezone(normalizedTz);
|
||||||
|
return createTimezoneEntry(
|
||||||
|
normalizedTz,
|
||||||
|
browserOffset,
|
||||||
|
TIMEZONE_TYPES.BROWSER,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterAndSortTimezones = (
|
||||||
|
allTimezones: string[],
|
||||||
|
browserTzName?: string,
|
||||||
|
includeEtcTimezones = false,
|
||||||
|
): Timezone[] =>
|
||||||
|
allTimezones
|
||||||
|
.filter((tz) => {
|
||||||
|
const isNotBrowserTz = tz !== browserTzName;
|
||||||
|
const isNotEtcTz = includeEtcTimezones || !tz.startsWith('Etc/');
|
||||||
|
return isNotBrowserTz && isNotEtcTz;
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.localeCompare(b))
|
||||||
|
.map((tz) => {
|
||||||
|
const normalizedTz = normalizeTimezoneName(tz);
|
||||||
|
const offset = getOffsetByTimezone(normalizedTz);
|
||||||
|
return createTimezoneEntry(normalizedTz, offset);
|
||||||
|
});
|
||||||
|
|
||||||
|
const generateTimezoneData = (includeEtcTimezones = false): Timezone[] => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const allTimezones = (Intl as any).supportedValuesOf('timeZone');
|
||||||
|
const timezones: Timezone[] = [];
|
||||||
|
|
||||||
|
// Add browser timezone
|
||||||
|
const browserTzObject = getBrowserTimezone();
|
||||||
|
timezones.push(browserTzObject);
|
||||||
|
|
||||||
|
// Add UTC timezone with divider
|
||||||
|
timezones.push(UTC_TIMEZONE);
|
||||||
|
|
||||||
|
timezones.push(
|
||||||
|
...filterAndSortTimezones(
|
||||||
|
allTimezones,
|
||||||
|
browserTzObject.value,
|
||||||
|
includeEtcTimezones,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return timezones;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTimezoneObjectByTimezoneString = (
|
||||||
|
timezone: string,
|
||||||
|
): Timezone => {
|
||||||
|
const utcOffset = getOffsetByTimezone(timezone);
|
||||||
|
return createTimezoneEntry(timezone, utcOffset);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TIMEZONE_DATA = generateTimezoneData();
|
||||||
@@ -1,15 +1,22 @@
|
|||||||
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(() => {
|
||||||
matchMedia();
|
Object.defineProperty(window, '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', () => {
|
||||||
@@ -34,18 +41,14 @@ 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(
|
||||||
<Provider store={store}>
|
<Table
|
||||||
<I18nextProvider i18n={i18n}>
|
components={{
|
||||||
<Table
|
body: {
|
||||||
components={{
|
row: DraggableTableRow,
|
||||||
body: {
|
},
|
||||||
row: DraggableTableRow,
|
}}
|
||||||
},
|
pagination={false}
|
||||||
}}
|
/>,
|
||||||
pagination={false}
|
|
||||||
/>
|
|
||||||
</I18nextProvider>
|
|
||||||
</Provider>,
|
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -99,5 +99,3 @@ exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</DocumentFragment>
|
</DocumentFragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`PipelinePage container test should render AddNewPipeline section 1`] = `<DocumentFragment />`;
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
_adapters,
|
||||||
BarController,
|
BarController,
|
||||||
BarElement,
|
BarElement,
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
@@ -18,8 +19,10 @@ import {
|
|||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||||
import { generateGridTitle } from 'container/GridPanelSwitch/utils';
|
import { generateGridTitle } from 'container/GridPanelSwitch/utils';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
memo,
|
memo,
|
||||||
@@ -62,6 +65,17 @@ Chart.register(
|
|||||||
|
|
||||||
Tooltip.positioners.custom = TooltipPositionHandler;
|
Tooltip.positioners.custom = TooltipPositionHandler;
|
||||||
|
|
||||||
|
// Map of Chart.js time formats to dayjs format strings
|
||||||
|
const formatMap = {
|
||||||
|
'HH:mm:ss': 'HH:mm:ss',
|
||||||
|
'HH:mm': 'HH:mm',
|
||||||
|
'MM/DD HH:mm': 'MM/DD HH:mm',
|
||||||
|
'MM/dd HH:mm': 'MM/DD HH:mm',
|
||||||
|
'MM/DD': 'MM/DD',
|
||||||
|
'YY-MM': 'YY-MM',
|
||||||
|
YY: 'YY',
|
||||||
|
};
|
||||||
|
|
||||||
const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
|
const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
@@ -80,11 +94,13 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
|
|||||||
dragSelectColor,
|
dragSelectColor,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
const nearestDatasetIndex = useRef<null | number>(null);
|
const nearestDatasetIndex = useRef<null | number>(null);
|
||||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const gridTitle = useMemo(() => generateGridTitle(title), [title]);
|
const gridTitle = useMemo(() => generateGridTitle(title), [title]);
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
const currentTheme = isDarkMode ? 'dark' : 'light';
|
const currentTheme = isDarkMode ? 'dark' : 'light';
|
||||||
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
|
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
|
||||||
@@ -112,6 +128,22 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
|
|||||||
return 'rgba(231,233,237,0.8)';
|
return 'rgba(231,233,237,0.8)';
|
||||||
}, [currentTheme]);
|
}, [currentTheme]);
|
||||||
|
|
||||||
|
// Override Chart.js date adapter to use dayjs with timezone support
|
||||||
|
useEffect(() => {
|
||||||
|
_adapters._date.override({
|
||||||
|
format(time: number | Date, fmt: string) {
|
||||||
|
const dayjsTime = dayjs(time).tz(timezone.value);
|
||||||
|
const format = formatMap[fmt as keyof typeof formatMap];
|
||||||
|
if (!format) {
|
||||||
|
console.warn(`Missing datetime format for ${fmt}`);
|
||||||
|
return dayjsTime.format('YYYY-MM-DD HH:mm:ss'); // fallback format
|
||||||
|
}
|
||||||
|
|
||||||
|
return dayjsTime.format(format);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [timezone]);
|
||||||
|
|
||||||
const buildChart = useCallback(() => {
|
const buildChart = useCallback(() => {
|
||||||
if (lineChartRef.current !== undefined) {
|
if (lineChartRef.current !== undefined) {
|
||||||
lineChartRef.current.destroy();
|
lineChartRef.current.destroy();
|
||||||
@@ -132,6 +164,7 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
|
|||||||
isStacked,
|
isStacked,
|
||||||
onClickHandler,
|
onClickHandler,
|
||||||
data,
|
data,
|
||||||
|
timezone,
|
||||||
);
|
);
|
||||||
|
|
||||||
const chartHasData = hasData(data);
|
const chartHasData = hasData(data);
|
||||||
@@ -166,6 +199,7 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
|
|||||||
isStacked,
|
isStacked,
|
||||||
onClickHandler,
|
onClickHandler,
|
||||||
data,
|
data,
|
||||||
|
timezone,
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Chart, ChartConfiguration, ChartData, Color } from 'chart.js';
|
import { Chart, ChartConfiguration, ChartData, Color } from 'chart.js';
|
||||||
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
|
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
|
||||||
|
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { MutableRefObject } from 'react';
|
import { MutableRefObject } from 'react';
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ export const getGraphOptions = (
|
|||||||
isStacked: boolean | undefined,
|
isStacked: boolean | undefined,
|
||||||
onClickHandler: GraphOnClickHandler | undefined,
|
onClickHandler: GraphOnClickHandler | undefined,
|
||||||
data: ChartData,
|
data: ChartData,
|
||||||
|
timezone: Timezone,
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
): CustomChartOptions => ({
|
): CustomChartOptions => ({
|
||||||
animation: {
|
animation: {
|
||||||
@@ -97,7 +99,7 @@ export const getGraphOptions = (
|
|||||||
callbacks: {
|
callbacks: {
|
||||||
title(context): string | string[] {
|
title(context): string | string[] {
|
||||||
const date = dayjs(context[0].parsed.x);
|
const date = dayjs(context[0].parsed.x);
|
||||||
return date.format('MMM DD, YYYY, HH:mm:ss');
|
return date.tz(timezone.value).format('MMM DD, YYYY, HH:mm:ss');
|
||||||
},
|
},
|
||||||
label(context): string | string[] {
|
label(context): string | string[] {
|
||||||
let label = context.dataset.label || '';
|
let label = context.dataset.label || '';
|
||||||
|
|||||||
@@ -5,18 +5,16 @@ 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 } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { user } = useAppContext();
|
||||||
const { t } = useTranslation(['infraMonitoring']);
|
const { t } = useTranslation(['infraMonitoring']);
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ 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 { useEffect, useState } from 'react';
|
import { useAppContext } from 'providers/App/App';
|
||||||
|
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';
|
||||||
@@ -39,31 +38,79 @@ 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 { data: licenseData, isFetching } = useLicense();
|
const {
|
||||||
|
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 showAddCreditCardModal =
|
const isChatSupportEnabled = useMemo(() => {
|
||||||
!isPremiumChatSupportEnabled &&
|
if (!isFetchingFeatureFlags && (featureFlags || featureFlagsFetchError)) {
|
||||||
!licenseData?.payload?.trialConvertedToSubscription;
|
let isChatSupportEnabled = false;
|
||||||
|
|
||||||
|
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(() => {
|
||||||
const activeValidLicense =
|
if (!isFetchingLicenses && licenses) {
|
||||||
licenseData?.payload?.licenses?.find(
|
const activeValidLicense =
|
||||||
(license) => license.isCurrent === true,
|
licenses.licenses?.find((license) => license.isCurrent === true) || null;
|
||||||
) || null;
|
setActiveLicense(activeValidLicense);
|
||||||
|
}
|
||||||
setActiveLicense(activeValidLicense);
|
}, [isFetchingLicenses, licenses]);
|
||||||
}, [licenseData, isFetching]);
|
|
||||||
|
|
||||||
const handleFacingIssuesClick = (): void => {
|
const handleFacingIssuesClick = (): void => {
|
||||||
if (showAddCreditCardModal) {
|
if (showAddCreditCardModal) {
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import LogDetail from 'components/LogDetail';
|
|||||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||||
import { unescapeString } from 'container/LogDetailedView/utils';
|
import { unescapeString } from 'container/LogDetailedView/utils';
|
||||||
import { FontSize } from 'container/OptionsMenu/types';
|
import { FontSize } from 'container/OptionsMenu/types';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
// utils
|
// utils
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
// interfaces
|
// interfaces
|
||||||
import { IField } from 'types/api/logs/fields';
|
import { IField } from 'types/api/logs/fields';
|
||||||
@@ -174,12 +174,20 @@ function ListLogView({
|
|||||||
[selectedFields],
|
[selectedFields],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
const timestampValue = useMemo(
|
const timestampValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
typeof flattenLogData.timestamp === 'string'
|
typeof flattenLogData.timestamp === 'string'
|
||||||
? dayjs(flattenLogData.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS')
|
? formatTimezoneAdjustedTimestamp(
|
||||||
: dayjs(flattenLogData.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'),
|
flattenLogData.timestamp,
|
||||||
[flattenLogData.timestamp],
|
'YYYY-MM-DD HH:mm:ss.SSS',
|
||||||
|
)
|
||||||
|
: formatTimezoneAdjustedTimestamp(
|
||||||
|
flattenLogData.timestamp / 1e6,
|
||||||
|
'YYYY-MM-DD HH:mm:ss.SSS',
|
||||||
|
),
|
||||||
|
[flattenLogData.timestamp, formatTimezoneAdjustedTimestamp],
|
||||||
);
|
);
|
||||||
|
|
||||||
const logType = getLogIndicatorType(logData);
|
const logType = getLogIndicatorType(logData);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import LogDetail from 'components/LogDetail';
|
|||||||
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
|
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
|
||||||
import { unescapeString } from 'container/LogDetailedView/utils';
|
import { unescapeString } from 'container/LogDetailedView/utils';
|
||||||
import LogsExplorerContext from 'container/LogsExplorerContext';
|
import LogsExplorerContext from 'container/LogsExplorerContext';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||||
@@ -14,6 +13,7 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
|||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
import { isEmpty, isNumber, isUndefined } from 'lodash-es';
|
import { isEmpty, isNumber, isUndefined } from 'lodash-es';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import {
|
import {
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
MouseEvent,
|
MouseEvent,
|
||||||
@@ -89,16 +89,24 @@ function RawLogView({
|
|||||||
attributesText += ' | ';
|
attributesText += ' | ';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
const text = useMemo(() => {
|
const text = useMemo(() => {
|
||||||
const date =
|
const date =
|
||||||
typeof data.timestamp === 'string'
|
typeof data.timestamp === 'string'
|
||||||
? dayjs(data.timestamp)
|
? formatTimezoneAdjustedTimestamp(data.timestamp, 'YYYY-MM-DD HH:mm:ss.SSS')
|
||||||
: dayjs(data.timestamp / 1e6);
|
: formatTimezoneAdjustedTimestamp(
|
||||||
|
data.timestamp / 1e6,
|
||||||
|
'YYYY-MM-DD HH:mm:ss.SSS',
|
||||||
|
);
|
||||||
|
|
||||||
return `${date.format('YYYY-MM-DD HH:mm:ss.SSS')} | ${attributesText} ${
|
return `${date} | ${attributesText} ${data.body}`;
|
||||||
data.body
|
}, [
|
||||||
}`;
|
data.timestamp,
|
||||||
}, [data.timestamp, data.body, attributesText]);
|
data.body,
|
||||||
|
attributesText,
|
||||||
|
formatTimezoneAdjustedTimestamp,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleClickExpand = useCallback(() => {
|
const handleClickExpand = useCallback(() => {
|
||||||
if (activeContextLog || isReadOnly) return;
|
if (activeContextLog || isReadOnly) return;
|
||||||
|
|||||||
@@ -22,6 +22,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.state-indicator {
|
||||||
|
width: 15px;
|
||||||
|
.log-state-indicator {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.table-timestamp {
|
.table-timestamp {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -29,10 +36,6 @@
|
|||||||
.ant-typography {
|
.ant-typography {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-state-indicator {
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import { Typography } from 'antd';
|
|||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { unescapeString } from 'container/LogDetailedView/utils';
|
import { unescapeString } from 'container/LogDetailedView/utils';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
||||||
|
|
||||||
@@ -44,6 +44,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
logs,
|
logs,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
|
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
|
||||||
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
|
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
|
||||||
.filter((e) => e.name !== 'id')
|
.filter((e) => e.name !== 'id')
|
||||||
@@ -73,23 +75,38 @@ 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, item): ColumnTypeRender<Record<string, unknown>> => {
|
render: (field): ColumnTypeRender<Record<string, unknown>> => {
|
||||||
const date =
|
const date =
|
||||||
typeof field === 'string'
|
typeof field === 'string'
|
||||||
? dayjs(field).format('YYYY-MM-DD HH:mm:ss.SSS')
|
? formatTimezoneAdjustedTimestamp(field, 'YYYY-MM-DD HH:mm:ss.SSS')
|
||||||
: dayjs(field / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS');
|
: formatTimezoneAdjustedTimestamp(
|
||||||
|
field / 1e6,
|
||||||
|
'YYYY-MM-DD HH:mm:ss.SSS',
|
||||||
|
);
|
||||||
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>
|
||||||
@@ -125,7 +142,15 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
},
|
},
|
||||||
...(appendTo === 'end' ? fieldColumns : []),
|
...(appendTo === 'end' ? fieldColumns : []),
|
||||||
];
|
];
|
||||||
}, [fields, isListViewPanel, appendTo, isDarkMode, linesPerRow, fontSize]);
|
}, [
|
||||||
|
fields,
|
||||||
|
isListViewPanel,
|
||||||
|
appendTo,
|
||||||
|
isDarkMode,
|
||||||
|
linesPerRow,
|
||||||
|
fontSize,
|
||||||
|
formatTimezoneAdjustedTimestamp,
|
||||||
|
]);
|
||||||
|
|
||||||
return { columns, dataSource: flattenLogData };
|
return { columns, dataSource: flattenLogData };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,31 +1,10 @@
|
|||||||
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 />
|
||||||
@@ -35,7 +14,7 @@ function NotFound({ text = defaultText }: Props): JSX.Element {
|
|||||||
<Text>Page Not Found</Text>
|
<Text>Page Not Found</Text>
|
||||||
</TextContainer>
|
</TextContainer>
|
||||||
|
|
||||||
<Button onClick={onClickHandler} to={ROUTES.APPLICATION} tabIndex={0}>
|
<Button to={ROUTES.APPLICATION} tabIndex={0}>
|
||||||
Return To Services Page
|
Return To Services Page
|
||||||
</Button>
|
</Button>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -1,40 +1,28 @@
|
|||||||
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 } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { user, setUserFlags } = useAppContext();
|
||||||
|
|
||||||
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 {
|
||||||
dispatch({
|
setUserFlags(flags);
|
||||||
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?.userId, flags });
|
const response = await setFlags({ userId: user.id, 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);
|
||||||
@@ -44,7 +32,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);
|
||||||
}
|
}
|
||||||
}, [dispatch, user]);
|
}, [setUserFlags, user]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageTip
|
<MessageTip
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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';
|
||||||
@@ -44,12 +45,13 @@ 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 { userFlags, currentVersion } = useSelector<AppState, AppReducer>(
|
const { user } = useAppContext();
|
||||||
|
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, userFlags),
|
item.match(path, currentVersion, user.flags),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!c) {
|
if (!c) {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import getFormattedDate from 'lib/getFormatedDate';
|
|
||||||
|
|
||||||
function Time({ CreatedOrUpdateTime }: DateProps): JSX.Element {
|
function Time({ CreatedOrUpdateTime }: DateProps): JSX.Element {
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
const time = new Date(CreatedOrUpdateTime);
|
const time = new Date(CreatedOrUpdateTime);
|
||||||
const date = getFormattedDate(time);
|
const timeString = formatTimezoneAdjustedTimestamp(
|
||||||
const timeString = `${date} ${convertDateToAmAndPm(time)}`;
|
time,
|
||||||
|
'MM/DD/YYYY hh:mm:ss A (UTC Z)',
|
||||||
|
);
|
||||||
return <Typography>{timeString}</Typography>;
|
return <Typography>{timeString}</Typography>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ function TabLabel({
|
|||||||
isDisabled,
|
isDisabled,
|
||||||
tooltipText,
|
tooltipText,
|
||||||
}: TabLabelProps): JSX.Element {
|
}: TabLabelProps): JSX.Element {
|
||||||
const currentLabel = <span>{label}</span>;
|
const currentLabel = <span data-testid={`${label}`}>{label}</span>;
|
||||||
|
|
||||||
if (isDisabled) {
|
if (isDisabled) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -21,4 +21,7 @@ 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',
|
||||||
|
UNAUTHENTICATED_ROUTE_HIT = 'UNAUTHENTICATED_ROUTE_HIT',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +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',
|
||||||
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',
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export const TimezonePickerShortcuts = {
|
||||||
|
CloseTimezonePicker: 'escape',
|
||||||
|
};
|
||||||
@@ -26,9 +26,9 @@ describe('APIKeys component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders APIKeys component without crashing', () => {
|
it('renders APIKeys component without crashing', () => {
|
||||||
expect(screen.getByText('Access Tokens')).toBeInTheDocument();
|
expect(screen.getByText('API Keys')).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByText('Create and manage access tokens for the SigNoz API'),
|
screen.getByText('Create and manage API keys for the SigNoz API'),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,16 +40,16 @@ describe('APIKeys component', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText('No Expiry Token')).toBeInTheDocument();
|
expect(screen.getByText('No Expiry Key')).toBeInTheDocument();
|
||||||
expect(screen.getByText('1-5 of 18 tokens')).toBeInTheDocument();
|
expect(screen.getByText('1-5 of 18 keys')).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 Token'));
|
fireEvent.click(screen.getByText('New Key'));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const createNewKeyBtn = screen.getByRole('button', {
|
const createNewKeyBtn = screen.getByRole('button', {
|
||||||
name: /Create new token/i,
|
name: /Create new key/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 Token'));
|
fireEvent.click(screen.getByText('New Key'));
|
||||||
|
|
||||||
const createNewKeyBtn = screen.getByRole('button', {
|
const createNewKeyBtn = screen.getByRole('button', {
|
||||||
name: /Create new token/i,
|
name: /Create new key/i,
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -79,10 +79,10 @@ describe('APIKeys component', () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('New Token'));
|
fireEvent.click(screen.getByText('New Key'));
|
||||||
|
|
||||||
const createNewKeyBtn = screen.getByRole('button', {
|
const createNewKeyBtn = screen.getByRole('button', {
|
||||||
name: /Create new token/i,
|
name: /Create new key/i,
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -90,7 +90,7 @@ describe('APIKeys component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
const inputElement = screen.getByPlaceholderText('Enter Token Name');
|
const inputElement = screen.getByPlaceholderText('Enter Key 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);
|
||||||
|
|||||||
@@ -44,14 +44,12 @@ 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 = (
|
||||||
@@ -99,7 +97,7 @@ export const getDateDifference = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
function APIKeys(): JSX.Element {
|
function APIKeys(): JSX.Element {
|
||||||
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { user } = useAppContext();
|
||||||
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);
|
||||||
@@ -514,15 +512,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">Access Tokens </Typography.Title>
|
<Typography.Title className="title">API Keys</Typography.Title>
|
||||||
<Typography.Text className="subtitle">
|
<Typography.Text className="subtitle">
|
||||||
Create and manage access tokens for the SigNoz API
|
Create and manage API keys 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 token..."
|
placeholder="Search for keys..."
|
||||||
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}
|
||||||
@@ -533,7 +531,7 @@ function APIKeys(): JSX.Element {
|
|||||||
type="primary"
|
type="primary"
|
||||||
onClick={showAddModal}
|
onClick={showAddModal}
|
||||||
>
|
>
|
||||||
<Plus size={14} /> New Token
|
<Plus size={14} /> New Key
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -546,7 +544,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} tokens`,
|
`${range[0]}-${range[1]} of ${total} keys`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -554,7 +552,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 Token</span>}
|
title={<span className="title">Delete Key</span>}
|
||||||
open={isDeleteModalOpen}
|
open={isDeleteModalOpen}
|
||||||
closable
|
closable
|
||||||
afterClose={handleModalClose}
|
afterClose={handleModalClose}
|
||||||
@@ -576,7 +574,7 @@ function APIKeys(): JSX.Element {
|
|||||||
onClick={onDeleteHandler}
|
onClick={onDeleteHandler}
|
||||||
className="delete-btn"
|
className="delete-btn"
|
||||||
>
|
>
|
||||||
Delete Token
|
Delete key
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -590,7 +588,7 @@ function APIKeys(): JSX.Element {
|
|||||||
{/* Edit Key Modal */}
|
{/* Edit Key Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
className="api-key-modal"
|
className="api-key-modal"
|
||||||
title="Edit token"
|
title="Edit key"
|
||||||
open={isEditModalOpen}
|
open={isEditModalOpen}
|
||||||
key="edit-api-key-modal"
|
key="edit-api-key-modal"
|
||||||
afterClose={handleModalClose}
|
afterClose={handleModalClose}
|
||||||
@@ -614,7 +612,7 @@ function APIKeys(): JSX.Element {
|
|||||||
icon={<Check size={14} />}
|
icon={<Check size={14} />}
|
||||||
onClick={onUpdateApiKey}
|
onClick={onUpdateApiKey}
|
||||||
>
|
>
|
||||||
Update Token
|
Update key
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -634,7 +632,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 Token Name" autoFocus />
|
<Input placeholder="Enter Key Name" autoFocus />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="role" label="Role">
|
<Form.Item name="role" label="Role">
|
||||||
@@ -668,7 +666,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 token"
|
title="Create new key"
|
||||||
open={isAddModalOpen}
|
open={isAddModalOpen}
|
||||||
key="create-api-key-modal"
|
key="create-api-key-modal"
|
||||||
closable
|
closable
|
||||||
@@ -685,7 +683,7 @@ function APIKeys(): JSX.Element {
|
|||||||
onClick={handleCopyClose}
|
onClick={handleCopyClose}
|
||||||
icon={<Check size={12} />}
|
icon={<Check size={12} />}
|
||||||
>
|
>
|
||||||
Copy token and close
|
Copy key and close
|
||||||
</Button>,
|
</Button>,
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
@@ -706,7 +704,7 @@ function APIKeys(): JSX.Element {
|
|||||||
loading={isLoadingCreateAPIKey}
|
loading={isLoadingCreateAPIKey}
|
||||||
onClick={onCreateAPIKey}
|
onClick={onCreateAPIKey}
|
||||||
>
|
>
|
||||||
Create new token
|
Create new key
|
||||||
</Button>,
|
</Button>,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -730,7 +728,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 Token Name" autoFocus />
|
<Input placeholder="Enter Key Name" autoFocus />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="role" label="Role">
|
<Form.Item name="role" label="Role">
|
||||||
@@ -771,7 +769,7 @@ function APIKeys(): JSX.Element {
|
|||||||
{showNewAPIKeyDetails && (
|
{showNewAPIKeyDetails && (
|
||||||
<div className="api-key-info-container">
|
<div className="api-key-info-container">
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={8}>Token</Col>
|
<Col span={8}>Key</Col>
|
||||||
<Col span={16}>
|
<Col span={16}>
|
||||||
<span className="copyable-text">
|
<span className="copyable-text">
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
|
|||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import heatmapPlugin from 'lib/uPlotLib/plugins/heatmapPlugin';
|
import heatmapPlugin from 'lib/uPlotLib/plugins/heatmapPlugin';
|
||||||
import timelinePlugin from 'lib/uPlotLib/plugins/timelinePlugin';
|
import timelinePlugin from 'lib/uPlotLib/plugins/timelinePlugin';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { UpdateTimeInterval } from 'store/actions';
|
import { UpdateTimeInterval } from 'store/actions';
|
||||||
@@ -48,6 +49,7 @@ function HorizontalTimelineGraph({
|
|||||||
|
|
||||||
const urlQuery = useUrlQuery();
|
const urlQuery = useUrlQuery();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
const options: uPlot.Options = useMemo(
|
const options: uPlot.Options = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -116,8 +118,18 @@ function HorizontalTimelineGraph({
|
|||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
|
|
||||||
|
tzDate: (timestamp: number): Date =>
|
||||||
|
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||||
}),
|
}),
|
||||||
[width, isDarkMode, transformedData.length, urlQuery, dispatch],
|
[
|
||||||
|
width,
|
||||||
|
isDarkMode,
|
||||||
|
transformedData.length,
|
||||||
|
urlQuery,
|
||||||
|
dispatch,
|
||||||
|
timezone.value,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
return <Uplot data={transformedData} options={options} />;
|
return <Uplot data={transformedData} options={options} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
useGetAlertRuleDetailsTimelineTable,
|
useGetAlertRuleDetailsTimelineTable,
|
||||||
useTimelineTable,
|
useTimelineTable,
|
||||||
} from 'pages/AlertDetails/hooks';
|
} from 'pages/AlertDetails/hooks';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { HTMLAttributes, useMemo, useState } from 'react';
|
import { HTMLAttributes, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
|
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
|
||||||
@@ -41,6 +42,8 @@ function TimelineTable(): JSX.Element {
|
|||||||
|
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
if (isError || !isValidRuleId || !ruleId) {
|
if (isError || !isValidRuleId || !ruleId) {
|
||||||
return <div>{t('something_went_wrong')}</div>;
|
return <div>{t('something_went_wrong')}</div>;
|
||||||
}
|
}
|
||||||
@@ -64,6 +67,7 @@ function TimelineTable(): JSX.Element {
|
|||||||
filters,
|
filters,
|
||||||
labels: labels ?? {},
|
labels: labels ?? {},
|
||||||
setFilters,
|
setFilters,
|
||||||
|
formatTimezoneAdjustedTimestamp,
|
||||||
})}
|
})}
|
||||||
onRow={handleRowClick}
|
onRow={handleRowClick}
|
||||||
dataSource={timelineData}
|
dataSource={timelineData}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import ClientSideQBSearch, {
|
|||||||
import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover';
|
import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover';
|
||||||
import { transformKeyValuesToAttributeValuesMap } from 'container/QueryBuilder/filters/utils';
|
import { transformKeyValuesToAttributeValuesMap } from 'container/QueryBuilder/filters/utils';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
|
||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
import AlertLabels, {
|
import AlertLabels, {
|
||||||
AlertLabelsProps,
|
AlertLabelsProps,
|
||||||
@@ -16,7 +17,6 @@ import AlertState from 'pages/AlertDetails/AlertHeader/AlertState/AlertState';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
|
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
|
||||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { formatEpochTimestamp } from 'utils/timeUtils';
|
|
||||||
|
|
||||||
const transformLabelsToQbKeys = (
|
const transformLabelsToQbKeys = (
|
||||||
labels: AlertRuleTimelineTableResponse['labels'],
|
labels: AlertRuleTimelineTableResponse['labels'],
|
||||||
@@ -74,10 +74,15 @@ export const timelineTableColumns = ({
|
|||||||
filters,
|
filters,
|
||||||
labels,
|
labels,
|
||||||
setFilters,
|
setFilters,
|
||||||
|
formatTimezoneAdjustedTimestamp,
|
||||||
}: {
|
}: {
|
||||||
filters: TagFilter;
|
filters: TagFilter;
|
||||||
labels: AlertLabelsProps['labels'];
|
labels: AlertLabelsProps['labels'];
|
||||||
setFilters: (filters: TagFilter) => void;
|
setFilters: (filters: TagFilter) => void;
|
||||||
|
formatTimezoneAdjustedTimestamp: (
|
||||||
|
input: TimestampInput,
|
||||||
|
format?: string,
|
||||||
|
) => string;
|
||||||
}): ColumnsType<AlertRuleTimelineTableResponse> => [
|
}): ColumnsType<AlertRuleTimelineTableResponse> => [
|
||||||
{
|
{
|
||||||
title: 'STATE',
|
title: 'STATE',
|
||||||
@@ -106,7 +111,9 @@ export const timelineTableColumns = ({
|
|||||||
dataIndex: 'unixMilli',
|
dataIndex: 'unixMilli',
|
||||||
width: 200,
|
width: 200,
|
||||||
render: (value): JSX.Element => (
|
render: (value): JSX.Element => (
|
||||||
<div className="alert-rule__created-at">{formatEpochTimestamp(value)}</div>
|
<div className="alert-rule__created-at">
|
||||||
|
{formatTimezoneAdjustedTimestamp(value, 'MMM D, YYYY ⎯ HH:mm:ss')}
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ 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';
|
||||||
|
|
||||||
@@ -20,8 +18,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 { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { user } = useAppContext();
|
||||||
const [action] = useComponentPermission(['new_alert_action'], role);
|
const [action] = useComponentPermission(['new_alert_action'], user.role);
|
||||||
|
|
||||||
const onClickEditHandler = useCallback((id: string) => {
|
const onClickEditHandler = useCallback((id: string) => {
|
||||||
history.replace(
|
history.replace(
|
||||||
|
|||||||
@@ -31,13 +31,6 @@ 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();
|
||||||
@@ -362,7 +355,7 @@ describe('Create Alert Channel', () => {
|
|||||||
expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue);
|
expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('Opsgenie', () => {
|
describe('Email', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
render(<CreateAlertChannels preType={ChannelType.Email} />);
|
render(<CreateAlertChannels preType={ChannelType.Email} />);
|
||||||
});
|
});
|
||||||
@@ -385,7 +378,9 @@ 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(screen.getByText('msteams')).toBeInTheDocument();
|
expect(
|
||||||
|
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 ', () => {
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ describe('Create Alert Channel (Normal User)', () => {
|
|||||||
expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue);
|
expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('Opsgenie', () => {
|
describe('Email', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
render(<CreateAlertChannels preType={ChannelType.Email} />);
|
render(<CreateAlertChannels preType={ChannelType.Email} />);
|
||||||
});
|
});
|
||||||
@@ -314,7 +314,8 @@ describe('Create Alert Channel (Normal User)', () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should check if the upgrade plan message is shown', () => {
|
// TODO[vikrantgupta25]: check with Shaheer
|
||||||
|
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./),
|
||||||
@@ -335,7 +336,7 @@ describe('Create Alert Channel (Normal User)', () => {
|
|||||||
screen.getByRole('button', { name: 'button_return' }),
|
screen.getByRole('button', { name: 'button_return' }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it('Should check if save and test buttons are disabled', () => {
|
it.skip('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();
|
||||||
|
|||||||
@@ -20,13 +20,6 @@ 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} />);
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ 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';
|
||||||
@@ -22,10 +20,10 @@ const { Paragraph } = Typography;
|
|||||||
|
|
||||||
function AlertChannels(): JSX.Element {
|
function AlertChannels(): JSX.Element {
|
||||||
const { t } = useTranslation(['channels']);
|
const { t } = useTranslation(['channels']);
|
||||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { user } = useAppContext();
|
||||||
const [addNewChannelPermission] = useComponentPermission(
|
const [addNewChannelPermission] = useComponentPermission(
|
||||||
['add_new_channel'],
|
['add_new_channel'],
|
||||||
role,
|
user.role,
|
||||||
);
|
);
|
||||||
const onToggleHandler = useCallback(() => {
|
const onToggleHandler = useCallback(() => {
|
||||||
history.push(ROUTES.CHANNELS_NEW);
|
history.push(ROUTES.CHANNELS_NEW);
|
||||||
|
|||||||
@@ -17,14 +17,15 @@ import getAll from 'api/errors/getAll';
|
|||||||
import getErrorCounts from 'api/errors/getErrorCounts';
|
import getErrorCounts from 'api/errors/getErrorCounts';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import useResourceAttribute from 'hooks/useResourceAttribute';
|
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||||
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
||||||
|
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { isUndefined } from 'lodash-es';
|
import { isUndefined } from 'lodash-es';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQueries } from 'react-query';
|
import { useQueries } from 'react-query';
|
||||||
@@ -155,8 +156,16 @@ function AllErrors(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [data?.error, data?.payload, t, notifications]);
|
}, [data?.error, data?.payload, t, notifications]);
|
||||||
|
|
||||||
const getDateValue = (value: string): JSX.Element => (
|
const getDateValue = (
|
||||||
<Typography>{dayjs(value).format('DD/MM/YYYY HH:mm:ss A')}</Typography>
|
value: string,
|
||||||
|
formatTimezoneAdjustedTimestamp: (
|
||||||
|
input: TimestampInput,
|
||||||
|
format?: string,
|
||||||
|
) => string,
|
||||||
|
): JSX.Element => (
|
||||||
|
<Typography>
|
||||||
|
{formatTimezoneAdjustedTimestamp(value, 'DD/MM/YYYY hh:mm:ss A')}
|
||||||
|
</Typography>
|
||||||
);
|
);
|
||||||
|
|
||||||
const filterIcon = useCallback(() => <SearchOutlined />, []);
|
const filterIcon = useCallback(() => <SearchOutlined />, []);
|
||||||
@@ -283,6 +292,8 @@ function AllErrors(): JSX.Element {
|
|||||||
[filterIcon, filterDropdownWrapper],
|
[filterIcon, filterDropdownWrapper],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
const columns: ColumnsType<Exception> = [
|
const columns: ColumnsType<Exception> = [
|
||||||
{
|
{
|
||||||
title: 'Exception Type',
|
title: 'Exception Type',
|
||||||
@@ -342,7 +353,8 @@ function AllErrors(): JSX.Element {
|
|||||||
dataIndex: 'lastSeen',
|
dataIndex: 'lastSeen',
|
||||||
width: 80,
|
width: 80,
|
||||||
key: 'lastSeen',
|
key: 'lastSeen',
|
||||||
render: getDateValue,
|
render: (value): JSX.Element =>
|
||||||
|
getDateValue(value, formatTimezoneAdjustedTimestamp),
|
||||||
sorter: true,
|
sorter: true,
|
||||||
defaultSortOrder: getDefaultOrder(
|
defaultSortOrder: getDefaultOrder(
|
||||||
getUpdatedParams,
|
getUpdatedParams,
|
||||||
@@ -355,7 +367,8 @@ function AllErrors(): JSX.Element {
|
|||||||
dataIndex: 'firstSeen',
|
dataIndex: 'firstSeen',
|
||||||
width: 80,
|
width: 80,
|
||||||
key: 'firstSeen',
|
key: 'firstSeen',
|
||||||
render: getDateValue,
|
render: (value): JSX.Element =>
|
||||||
|
getDateValue(value, formatTimezoneAdjustedTimestamp),
|
||||||
sorter: true,
|
sorter: true,
|
||||||
defaultSortOrder: getDefaultOrder(
|
defaultSortOrder: getDefaultOrder(
|
||||||
getUpdatedParams,
|
getUpdatedParams,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import getAxes from 'lib/uPlotLib/utils/getAxes';
|
|||||||
import { getUplotChartDataForAnomalyDetection } from 'lib/uPlotLib/utils/getUplotChartData';
|
import { getUplotChartDataForAnomalyDetection } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
import { getYAxisScaleForAnomalyDetection } from 'lib/uPlotLib/utils/getYAxisScale';
|
import { getYAxisScaleForAnomalyDetection } from 'lib/uPlotLib/utils/getYAxisScale';
|
||||||
import { LineChart } from 'lucide-react';
|
import { LineChart } from 'lucide-react';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
@@ -148,10 +149,12 @@ function AnomalyAlertEvaluationView({
|
|||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
width: dimensions.width,
|
width: dimensions.width,
|
||||||
height: dimensions.height - 36,
|
height: dimensions.height - 36,
|
||||||
plugins: [bandsPlugin, tooltipPlugin(isDarkMode)],
|
plugins: [bandsPlugin, tooltipPlugin(isDarkMode, timezone.value)],
|
||||||
focus: {
|
focus: {
|
||||||
alpha: 0.3,
|
alpha: 0.3,
|
||||||
},
|
},
|
||||||
@@ -256,6 +259,8 @@ function AnomalyAlertEvaluationView({
|
|||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
axes: getAxes(isDarkMode, yAxisUnit),
|
axes: getAxes(isDarkMode, yAxisUnit),
|
||||||
|
tzDate: (timestamp: number): Date =>
|
||||||
|
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = (searchText: string): void => {
|
const handleSearch = (searchText: string): void => {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { themeColors } from 'constants/theme';
|
import { themeColors } from 'constants/theme';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||||
|
|
||||||
const tooltipPlugin = (
|
const tooltipPlugin = (
|
||||||
isDarkMode: boolean,
|
isDarkMode: boolean,
|
||||||
|
timezone: string,
|
||||||
): { hooks: { init: (u: any) => void } } => {
|
): { hooks: { init: (u: any) => void } } => {
|
||||||
let tooltip: HTMLDivElement;
|
let tooltip: HTMLDivElement;
|
||||||
const tooltipLeftOffset = 10;
|
const tooltipLeftOffset = 10;
|
||||||
@@ -17,7 +19,7 @@ const tooltipPlugin = (
|
|||||||
return value.toFixed(3);
|
return value.toFixed(3);
|
||||||
}
|
}
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
return value.toLocaleString();
|
return dayjs(value).tz(timezone).format('MM/DD/YYYY, h:mm:ss A');
|
||||||
}
|
}
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return 'N/A';
|
return 'N/A';
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ 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';
|
||||||
@@ -29,10 +27,9 @@ 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, useSelector } from 'react-redux';
|
import { useDispatch } 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,
|
||||||
@@ -43,7 +40,6 @@ 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,
|
||||||
@@ -56,11 +52,18 @@ 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 { isLoggedIn, user, role } = useSelector<AppState, AppReducer>(
|
const {
|
||||||
(state) => state.app,
|
isLoggedIn,
|
||||||
);
|
user,
|
||||||
|
licenses,
|
||||||
|
isFetchingLicenses,
|
||||||
|
activeLicenseV3,
|
||||||
|
isFetchingActiveLicenseV3,
|
||||||
|
featureFlags,
|
||||||
|
isFetchingFeatureFlags,
|
||||||
|
featureFlagsFetchError,
|
||||||
|
} = useAppContext();
|
||||||
|
|
||||||
const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext();
|
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
const [
|
const [
|
||||||
@@ -98,23 +101,6 @@ 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']);
|
||||||
|
|
||||||
@@ -248,15 +234,16 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!isFetching &&
|
!isFetchingLicenses &&
|
||||||
licenseData?.payload?.onTrial &&
|
licenses &&
|
||||||
!licenseData?.payload?.trialConvertedToSubscription &&
|
licenses.onTrial &&
|
||||||
!licenseData?.payload?.workSpaceBlock &&
|
!licenses.trialConvertedToSubscription &&
|
||||||
getRemainingDays(licenseData?.payload.trialEnd) < 7
|
!licenses.workSpaceBlock &&
|
||||||
|
getRemainingDays(licenses.trialEnd) < 7
|
||||||
) {
|
) {
|
||||||
setShowTrialExpiryBanner(true);
|
setShowTrialExpiryBanner(true);
|
||||||
}
|
}
|
||||||
}, [licenseData, isFetching]);
|
}, [isFetchingLicenses, licenses]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@@ -272,11 +259,12 @@ 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 (role === 'ADMIN') {
|
if (user.role === 'ADMIN') {
|
||||||
history.push(ROUTES.BILLING);
|
history.push(ROUTES.BILLING);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -327,6 +315,41 @@ 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>
|
||||||
@@ -336,10 +359,8 @@ 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>
|
<span>{getFormattedDate(licenses?.trialEnd || Date.now())}.</span>
|
||||||
{getFormattedDate(licenseData?.payload?.trialEnd || Date.now())}.
|
{user.role === 'ADMIN' ? (
|
||||||
</span>
|
|
||||||
{role === 'ADMIN' ? (
|
|
||||||
<span>
|
<span>
|
||||||
{' '}
|
{' '}
|
||||||
Please{' '}
|
Please{' '}
|
||||||
@@ -362,7 +383,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
.
|
.
|
||||||
</span>
|
</span>
|
||||||
{role === 'ADMIN' ? (
|
{user.role === 'ADMIN' ? (
|
||||||
<span>
|
<span>
|
||||||
{' '}
|
{' '}
|
||||||
Please{' '}
|
Please{' '}
|
||||||
@@ -385,9 +406,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}>
|
<Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}>
|
||||||
{isToDisplayLayout && !renderFullScreen && (
|
{isToDisplayLayout && !renderFullScreen && <SideNav />}
|
||||||
<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>
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
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 { server } from 'mocks-server/server';
|
import { act, render, screen, waitFor } from 'tests/test-utils';
|
||||||
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(),
|
||||||
@@ -38,9 +35,7 @@ window.ResizeObserver =
|
|||||||
|
|
||||||
describe('BillingContainer', () => {
|
describe('BillingContainer', () => {
|
||||||
test('Component should render', async () => {
|
test('Component should render', async () => {
|
||||||
act(() => {
|
render(<BillingContainer />);
|
||||||
render(<BillingContainer />);
|
|
||||||
});
|
|
||||||
|
|
||||||
const dataInjection = screen.getByRole('columnheader', {
|
const dataInjection = screen.getByRole('columnheader', {
|
||||||
name: /data ingested/i,
|
name: /data ingested/i,
|
||||||
@@ -55,13 +50,18 @@ 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(/\$0/i);
|
const dollar = screen.getByText(/\$1,278.3/i);
|
||||||
expect(dollar).toBeInTheDocument();
|
await waitFor(() => expect(dollar).toBeInTheDocument());
|
||||||
|
|
||||||
const currentBill = screen.getByText('billing');
|
const currentBill = screen.getByText('billing');
|
||||||
expect(currentBill).toBeInTheDocument();
|
expect(currentBill).toBeInTheDocument();
|
||||||
@@ -69,7 +69,9 @@ describe('BillingContainer', () => {
|
|||||||
|
|
||||||
test('OnTrail', async () => {
|
test('OnTrail', async () => {
|
||||||
act(() => {
|
act(() => {
|
||||||
render(<BillingContainer />);
|
render(<BillingContainer />, undefined, undefined, {
|
||||||
|
licenses: licensesSuccessResponse.data,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const freeTrailText = await screen.findByText('Free Trial');
|
const freeTrailText = await screen.findByText('Free Trial');
|
||||||
@@ -100,14 +102,10 @@ 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 />);
|
render(<BillingContainer />, undefined, undefined, {
|
||||||
|
licenses: trialConvertedToSubscriptionResponse.data,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentBill = screen.getByText('billing');
|
const currentBill = screen.getByText('billing');
|
||||||
@@ -138,12 +136,9 @@ describe('BillingContainer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Not on ontrail', async () => {
|
test('Not on ontrail', async () => {
|
||||||
server.use(
|
const { findByText } = render(<BillingContainer />, undefined, undefined, {
|
||||||
rest.get(lisenceUrl, (req, res, ctx) =>
|
licenses: notOfTrailResponse.data,
|
||||||
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,
|
||||||
@@ -168,17 +163,4 @@ 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();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,18 +24,15 @@ 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';
|
||||||
|
|
||||||
@@ -137,9 +134,13 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
Partial<UsageResponsePayloadProps>
|
Partial<UsageResponsePayloadProps>
|
||||||
>({});
|
>({});
|
||||||
|
|
||||||
const { isFetching, data: licensesData, error: licenseError } = useLicense();
|
const {
|
||||||
|
user,
|
||||||
const { user, org } = useSelector<AppState, AppReducer>((state) => state.app);
|
org,
|
||||||
|
licenses,
|
||||||
|
isFetchingLicenses,
|
||||||
|
licensesFetchError,
|
||||||
|
} = useAppContext();
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
const handleError = useAxiosError();
|
const handleError = useAxiosError();
|
||||||
@@ -181,7 +182,7 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
|
|
||||||
setData(formattedUsageData);
|
setData(formattedUsageData);
|
||||||
|
|
||||||
if (!licensesData?.payload?.onTrial) {
|
if (!licenses?.onTrial) {
|
||||||
const remainingDays = getRemainingDays(billingPeriodEnd) - 1;
|
const remainingDays = getRemainingDays(billingPeriodEnd) - 1;
|
||||||
|
|
||||||
setHeaderText(
|
setHeaderText(
|
||||||
@@ -195,14 +196,14 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
|
|
||||||
setApiResponse(data?.payload || {});
|
setApiResponse(data?.payload || {});
|
||||||
},
|
},
|
||||||
[licensesData?.payload?.onTrial],
|
[licenses?.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?.userId],
|
[REACT_QUERY_KEY.GET_BILLING_USAGE, user?.id],
|
||||||
{
|
{
|
||||||
queryFn: () => getUsage(activeLicense?.key || ''),
|
queryFn: () => getUsage(activeLicense?.key || ''),
|
||||||
onError: handleError,
|
onError: handleError,
|
||||||
@@ -213,25 +214,29 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const activeValidLicense =
|
const activeValidLicense =
|
||||||
licensesData?.payload?.licenses?.find(
|
licenses?.licenses?.find((license) => license.isCurrent === true) || null;
|
||||||
(license) => license.isCurrent === true,
|
|
||||||
) || null;
|
|
||||||
|
|
||||||
setActiveLicense(activeValidLicense);
|
setActiveLicense(activeValidLicense);
|
||||||
|
|
||||||
if (!isFetching && licensesData?.payload?.onTrial && !licenseError) {
|
if (!isFetchingLicenses && licenses?.onTrial && !licensesFetchError) {
|
||||||
const remainingDays = getRemainingDays(licensesData?.payload?.trialEnd);
|
const remainingDays = getRemainingDays(licenses?.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(
|
||||||
licensesData?.payload?.trialEnd,
|
licenses?.trialEnd,
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [isFetching, licensesData?.payload, licenseError]);
|
}, [
|
||||||
|
licenses?.licenses,
|
||||||
|
licenses?.onTrial,
|
||||||
|
licenses?.trialEnd,
|
||||||
|
isFetchingLicenses,
|
||||||
|
licensesFetchError,
|
||||||
|
]);
|
||||||
|
|
||||||
const columns: ColumnsType<DataType> = [
|
const columns: ColumnsType<DataType> = [
|
||||||
{
|
{
|
||||||
@@ -313,7 +318,7 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleBilling = useCallback(async () => {
|
const handleBilling = useCallback(async () => {
|
||||||
if (isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription) {
|
if (isFreeTrial && !licenses?.trialConvertedToSubscription) {
|
||||||
logEvent('Billing : Upgrade Plan', {
|
logEvent('Billing : Upgrade Plan', {
|
||||||
user: pick(user, ['email', 'userId', 'name']),
|
user: pick(user, ['email', 'userId', 'name']),
|
||||||
org,
|
org,
|
||||||
@@ -340,7 +345,7 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
}, [
|
}, [
|
||||||
activeLicense?.key,
|
activeLicense?.key,
|
||||||
isFreeTrial,
|
isFreeTrial,
|
||||||
licensesData?.payload?.trialConvertedToSubscription,
|
licenses?.trialConvertedToSubscription,
|
||||||
manageCreditCard,
|
manageCreditCard,
|
||||||
updateCreditCard,
|
updateCreditCard,
|
||||||
]);
|
]);
|
||||||
@@ -452,22 +457,21 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
onClick={handleBilling}
|
onClick={handleBilling}
|
||||||
>
|
>
|
||||||
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription
|
{isFreeTrial && !licenses?.trialConvertedToSubscription
|
||||||
? t('upgrade_plan')
|
? t('upgrade_plan')
|
||||||
: t('manage_billing')}
|
: t('manage_billing')}
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{licensesData?.payload?.onTrial &&
|
{licenses?.onTrial && licenses?.trialConvertedToSubscription && (
|
||||||
licensesData?.payload?.trialConvertedToSubscription && (
|
<Typography.Text
|
||||||
<Typography.Text
|
ellipsis
|
||||||
ellipsis
|
style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }}
|
||||||
style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }}
|
>
|
||||||
>
|
{t('card_details_recieved_and_billing_info')}
|
||||||
{t('card_details_recieved_and_billing_info')}
|
</Typography.Text>
|
||||||
</Typography.Text>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLoading && !isFetchingBillingData ? (
|
{!isLoading && !isFetchingBillingData ? (
|
||||||
headerText && (
|
headerText && (
|
||||||
@@ -510,7 +514,7 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
{(isLoading || isFetchingBillingData) && renderTableSkeleton()}
|
{(isLoading || isFetchingBillingData) && renderTableSkeleton()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription && (
|
{isFreeTrial && !licenses?.trialConvertedToSubscription && (
|
||||||
<div className="upgrade-plan-benefits">
|
<div className="upgrade-plan-benefits">
|
||||||
<Row
|
<Row
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
|
|||||||
@@ -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 useFeatureFlags from 'hooks/useFeatureFlag';
|
import { useAppContext } from 'providers/App/App';
|
||||||
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,9 +13,11 @@ 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 =
|
||||||
useFeatureFlags(FeatureKeys.ANOMALY_DETECTION)?.active || false;
|
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
|
||||||
|
?.active || false;
|
||||||
|
|
||||||
const optionList = getOptionList(t, isAnomalyDetectionEnabled);
|
const optionList = getOptionList(t, isAnomalyDetectionEnabled);
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export const logAlertDefaults: AlertDef = {
|
|||||||
chQueries: {
|
chQueries: {
|
||||||
A: {
|
A: {
|
||||||
name: 'A',
|
name: 'A',
|
||||||
query: `select \ntoStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 MINUTE) AS interval, \ntoFloat64(count()) as value \nFROM signoz_logs.distributed_logs_v2 \nWHERE timestamp BETWEEN {{.start_timestamp_nano}} AND {{.end_timestamp_nano}} \nGROUP BY interval;\n\n-- available variables:\n-- \t{{.start_timestamp_nano}}\n-- \t{{.end_timestamp_nano}}\n\n-- required columns (or alias):\n-- \tvalue\n-- \tinterval`,
|
query: `select \ntoStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 MINUTE) AS interval, \ntoFloat64(count()) as value \nFROM signoz_logs.distributed_logs_v2 \nWHERE timestamp BETWEEN {{.start_timestamp_nano}} AND {{.end_timestamp_nano}} \nAND ts_bucket_start BETWEEN {{.start_timestamp}} - 1800 AND {{.end_timestamp}} \nGROUP BY interval;\n\n-- Please check docs here https://signoz.io/docs/userguide/logs_clickhouse_queries/\n\n-- available variables:\n-- \t{{.start_timestamp_nano}}\n-- \t{{.end_timestamp_nano}}\n-- \t{{.start_timestamp}}\n-- \t{{.end_timestamp}}\n\n-- required columns (or alias):\n-- \tvalue\n-- \tinterval`,
|
||||||
legend: '',
|
legend: '',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
@@ -149,7 +149,7 @@ export const traceAlertDefaults: AlertDef = {
|
|||||||
chQueries: {
|
chQueries: {
|
||||||
A: {
|
A: {
|
||||||
name: 'A',
|
name: 'A',
|
||||||
query: `SELECT \n\ttoStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS interval, \n\tstringTagMap['peer.service'] AS op_name, \n\ttoFloat64(avg(durationNano)) AS value \nFROM signoz_traces.distributed_signoz_index_v2 \nWHERE stringTagMap['peer.service']!='' \nAND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}} \nGROUP BY (op_name, interval);\n\n-- available variables:\n-- \t{{.start_datetime}}\n-- \t{{.end_datetime}}\n\n-- required column alias:\n-- \tvalue\n-- \tinterval`,
|
query: `SELECT \n\ttoStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS interval, \n\tresource_string_service$$name AS \`service.name\`, \n\ttoFloat64(avg(duration_nano)) AS value \nFROM signoz_traces.distributed_signoz_index_v3 \nWHERE resource_string_service$$name !='' \nAND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}} \nAND ts_bucket_start BETWEEN {{.start_timestamp}} - 1800 AND {{.end_timestamp}} \nGROUP BY (\`service.name\`, interval);\n\n-- Please check docs here https://signoz.io/docs/userguide/writing-clickhouse-traces-query/\n\n-- available variables:\n-- \t{{.start_datetime}}\n-- \t{{.end_datetime}}\n-- \t{{.start_timestamp}}\n-- \t{{.end_timestamp}}\n\n-- required column alias:\n-- \tvalue\n-- \tinterval`,
|
||||||
legend: '',
|
legend: '',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import getNextPrevId from 'api/errors/getNextPrevId';
|
|||||||
import Editor from 'components/Editor';
|
import Editor from 'components/Editor';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
import { getNanoSeconds } from 'container/AllError/utils';
|
import { getNanoSeconds } from 'container/AllError/utils';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { isUndefined } from 'lodash-es';
|
import { isUndefined } from 'lodash-es';
|
||||||
import { urlKey } from 'pages/ErrorDetails/utils';
|
import { urlKey } from 'pages/ErrorDetails/utils';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
@@ -103,8 +103,6 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const timeStamp = dayjs(errorDetail.timestamp);
|
|
||||||
|
|
||||||
const data: { key: string; value: string }[] = Object.keys(errorDetail)
|
const data: { key: string; value: string }[] = Object.keys(errorDetail)
|
||||||
.filter((e) => !keyToExclude.includes(e))
|
.filter((e) => !keyToExclude.includes(e))
|
||||||
.map((key) => ({
|
.map((key) => ({
|
||||||
@@ -136,6 +134,8 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
const { formatTimezoneAdjustedTimestamp } = useTimezone();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Typography>{errorDetail.exceptionType}</Typography>
|
<Typography>{errorDetail.exceptionType}</Typography>
|
||||||
@@ -145,7 +145,12 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
|||||||
<EventContainer>
|
<EventContainer>
|
||||||
<div>
|
<div>
|
||||||
<Typography>Event {errorDetail.errorId}</Typography>
|
<Typography>Event {errorDetail.errorId}</Typography>
|
||||||
<Typography>{timeStamp.format('MMM DD YYYY hh:mm:ss A')}</Typography>
|
<Typography>
|
||||||
|
{formatTimezoneAdjustedTimestamp(
|
||||||
|
errorDetail.timestamp,
|
||||||
|
'DD/MM/YYYY hh:mm:ss A (UTC Z)',
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Space align="end" direction="horizontal">
|
<Space align="end" direction="horizontal">
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useAppContext } from 'providers/App/App';
|
||||||
import {
|
import {
|
||||||
CSSProperties,
|
CSSProperties,
|
||||||
Dispatch,
|
Dispatch,
|
||||||
@@ -56,15 +57,12 @@ 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';
|
||||||
@@ -114,6 +112,7 @@ function ExplorerOptions({
|
|||||||
panelType,
|
panelType,
|
||||||
isStagedQueryUpdated,
|
isStagedQueryUpdated,
|
||||||
redirectWithQueryBuilderData,
|
redirectWithQueryBuilderData,
|
||||||
|
isDefaultQuery,
|
||||||
} = useQueryBuilder();
|
} = useQueryBuilder();
|
||||||
|
|
||||||
const handleSaveViewModalToggle = (): void => {
|
const handleSaveViewModalToggle = (): void => {
|
||||||
@@ -133,7 +132,7 @@ function ExplorerOptions({
|
|||||||
setIsSaveModalOpen(false);
|
setIsSaveModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { user } = useAppContext();
|
||||||
|
|
||||||
const handleConditionalQueryModification = useCallback((): string => {
|
const handleConditionalQueryModification = useCallback((): string => {
|
||||||
if (
|
if (
|
||||||
@@ -472,7 +471,7 @@ function ExplorerOptions({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isEditDeleteSupported = allowedRoles.includes(role as string);
|
const isEditDeleteSupported = allowedRoles.includes(user.role as string);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
isRecentlyUsedSavedViewSelected,
|
isRecentlyUsedSavedViewSelected,
|
||||||
@@ -480,6 +479,11 @@ 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) || '{}',
|
||||||
);
|
);
|
||||||
@@ -501,12 +505,18 @@ function ExplorerOptions({
|
|||||||
setIsRecentlyUsedSavedViewSelected(false);
|
setIsRecentlyUsedSavedViewSelected(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (): void => clearTimeout(timeoutId);
|
// eslint-disable-next-line consistent-return
|
||||||
|
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,
|
||||||
|
|||||||
@@ -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,15 +39,21 @@ function FormAlertChannels({
|
|||||||
editing = false,
|
editing = false,
|
||||||
}: FormAlertChannelsProps): JSX.Element {
|
}: FormAlertChannelsProps): JSX.Element {
|
||||||
const { t } = useTranslation('channels');
|
const { t } = useTranslation('channels');
|
||||||
const isUserOnEEPlan = useFeatureFlags(FeatureKeys.ENTERPRISE_PLAN);
|
const { featureFlags } = useAppContext();
|
||||||
|
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 hasFeature = useFeatureFlags(
|
const featureKey = isFeatureKeys(feature)
|
||||||
isFeatureKeys(feature) ? feature : FeatureKeys.ALERT_CHANNEL_SLACK,
|
? feature
|
||||||
);
|
: FeatureKeys.ALERT_CHANNEL_SLACK;
|
||||||
|
const hasFeature = featureFlags?.find((flag) => flag.name === featureKey);
|
||||||
|
|
||||||
const isOssFeature = useFeatureFlags(FeatureKeys.OSS);
|
const isOssFeature = featureFlags?.find(
|
||||||
|
(flag) => flag.name === FeatureKeys.OSS,
|
||||||
|
);
|
||||||
|
|
||||||
const renderSettings = (): ReactElement | null => {
|
const renderSettings = (): ReactElement | null => {
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -8,13 +8,11 @@ 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';
|
||||||
|
|
||||||
@@ -45,10 +43,10 @@ function BasicInfo({
|
|||||||
const { t } = useTranslation('alerts');
|
const { t } = useTranslation('alerts');
|
||||||
|
|
||||||
const channels = useFetch(getChannels);
|
const channels = useFetch(getChannels);
|
||||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { user } = useAppContext();
|
||||||
const [addNewChannelPermission] = useComponentPermission(
|
const [addNewChannelPermission] = useComponentPermission(
|
||||||
['add_new_channel'],
|
['add_new_channel'],
|
||||||
role,
|
user.role,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ 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';
|
||||||
|
|
||||||
@@ -49,10 +47,10 @@ function ChannelSelect({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { user } = useAppContext();
|
||||||
const [addNewChannelPermission] = useComponentPermission(
|
const [addNewChannelPermission] = useComponentPermission(
|
||||||
['add_new_channel'],
|
['add_new_channel'],
|
||||||
role,
|
user.role,
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderOptions = (): ReactNode[] => {
|
const renderOptions = (): ReactNode[] => {
|
||||||
|
|||||||
@@ -18,13 +18,14 @@ 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 { 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';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
@@ -35,6 +36,7 @@ import { AlertDef } from 'types/api/alerts/def';
|
|||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import uPlot from 'uplot';
|
||||||
import { getGraphType } from 'utils/getGraphType';
|
import { getGraphType } from 'utils/getGraphType';
|
||||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||||
import { getTimeRange } from 'utils/getTimeRange';
|
import { getTimeRange } from 'utils/getTimeRange';
|
||||||
@@ -82,6 +84,8 @@ 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);
|
||||||
@@ -201,6 +205,8 @@ function ChartPreview({
|
|||||||
[dispatch, location.pathname, urlQuery],
|
[dispatch, location.pathname, urlQuery],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
const options = useMemo(
|
const options = useMemo(
|
||||||
() =>
|
() =>
|
||||||
getUPlotChartOptions({
|
getUPlotChartOptions({
|
||||||
@@ -236,6 +242,9 @@ function ChartPreview({
|
|||||||
softMax: null,
|
softMax: null,
|
||||||
softMin: null,
|
softMin: null,
|
||||||
panelType: graphType,
|
panelType: graphType,
|
||||||
|
tzDate: (timestamp: number) =>
|
||||||
|
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
|
||||||
|
timezone: timezone.value,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
@@ -250,6 +259,7 @@ function ChartPreview({
|
|||||||
optionName,
|
optionName,
|
||||||
alertDef?.condition.targetUnit,
|
alertDef?.condition.targetUnit,
|
||||||
graphType,
|
graphType,
|
||||||
|
timezone.value,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -262,7 +272,8 @@ function ChartPreview({
|
|||||||
chartData && !queryResponse.isError && !queryResponse.isLoading;
|
chartData && !queryResponse.isError && !queryResponse.isLoading;
|
||||||
|
|
||||||
const isAnomalyDetectionEnabled =
|
const isAnomalyDetectionEnabled =
|
||||||
useFeatureFlags(FeatureKeys.ANOMALY_DETECTION)?.active || false;
|
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
|
||||||
|
?.active || false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="alert-chart-container" ref={graphRef}>
|
<div className="alert-chart-container" ref={graphRef}>
|
||||||
|
|||||||
@@ -14,12 +14,9 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
|||||||
import { Atom, Play, Terminal } from 'lucide-react';
|
import { Atom, Play, Terminal } from 'lucide-react';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, 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 } from 'types/api/alerts/def';
|
import { AlertDef } from 'types/api/alerts/def';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import AppReducer from 'types/reducer/app';
|
|
||||||
|
|
||||||
import ChQuerySection from './ChQuerySection';
|
import ChQuerySection from './ChQuerySection';
|
||||||
import PromqlSection from './PromqlSection';
|
import PromqlSection from './PromqlSection';
|
||||||
@@ -38,14 +35,9 @@ function QuerySection({
|
|||||||
const { t } = useTranslation('alerts');
|
const { t } = useTranslation('alerts');
|
||||||
const [currentTab, setCurrentTab] = useState(queryCategory);
|
const [currentTab, setCurrentTab] = useState(queryCategory);
|
||||||
|
|
||||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
// TODO[vikrantgupta25] : check if this is still required ??
|
||||||
(state) => state.app,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleQueryCategoryChange = (queryType: string): void => {
|
const handleQueryCategoryChange = (queryType: string): void => {
|
||||||
featureResponse.refetch().then(() => {
|
setQueryCategory(queryType as EQueryType);
|
||||||
setQueryCategory(queryType as EQueryType);
|
|
||||||
});
|
|
||||||
setCurrentTab(queryType as EQueryType);
|
setCurrentTab(queryType as EQueryType);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
import './FormAlertRules.styles.scss';
|
import './FormAlertRules.styles.scss';
|
||||||
|
|
||||||
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
||||||
import {
|
import { Button, FormInstance, Modal, SelectProps, Typography } from 'antd';
|
||||||
Button,
|
|
||||||
FormInstance,
|
|
||||||
Modal,
|
|
||||||
SelectProps,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
} from 'antd';
|
|
||||||
import saveAlertApi from 'api/alerts/save';
|
import saveAlertApi from 'api/alerts/save';
|
||||||
import testAlertApi from 'api/alerts/testAlert';
|
import testAlertApi from 'api/alerts/testAlert';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
@@ -23,10 +16,6 @@ import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
|
|||||||
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters';
|
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
import useFeatureFlag, {
|
|
||||||
MESSAGE,
|
|
||||||
useIsFeatureDisabled,
|
|
||||||
} from 'hooks/useFeatureFlag';
|
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
@@ -35,6 +24,7 @@ import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQu
|
|||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { BellDot, ExternalLink } from 'lucide-react';
|
import { BellDot, ExternalLink } from 'lucide-react';
|
||||||
import Tabs2 from 'periscope/components/Tabs2';
|
import Tabs2 from 'periscope/components/Tabs2';
|
||||||
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQueryClient } from 'react-query';
|
import { useQueryClient } from 'react-query';
|
||||||
@@ -96,6 +86,7 @@ function FormAlertRules({
|
|||||||
}: FormAlertRuleProps): JSX.Element {
|
}: FormAlertRuleProps): JSX.Element {
|
||||||
// init namespace for translations
|
// init namespace for translations
|
||||||
const { t } = useTranslation('alerts');
|
const { t } = useTranslation('alerts');
|
||||||
|
const { featureFlags } = useAppContext();
|
||||||
|
|
||||||
const { selectedTime: globalSelectedInterval } = useSelector<
|
const { selectedTime: globalSelectedInterval } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
@@ -476,9 +467,9 @@ function FormAlertRules({
|
|||||||
panelType,
|
panelType,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isAlertAvailable = useIsFeatureDisabled(
|
const isAlertAvailable =
|
||||||
FeatureKeys.QUERY_BUILDER_ALERTS,
|
!featureFlags?.find((flag) => flag.name === FeatureKeys.QUERY_BUILDER_ALERTS)
|
||||||
);
|
?.active || false;
|
||||||
|
|
||||||
const saveRule = useCallback(async () => {
|
const saveRule = useCallback(async () => {
|
||||||
if (!isFormValid()) {
|
if (!isFormValid()) {
|
||||||
@@ -766,7 +757,8 @@ function FormAlertRules({
|
|||||||
];
|
];
|
||||||
|
|
||||||
const isAnomalyDetectionEnabled =
|
const isAnomalyDetectionEnabled =
|
||||||
useFeatureFlag(FeatureKeys.ANOMALY_DETECTION)?.active || false;
|
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
|
||||||
|
?.active || false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -866,22 +858,20 @@ function FormAlertRules({
|
|||||||
{renderBasicInfo()}
|
{renderBasicInfo()}
|
||||||
</div>
|
</div>
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<Tooltip title={isAlertAvailableToSave ? MESSAGE.ALERT : ''}>
|
<ActionButton
|
||||||
<ActionButton
|
loading={loading || false}
|
||||||
loading={loading || false}
|
type="primary"
|
||||||
type="primary"
|
onClick={onSaveHandler}
|
||||||
onClick={onSaveHandler}
|
icon={<SaveOutlined />}
|
||||||
icon={<SaveOutlined />}
|
disabled={
|
||||||
disabled={
|
isAlertNameMissing ||
|
||||||
isAlertNameMissing ||
|
isAlertAvailableToSave ||
|
||||||
isAlertAvailableToSave ||
|
!isChannelConfigurationValid ||
|
||||||
!isChannelConfigurationValid ||
|
queryStatus === 'error'
|
||||||
queryStatus === 'error'
|
}
|
||||||
}
|
>
|
||||||
>
|
{isNewRule ? t('button_createrule') : t('button_savechanges')}
|
||||||
{isNewRule ? t('button_createrule') : t('button_savechanges')}
|
</ActionButton>
|
||||||
</ActionButton>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
loading={loading || false}
|
loading={loading || false}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Popover, Typography } from 'antd';
|
|||||||
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { toFixed } from 'utils/toFixed';
|
import { toFixed } from 'utils/toFixed';
|
||||||
|
|
||||||
@@ -32,13 +33,17 @@ function Span(props: SpanLengthProps): JSX.Element {
|
|||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
|
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
|
||||||
|
|
||||||
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.scrollTop = document.documentElement.clientHeight;
|
document.documentElement.scrollTop = document.documentElement.clientHeight;
|
||||||
document.documentElement.scrollLeft = document.documentElement.clientWidth;
|
document.documentElement.scrollLeft = document.documentElement.clientWidth;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getContent = (): JSX.Element => {
|
const getContent = (): JSX.Element => {
|
||||||
const timeStamp = dayjs(startTime).format('h:mm:ss:SSS A');
|
const timeStamp = dayjs(startTime)
|
||||||
|
.tz(timezone.value)
|
||||||
|
.format('h:mm:ss:SSS A (UTC Z)');
|
||||||
const startTimeInMs = startTime - globalStart;
|
const startTimeInMs = startTime - globalStart;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
|
|||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import find from 'lodash-es/find';
|
import find from 'lodash-es/find';
|
||||||
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { UseQueryResult } from 'react-query';
|
import { UseQueryResult } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { useInterval } from 'react-use';
|
import { useInterval } from 'react-use';
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import {
|
import {
|
||||||
IDiskType,
|
IDiskType,
|
||||||
@@ -24,7 +23,6 @@ import {
|
|||||||
PayloadPropsMetrics as GetRetentionPeriodMetricsPayload,
|
PayloadPropsMetrics as GetRetentionPeriodMetricsPayload,
|
||||||
PayloadPropsTraces as GetRetentionPeriodTracesPayload,
|
PayloadPropsTraces as GetRetentionPeriodTracesPayload,
|
||||||
} from 'types/api/settings/getRetention';
|
} from 'types/api/settings/getRetention';
|
||||||
import AppReducer from 'types/reducer/app';
|
|
||||||
import { isCloudUser } from 'utils/app';
|
import { isCloudUser } from 'utils/app';
|
||||||
|
|
||||||
import Retention from './Retention';
|
import Retention from './Retention';
|
||||||
@@ -68,11 +66,11 @@ function GeneralSettings({
|
|||||||
logsTtlValuesPayload,
|
logsTtlValuesPayload,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { user } = useAppContext();
|
||||||
|
|
||||||
const [setRetentionPermission] = useComponentPermission(
|
const [setRetentionPermission] = useComponentPermission(
|
||||||
['set_retention_period'],
|
['set_retention_period'],
|
||||||
role,
|
user.role,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ import { Typography } from 'antd';
|
|||||||
import getDisks from 'api/disks/getDisks';
|
import getDisks from 'api/disks/getDisks';
|
||||||
import getRetentionPeriodApi from 'api/settings/getRetention';
|
import getRetentionPeriodApi from 'api/settings/getRetention';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQueries } from 'react-query';
|
import { useQueries } 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 { TTTLType } from 'types/api/settings/common';
|
import { TTTLType } from 'types/api/settings/common';
|
||||||
import { PayloadProps as GetRetentionPeriodAPIPayloadProps } from 'types/api/settings/getRetention';
|
import { PayloadProps as GetRetentionPeriodAPIPayloadProps } from 'types/api/settings/getRetention';
|
||||||
import AppReducer from 'types/reducer/app';
|
|
||||||
|
|
||||||
import GeneralSettingsContainer from './GeneralSettings';
|
import GeneralSettingsContainer from './GeneralSettings';
|
||||||
|
|
||||||
@@ -19,7 +17,7 @@ type TRetentionAPIReturn<T extends TTTLType> = Promise<
|
|||||||
|
|
||||||
function GeneralSettings(): JSX.Element {
|
function GeneralSettings(): JSX.Element {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { user } = useAppContext();
|
||||||
|
|
||||||
const [
|
const [
|
||||||
getRetentionPeriodMetricsApiResponse,
|
getRetentionPeriodMetricsApiResponse,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user