Compare commits
15 Commits
v0.65.0-cl
...
dashboard-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6fd19b363 | ||
|
|
e50a773fa9 | ||
|
|
cb91fee7c3 | ||
|
|
351178ef34 | ||
|
|
4b6e934510 | ||
|
|
99fb8c2a64 | ||
|
|
ae98aaad2d | ||
|
|
23d808af08 | ||
|
|
c991ee6239 | ||
|
|
f098518faa | ||
|
|
421d355e29 | ||
|
|
eb75e636e8 | ||
|
|
f121240c82 | ||
|
|
a60dbf7f89 | ||
|
|
fa4aeae508 |
1
.github/workflows/build.yaml
vendored
1
.github/workflows/build.yaml
vendored
@@ -3,6 +3,7 @@ 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:
|
||||||
- main
|
- develop
|
||||||
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/main/sample-apps/hotrod/hotrod.yaml
|
kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/develop/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,8 +2,7 @@ name: Jest Coverage - changed files
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches: develop
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -12,7 +11,7 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: "refs/heads/main"
|
ref: "refs/heads/develop"
|
||||||
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
|
||||||
|
|||||||
1
.github/workflows/push.yaml
vendored
1
.github/workflows/push.yaml
vendored
@@ -4,6 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- develop
|
||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
|
|
||||||
|
|||||||
1
.github/workflows/sonar.yml
vendored
1
.github/workflows/sonar.yml
vendored
@@ -3,6 +3,7 @@ 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 main branch
|
# Trigger deployment only on push to develop branch
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- develop
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
name: Deploy latest main branch to staging
|
name: Deploy latest develop 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 main
|
git checkout develop
|
||||||
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/main/sample-apps/hotrod/hotrod-install.sh \
|
curl -sL https://github.com/SigNoz/signoz/raw/develop/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/main/sample-apps/hotrod/hotrod-delete.sh \
|
curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-delete.sh \
|
||||||
| HOTROD_NAMESPACE=sample-application bash
|
| HOTROD_NAMESPACE=sample-application bash
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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/main/sample-apps/hotrod/hotrod.yaml
|
kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/develop/sample-apps/hotrod/hotrod.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
To generate load:
|
To generate load:
|
||||||
|
|||||||
@@ -146,12 +146,11 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.64.0
|
image: signoz/query-service:0.61.0
|
||||||
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" # pprof port
|
# - "6060:6060" # pprof port
|
||||||
@@ -187,7 +186,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:0.64.0
|
image: signoz/frontend:0.61.0
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
@@ -200,7 +199,7 @@ services:
|
|||||||
- ../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.16
|
image: signoz/signoz-otel-collector:0.111.14
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
@@ -238,7 +237,7 @@ services:
|
|||||||
- query-service
|
- query-service
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:0.111.16
|
image: signoz/signoz-schema-migrator:0.111.14
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ 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,7 +69,7 @@ services:
|
|||||||
- --storage.path=/data
|
- --storage.path=/data
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.16}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.14}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "sync"
|
- "sync"
|
||||||
@@ -86,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.16
|
image: signoz/signoz-otel-collector:0.111.14
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ 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"
|
||||||
|
|||||||
@@ -162,13 +162,12 @@ 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`
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:${DOCKER_TAG:-0.64.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.61.0}
|
||||||
container_name: signoz-query-service
|
container_name: signoz-query-service
|
||||||
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" # pprof port
|
# - "6060:6060" # pprof port
|
||||||
@@ -202,7 +201,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.64.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.61.0}
|
||||||
container_name: signoz-frontend
|
container_name: signoz-frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -214,7 +213,7 @@ services:
|
|||||||
- ../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.16}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.14}
|
||||||
container_name: otel-migrator-sync
|
container_name: otel-migrator-sync
|
||||||
command:
|
command:
|
||||||
- "sync"
|
- "sync"
|
||||||
@@ -229,7 +228,7 @@ services:
|
|||||||
# condition: service_healthy
|
# condition: service_healthy
|
||||||
|
|
||||||
otel-collector-migrator-async:
|
otel-collector-migrator-async:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.16}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.14}
|
||||||
container_name: otel-migrator-async
|
container_name: otel-migrator-async
|
||||||
command:
|
command:
|
||||||
- "async"
|
- "async"
|
||||||
@@ -246,7 +245,7 @@ services:
|
|||||||
# condition: service_healthy
|
# condition: service_healthy
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.16}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.14}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -167,14 +167,13 @@ 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`
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:${DOCKER_TAG:-0.64.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.61.0}
|
||||||
container_name: signoz-query-service
|
container_name: signoz-query-service
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"-config=/root/config/prometheus.yml",
|
"-config=/root/config/prometheus.yml",
|
||||||
"-gateway-url=https://api.staging.signoz.cloud",
|
"-gateway-url=https://api.staging.signoz.cloud",
|
||||||
"--use-logs-new-schema=true",
|
"--use-logs-new-schema=true"
|
||||||
"--use-trace-new-schema=true"
|
|
||||||
]
|
]
|
||||||
# ports:
|
# ports:
|
||||||
# - "6060:6060" # pprof port
|
# - "6060:6060" # pprof port
|
||||||
@@ -209,7 +208,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.64.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.61.0}
|
||||||
container_name: signoz-frontend
|
container_name: signoz-frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -221,7 +220,7 @@ services:
|
|||||||
- ../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.16}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.14}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
@@ -235,7 +234,7 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.16}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.14}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -119,7 +119,6 @@ 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:
|
||||||
|
|||||||
@@ -13,3 +13,8 @@ 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
|
||||||
@@ -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": "API Keys",
|
"api_keys": "Access Tokens",
|
||||||
"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 | API Keys",
|
"API_KEYS": "SigNoz | Access Tokens",
|
||||||
"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}} key? Deleting a key is irreversible and cannot be undone."
|
"delete_confirm_message": "Are you sure you want to delete {{keyName}} token? Deleting a token is irreversible and cannot be undone."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "API Keys",
|
"api_keys": "Access Tokens",
|
||||||
"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 | API Keys",
|
"API_KEYS": "SigNoz | Access Tokens",
|
||||||
"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,16 +1,29 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
|
||||||
import getOrgUser from 'api/user/getOrgUser';
|
import getOrgUser from 'api/user/getOrgUser';
|
||||||
|
import loginApi from 'api/user/login';
|
||||||
|
import { Logout } from 'api/utils';
|
||||||
|
import Spinner from 'components/Spinner';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
|
import useLicense from 'hooks/useLicense';
|
||||||
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { isEmpty } from 'lodash-es';
|
import { isEmpty, isNull } from 'lodash-es';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { ReactChild, useCallback, useEffect, useMemo, useState } from 'react';
|
import { ReactChild, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { matchPath, useLocation } from 'react-router-dom';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { matchPath, Redirect, useLocation } from 'react-router-dom';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { getInitialUserTokenRefreshToken } from 'store/utils';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { UPDATE_USER_IS_FETCH } from 'types/actions/app';
|
||||||
import { LicenseState, LicenseStatus } from 'types/api/licensesV3/getActive';
|
import { LicenseState, LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||||
import { Organization } from 'types/api/user/getOrganization';
|
import { Organization } from 'types/api/user/getOrganization';
|
||||||
|
import 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';
|
||||||
|
|
||||||
@@ -18,28 +31,32 @@ import routes, {
|
|||||||
LIST_LICENSES,
|
LIST_LICENSES,
|
||||||
oldNewRoutesMapping,
|
oldNewRoutesMapping,
|
||||||
oldRoutes,
|
oldRoutes,
|
||||||
ROUTES_NOT_TO_BE_OVERRIDEN,
|
|
||||||
SUPPORT_ROUTE,
|
|
||||||
} from './routes';
|
} from './routes';
|
||||||
|
import afterLogin from './utils';
|
||||||
|
|
||||||
function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { pathname } = location;
|
const { pathname } = location;
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
org,
|
org,
|
||||||
orgPreferences,
|
orgPreferences,
|
||||||
user,
|
user,
|
||||||
|
role,
|
||||||
|
isUserFetching,
|
||||||
|
isUserFetchingError,
|
||||||
isLoggedIn: isLoggedInState,
|
isLoggedIn: isLoggedInState,
|
||||||
isFetchingOrgPreferences,
|
isFetchingOrgPreferences,
|
||||||
licenses,
|
} = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
isFetchingLicenses,
|
|
||||||
activeLicenseV3,
|
const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext();
|
||||||
isFetchingActiveLicenseV3,
|
|
||||||
} = useAppContext();
|
|
||||||
const mapRoutes = useMemo(
|
const mapRoutes = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new Map(
|
new Map(
|
||||||
[...routes, LIST_LICENSES, SUPPORT_ROUTE].map((e) => {
|
[...routes, LIST_LICENSES].map((e) => {
|
||||||
const currentPath = matchPath(pathname, {
|
const currentPath = matchPath(pathname, {
|
||||||
path: e.path,
|
path: e.path,
|
||||||
});
|
});
|
||||||
@@ -48,13 +65,52 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
),
|
),
|
||||||
[pathname],
|
[pathname],
|
||||||
);
|
);
|
||||||
const isOldRoute = oldRoutes.indexOf(pathname) > -1;
|
|
||||||
const currentRoute = mapRoutes.get('current');
|
const isOnboardingComplete = useMemo(
|
||||||
|
() =>
|
||||||
|
orgPreferences?.find(
|
||||||
|
(preference: Record<string, any>) => preference.key === 'ORG_ONBOARDING',
|
||||||
|
)?.value,
|
||||||
|
[orgPreferences],
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: licensesData,
|
||||||
|
isFetching: isFetchingLicensesData,
|
||||||
|
} = useLicense();
|
||||||
|
|
||||||
|
const { t } = useTranslation(['common']);
|
||||||
|
|
||||||
const isCloudUserVal = isCloudUser();
|
const isCloudUserVal = isCloudUser();
|
||||||
|
|
||||||
|
const localStorageUserAuthToken = getInitialUserTokenRefreshToken();
|
||||||
|
|
||||||
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
|
|
||||||
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
|
const currentRoute = mapRoutes.get('current');
|
||||||
|
|
||||||
|
const isOldRoute = oldRoutes.indexOf(pathname) > -1;
|
||||||
|
|
||||||
const [orgData, setOrgData] = useState<Organization | undefined>(undefined);
|
const [orgData, setOrgData] = useState<Organization | undefined>(undefined);
|
||||||
|
|
||||||
const { data: orgUsers, isFetching: isFetchingOrgUsers } = useQuery({
|
const isLocalStorageLoggedIn =
|
||||||
|
getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true';
|
||||||
|
|
||||||
|
const navigateToLoginIfNotLoggedIn = (isLoggedIn = isLoggedInState): void => {
|
||||||
|
dispatch({
|
||||||
|
type: UPDATE_USER_IS_FETCH,
|
||||||
|
payload: {
|
||||||
|
isUserFetching: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
history.push(ROUTES.LOGIN, { from: pathname });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: orgUsers, isLoading: isLoadingOrgUsers } = useQuery({
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
if (orgData && orgData.id !== undefined) {
|
if (orgData && orgData.id !== undefined) {
|
||||||
return getOrgUser({
|
return getOrgUser({
|
||||||
@@ -64,10 +120,10 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
return undefined;
|
return undefined;
|
||||||
},
|
},
|
||||||
queryKey: ['getOrgUser'],
|
queryKey: ['getOrgUser'],
|
||||||
enabled: !isEmpty(orgData) && user.role === 'ADMIN',
|
enabled: !isEmpty(orgData),
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkFirstTimeUser = useCallback((): boolean => {
|
const checkFirstTimeUser = (): boolean => {
|
||||||
const users = orgUsers?.payload || [];
|
const users = orgUsers?.payload || [];
|
||||||
|
|
||||||
const remainingUsers = users.filter(
|
const remainingUsers = users.filter(
|
||||||
@@ -75,80 +131,154 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return remainingUsers.length === 1;
|
return remainingUsers.length === 1;
|
||||||
}, [orgUsers?.payload]);
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
// Check if the onboarding should be shown based on the org users and onboarding completion status, wait for org users and preferences to load
|
||||||
|
const shouldShowOnboarding = (): boolean => {
|
||||||
|
// Only run this effect if the org users and preferences are loaded
|
||||||
|
|
||||||
|
if (!isLoadingOrgUsers && !isFetchingOrgPreferences) {
|
||||||
|
const isFirstUser = checkFirstTimeUser();
|
||||||
|
|
||||||
|
// Redirect to get started if it's not the first user or if the onboarding is complete
|
||||||
|
return isFirstUser && !isOnboardingComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRedirectForOrgOnboarding = (key: string): void => {
|
||||||
if (
|
if (
|
||||||
|
isLoggedInState &&
|
||||||
isCloudUserVal &&
|
isCloudUserVal &&
|
||||||
!isFetchingOrgPreferences &&
|
!isFetchingOrgPreferences &&
|
||||||
orgPreferences &&
|
!isLoadingOrgUsers &&
|
||||||
!isFetchingOrgUsers &&
|
!isEmpty(orgUsers?.payload) &&
|
||||||
orgUsers &&
|
!isNull(orgPreferences)
|
||||||
orgUsers.payload
|
|
||||||
) {
|
) {
|
||||||
const isOnboardingComplete = orgPreferences?.find(
|
if (key === 'ONBOARDING' && isOnboardingComplete) {
|
||||||
(preference: Record<string, any>) => preference.key === 'ORG_ONBOARDING',
|
history.push(ROUTES.APPLICATION);
|
||||||
)?.value;
|
}
|
||||||
|
|
||||||
const isFirstUser = checkFirstTimeUser();
|
const isFirstTimeUser = checkFirstTimeUser();
|
||||||
if (
|
|
||||||
isFirstUser &&
|
if (isFirstTimeUser && !isOnboardingComplete) {
|
||||||
!isOnboardingComplete &&
|
|
||||||
// if the current route is allowed to be overriden by org onboarding then only do the same
|
|
||||||
!ROUTES_NOT_TO_BE_OVERRIDEN.includes(pathname)
|
|
||||||
) {
|
|
||||||
history.push(ROUTES.ONBOARDING);
|
history.push(ROUTES.ONBOARDING);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
|
||||||
checkFirstTimeUser,
|
if (!isCloudUserVal && key === 'ONBOARDING') {
|
||||||
isCloudUserVal,
|
history.push(ROUTES.APPLICATION);
|
||||||
isFetchingOrgPreferences,
|
}
|
||||||
isFetchingOrgUsers,
|
};
|
||||||
orgPreferences,
|
|
||||||
orgUsers,
|
const handleUserLoginIfTokenPresent = async (
|
||||||
pathname,
|
key: keyof typeof ROUTES,
|
||||||
]);
|
): Promise<void> => {
|
||||||
|
if (localStorageUserAuthToken?.refreshJwt) {
|
||||||
|
// localstorage token is present
|
||||||
|
|
||||||
|
// renew web access token
|
||||||
|
const response = await loginApi({
|
||||||
|
refreshToken: localStorageUserAuthToken?.refreshJwt,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
const route = routePermission[key];
|
||||||
|
|
||||||
|
// get all resource and put it over redux
|
||||||
|
const userResponse = await afterLogin(
|
||||||
|
response.payload.userId,
|
||||||
|
response.payload.accessJwt,
|
||||||
|
response.payload.refreshJwt,
|
||||||
|
);
|
||||||
|
|
||||||
|
handleRedirectForOrgOnboarding(key);
|
||||||
|
|
||||||
|
if (
|
||||||
|
userResponse &&
|
||||||
|
route &&
|
||||||
|
route.find((e) => e === userResponse.payload.role) === undefined
|
||||||
|
) {
|
||||||
|
history.push(ROUTES.UN_AUTHORIZED);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logout();
|
||||||
|
|
||||||
|
notifications.error({
|
||||||
|
message: response.error || t('something_went_wrong'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrivateRoutes = async (
|
||||||
|
key: keyof typeof ROUTES,
|
||||||
|
): Promise<void> => {
|
||||||
|
if (
|
||||||
|
localStorageUserAuthToken &&
|
||||||
|
localStorageUserAuthToken.refreshJwt &&
|
||||||
|
isUserFetching
|
||||||
|
) {
|
||||||
|
handleUserLoginIfTokenPresent(key);
|
||||||
|
} else {
|
||||||
|
handleRedirectForOrgOnboarding(key);
|
||||||
|
|
||||||
|
navigateToLoginIfNotLoggedIn(isLocalStorageLoggedIn);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const navigateToWorkSpaceBlocked = (route: any): void => {
|
const navigateToWorkSpaceBlocked = (route: any): void => {
|
||||||
const { path } = route;
|
const { path } = route;
|
||||||
|
|
||||||
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 (!isFetchingLicenses) {
|
if (!isFetchingLicensesData) {
|
||||||
const currentRoute = mapRoutes.get('current');
|
const shouldBlockWorkspace = licensesData?.payload?.workSpaceBlock;
|
||||||
const shouldBlockWorkspace = licenses?.workSpaceBlock;
|
|
||||||
|
|
||||||
if (shouldBlockWorkspace && currentRoute) {
|
if (shouldBlockWorkspace) {
|
||||||
navigateToWorkSpaceBlocked(currentRoute);
|
navigateToWorkSpaceBlocked(currentRoute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isFetchingLicenses, licenses?.workSpaceBlock, mapRoutes, pathname]);
|
}, [isFetchingLicensesData]);
|
||||||
|
|
||||||
const navigateToWorkSpaceSuspended = (route: any): void => {
|
const navigateToWorkSpaceSuspended = (route: any): void => {
|
||||||
const { path } = route;
|
const { path } = route;
|
||||||
|
|
||||||
if (path && path !== ROUTES.WORKSPACE_SUSPENDED) {
|
if (path && path !== ROUTES.WORKSPACE_SUSPENDED) {
|
||||||
history.push(ROUTES.WORKSPACE_SUSPENDED);
|
history.push(ROUTES.WORKSPACE_SUSPENDED);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: UPDATE_USER_IS_FETCH,
|
||||||
|
payload: {
|
||||||
|
isUserFetching: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isFetchingActiveLicenseV3 && activeLicenseV3) {
|
if (!isFetchingActiveLicenseV3 && activeLicenseV3) {
|
||||||
const currentRoute = mapRoutes.get('current');
|
|
||||||
const shouldSuspendWorkspace =
|
const shouldSuspendWorkspace =
|
||||||
activeLicenseV3.status === LicenseStatus.SUSPENDED &&
|
activeLicenseV3.status === LicenseStatus.SUSPENDED &&
|
||||||
activeLicenseV3.state === LicenseState.PAYMENT_FAILED;
|
activeLicenseV3.state === LicenseState.PAYMENT_FAILED;
|
||||||
|
|
||||||
if (shouldSuspendWorkspace && currentRoute) {
|
if (shouldSuspendWorkspace) {
|
||||||
navigateToWorkSpaceSuspended(currentRoute);
|
navigateToWorkSpaceSuspended(currentRoute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isFetchingActiveLicenseV3, activeLicenseV3, mapRoutes, pathname]);
|
}, [isFetchingActiveLicenseV3, activeLicenseV3]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (org && org.length > 0 && org[0].id !== undefined) {
|
if (org && org.length > 0 && org[0].id !== undefined) {
|
||||||
@@ -156,70 +286,103 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [org]);
|
}, [org]);
|
||||||
|
|
||||||
|
const handleRouting = (): void => {
|
||||||
|
const showOrgOnboarding = shouldShowOnboarding();
|
||||||
|
|
||||||
|
if (showOrgOnboarding && !isOnboardingComplete && isCloudUserVal) {
|
||||||
|
history.push(ROUTES.ONBOARDING);
|
||||||
|
} else {
|
||||||
|
history.push(ROUTES.APPLICATION);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const { isPrivate } = currentRoute || {
|
||||||
|
isPrivate: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoggedInState && role && role !== 'ADMIN') {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPrivate) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isEmpty(user) &&
|
||||||
|
!isFetchingOrgPreferences &&
|
||||||
|
!isEmpty(orgUsers?.payload) &&
|
||||||
|
!isNull(orgPreferences)
|
||||||
|
) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [currentRoute, user, role, orgUsers, orgPreferences]);
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// if it is an old route navigate to the new route
|
(async (): Promise<void> => {
|
||||||
if (isOldRoute) {
|
try {
|
||||||
const redirectUrl = oldNewRoutesMapping[pathname];
|
if (isOldRoute) {
|
||||||
|
const redirectUrl = oldNewRoutesMapping[pathname];
|
||||||
|
|
||||||
const newLocation = {
|
const newLocation = {
|
||||||
...location,
|
...location,
|
||||||
pathname: redirectUrl,
|
pathname: redirectUrl,
|
||||||
};
|
};
|
||||||
history.replace(newLocation);
|
history.replace(newLocation);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
// if the current route
|
if (currentRoute) {
|
||||||
if (currentRoute) {
|
const { isPrivate, key } = currentRoute;
|
||||||
const { isPrivate, key } = currentRoute;
|
|
||||||
if (isPrivate) {
|
if (isPrivate && key !== String(ROUTES.WORKSPACE_LOCKED)) {
|
||||||
if (isLoggedInState) {
|
handlePrivateRoutes(key);
|
||||||
const route = routePermission[key];
|
} else {
|
||||||
if (route && route.find((e) => e === user.role) === undefined) {
|
// no need to fetch the user and make user fetching false
|
||||||
history.push(ROUTES.UN_AUTHORIZED);
|
if (getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true') {
|
||||||
|
handleRouting();
|
||||||
|
}
|
||||||
|
dispatch({
|
||||||
|
type: UPDATE_USER_IS_FETCH,
|
||||||
|
payload: {
|
||||||
|
isUserFetching: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (pathname === ROUTES.HOME_PAGE) {
|
||||||
|
// routing to application page over root page
|
||||||
|
if (isLoggedInState) {
|
||||||
|
handleRouting();
|
||||||
|
} else {
|
||||||
|
navigateToLoginIfNotLoggedIn();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, pathname);
|
// not found
|
||||||
history.push(ROUTES.LOGIN);
|
navigateToLoginIfNotLoggedIn(isLocalStorageLoggedIn);
|
||||||
}
|
}
|
||||||
} else if (isLoggedInState) {
|
} catch (error) {
|
||||||
const fromPathname = getLocalStorageApi(
|
// something went wrong
|
||||||
LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT,
|
history.push(ROUTES.SOMETHING_WENT_WRONG);
|
||||||
);
|
|
||||||
if (fromPathname) {
|
|
||||||
history.push(fromPathname);
|
|
||||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, '');
|
|
||||||
} else {
|
|
||||||
history.push(ROUTES.APPLICATION);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// do nothing as the unauthenticated routes are LOGIN and SIGNUP and the LOGIN container takes care of routing to signup if
|
|
||||||
// setup is not completed
|
|
||||||
}
|
}
|
||||||
} else if (isLoggedInState) {
|
})();
|
||||||
const fromPathname = getLocalStorageApi(
|
|
||||||
LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT,
|
|
||||||
);
|
|
||||||
if (fromPathname) {
|
|
||||||
history.push(fromPathname);
|
|
||||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, '');
|
|
||||||
} else {
|
|
||||||
history.push(ROUTES.APPLICATION);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, pathname);
|
|
||||||
history.push(ROUTES.LOGIN);
|
|
||||||
}
|
|
||||||
}, [
|
}, [
|
||||||
licenses,
|
dispatch,
|
||||||
isLoggedInState,
|
isLoggedInState,
|
||||||
pathname,
|
|
||||||
user,
|
|
||||||
isOldRoute,
|
|
||||||
currentRoute,
|
currentRoute,
|
||||||
location,
|
licensesData,
|
||||||
|
orgUsers,
|
||||||
|
orgPreferences,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (isUserFetchingError) {
|
||||||
|
return <Redirect to={ROUTES.SOMETHING_WENT_WRONG} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUserFetching || isLoading) {
|
||||||
|
return <Spinner tip="Loading..." />;
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: disabling this rule as there is no need to have div
|
// NOTE: disabling this rule as there is no need to have div
|
||||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { ConfigProvider } from 'antd';
|
import { ConfigProvider } from 'antd';
|
||||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import getAllOrgPreferences from 'api/preferences/getAllOrgPreferences';
|
||||||
import NotFound from 'components/NotFound';
|
import NotFound from 'components/NotFound';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
@@ -9,21 +11,35 @@ import ROUTES from 'constants/routes';
|
|||||||
import AppLayout from 'container/AppLayout';
|
import AppLayout from 'container/AppLayout';
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||||
import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys';
|
import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||||
import { useThemeConfig } from 'hooks/useDarkMode';
|
import { useIsDarkMode, useThemeConfig } from 'hooks/useDarkMode';
|
||||||
import { LICENSE_PLAN_KEY } from 'hooks/useLicense';
|
import { THEME_MODE } from 'hooks/useDarkMode/constant';
|
||||||
|
import useFeatureFlags from 'hooks/useFeatureFlag';
|
||||||
|
import useGetFeatureFlag from 'hooks/useGetFeatureFlag';
|
||||||
|
import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense';
|
||||||
import { NotificationProvider } from 'hooks/useNotifications';
|
import { NotificationProvider } from 'hooks/useNotifications';
|
||||||
import { ResourceProvider } from 'hooks/useResourceAttribute';
|
import { ResourceProvider } from 'hooks/useResourceAttribute';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { identity, pickBy } from 'lodash-es';
|
import { identity, pick, pickBy } from 'lodash-es';
|
||||||
import posthog from 'posthog-js';
|
import posthog from 'posthog-js';
|
||||||
import AlertRuleProvider from 'providers/Alert';
|
import AlertRuleProvider from 'providers/Alert';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { AppProvider } from 'providers/App/App';
|
||||||
import { IUser } from 'providers/App/types';
|
|
||||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||||
import { Suspense, useCallback, useEffect, useState } from 'react';
|
import { Suspense, useEffect, useState } from 'react';
|
||||||
import { Redirect, Route, Router, Switch } from 'react-router-dom';
|
import { useQuery } from 'react-query';
|
||||||
|
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';
|
||||||
@@ -35,20 +51,14 @@ import defaultRoutes, {
|
|||||||
|
|
||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
const themeConfig = useThemeConfig();
|
const themeConfig = useThemeConfig();
|
||||||
const {
|
const { data: licenseData } = useLicense();
|
||||||
licenses,
|
|
||||||
user,
|
|
||||||
isFetchingUser,
|
|
||||||
isFetchingLicenses,
|
|
||||||
isFetchingFeatureFlags,
|
|
||||||
userFetchError,
|
|
||||||
licensesFetchError,
|
|
||||||
featureFlagsFetchError,
|
|
||||||
isLoggedIn: isLoggedInState,
|
|
||||||
featureFlags,
|
|
||||||
org,
|
|
||||||
} = useAppContext();
|
|
||||||
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
|
const [routes, setRoutes] = useState<AppRoutes[]>(defaultRoutes);
|
||||||
|
const { role, isLoggedIn: isLoggedInState, user, org } = useSelector<
|
||||||
|
AppState,
|
||||||
|
AppReducer
|
||||||
|
>((state) => state.app);
|
||||||
|
|
||||||
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
|
|
||||||
const { trackPageView } = useAnalytics();
|
const { trackPageView } = useAnalytics();
|
||||||
|
|
||||||
@@ -56,114 +66,164 @@ function App(): JSX.Element {
|
|||||||
|
|
||||||
const isCloudUserVal = isCloudUser();
|
const isCloudUserVal = isCloudUser();
|
||||||
|
|
||||||
const enableAnalytics = useCallback(
|
const isDarkMode = useIsDarkMode();
|
||||||
(user: IUser): void => {
|
|
||||||
// wait for the required data to be loaded before doing init for anything!
|
|
||||||
if (!isFetchingLicenses && licenses && org) {
|
|
||||||
const orgName =
|
|
||||||
org && Array.isArray(org) && org.length > 0 ? org[0].name : '';
|
|
||||||
|
|
||||||
const { name, email, role } = user;
|
const isChatSupportEnabled =
|
||||||
|
useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active || false;
|
||||||
|
|
||||||
const identifyPayload = {
|
const isPremiumSupportEnabled =
|
||||||
email,
|
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
|
||||||
name,
|
|
||||||
company_name: orgName,
|
|
||||||
role,
|
|
||||||
source: 'signoz-ui',
|
|
||||||
};
|
|
||||||
|
|
||||||
const sanitizedIdentifyPayload = pickBy(identifyPayload, identity);
|
const { data: orgPreferences, isLoading: isLoadingOrgPreferences } = useQuery({
|
||||||
const domain = extractDomain(email);
|
queryFn: () => getAllOrgPreferences(),
|
||||||
const hostNameParts = hostname.split('.');
|
queryKey: ['getOrgPreferences'],
|
||||||
|
enabled: isLoggedInState && role === USER_ROLES.ADMIN,
|
||||||
|
});
|
||||||
|
|
||||||
const groupTraits = {
|
|
||||||
name: orgName,
|
|
||||||
tenant_id: hostNameParts[0],
|
|
||||||
data_region: hostNameParts[1],
|
|
||||||
tenant_url: hostname,
|
|
||||||
company_domain: domain,
|
|
||||||
source: 'signoz-ui',
|
|
||||||
};
|
|
||||||
|
|
||||||
window.analytics.identify(email, sanitizedIdentifyPayload);
|
|
||||||
window.analytics.group(domain, groupTraits);
|
|
||||||
|
|
||||||
posthog?.identify(email, {
|
|
||||||
email,
|
|
||||||
name,
|
|
||||||
orgName,
|
|
||||||
tenant_id: hostNameParts[0],
|
|
||||||
data_region: hostNameParts[1],
|
|
||||||
tenant_url: hostname,
|
|
||||||
company_domain: domain,
|
|
||||||
source: 'signoz-ui',
|
|
||||||
isPaidUser: !!licenses?.trialConvertedToSubscription,
|
|
||||||
});
|
|
||||||
|
|
||||||
posthog?.group('company', domain, {
|
|
||||||
name: orgName,
|
|
||||||
tenant_id: hostNameParts[0],
|
|
||||||
data_region: hostNameParts[1],
|
|
||||||
tenant_url: hostname,
|
|
||||||
company_domain: domain,
|
|
||||||
source: 'signoz-ui',
|
|
||||||
isPaidUser: !!licenses?.trialConvertedToSubscription,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[hostname, isFetchingLicenses, licenses, org],
|
|
||||||
);
|
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (orgPreferences && !isLoadingOrgPreferences) {
|
||||||
!isFetchingLicenses &&
|
dispatch({
|
||||||
licenses &&
|
type: UPDATE_IS_FETCHING_ORG_PREFERENCES,
|
||||||
!isFetchingUser &&
|
payload: {
|
||||||
user &&
|
isFetchingOrgPreferences: false,
|
||||||
!!user.email
|
},
|
||||||
) {
|
});
|
||||||
const isOnBasicPlan =
|
|
||||||
licenses.licenses?.some(
|
|
||||||
(license) =>
|
|
||||||
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
|
|
||||||
) || licenses.licenses === null;
|
|
||||||
|
|
||||||
const isIdentifiedUser = getLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER);
|
dispatch({
|
||||||
|
type: UPDATE_ORG_PREFERENCES,
|
||||||
if (isLoggedInState && user && user.id && user.email && !isIdentifiedUser) {
|
payload: {
|
||||||
setLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER, 'true');
|
orgPreferences: orgPreferences.payload?.data || null,
|
||||||
}
|
},
|
||||||
|
});
|
||||||
let updatedRoutes = defaultRoutes;
|
|
||||||
// if the user is a cloud user
|
|
||||||
if (isCloudUserVal || isEECloudUser()) {
|
|
||||||
// if the user is on basic plan then remove billing
|
|
||||||
if (isOnBasicPlan) {
|
|
||||||
updatedRoutes = updatedRoutes.filter(
|
|
||||||
(route) => route?.path !== ROUTES.BILLING,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// always add support route for cloud users
|
|
||||||
updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE];
|
|
||||||
} else {
|
|
||||||
// if not a cloud user then remove billing and add list licenses route
|
|
||||||
updatedRoutes = updatedRoutes.filter(
|
|
||||||
(route) => route?.path !== ROUTES.BILLING,
|
|
||||||
);
|
|
||||||
updatedRoutes = [...updatedRoutes, LIST_LICENSES];
|
|
||||||
}
|
|
||||||
setRoutes(updatedRoutes);
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [orgPreferences, dispatch, isLoadingOrgPreferences]);
|
||||||
isLoggedInState,
|
|
||||||
user,
|
useEffect(() => {
|
||||||
licenses,
|
if (isLoggedInState && role !== USER_ROLES.ADMIN) {
|
||||||
isCloudUserVal,
|
dispatch({
|
||||||
isFetchingLicenses,
|
type: UPDATE_IS_FETCHING_ORG_PREFERENCES,
|
||||||
isFetchingUser,
|
payload: {
|
||||||
]);
|
isFetchingOrgPreferences: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isLoggedInState, role, dispatch]);
|
||||||
|
|
||||||
|
const featureResponse = useGetFeatureFlag((allFlags) => {
|
||||||
|
dispatch({
|
||||||
|
type: UPDATE_FEATURE_FLAG_RESPONSE,
|
||||||
|
payload: {
|
||||||
|
featureFlag: allFlags,
|
||||||
|
refetch: featureResponse.refetch,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isOnboardingEnabled =
|
||||||
|
allFlags.find((flag) => flag.name === FeatureKeys.ONBOARDING)?.active ||
|
||||||
|
false;
|
||||||
|
|
||||||
|
if (!isOnboardingEnabled || !isCloudUserVal) {
|
||||||
|
const newRoutes = routes.filter(
|
||||||
|
(route) => route?.path !== ROUTES.GET_STARTED,
|
||||||
|
);
|
||||||
|
|
||||||
|
setRoutes(newRoutes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isOnBasicPlan =
|
||||||
|
licenseData?.payload?.licenses?.some(
|
||||||
|
(license) =>
|
||||||
|
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
|
||||||
|
) || licenseData?.payload?.licenses === null;
|
||||||
|
|
||||||
|
const enableAnalytics = (user: User): void => {
|
||||||
|
const orgName =
|
||||||
|
org && Array.isArray(org) && org.length > 0 ? org[0].name : '';
|
||||||
|
|
||||||
|
const { name, email } = user;
|
||||||
|
|
||||||
|
const identifyPayload = {
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
company_name: orgName,
|
||||||
|
role,
|
||||||
|
source: 'signoz-ui',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizedIdentifyPayload = pickBy(identifyPayload, identity);
|
||||||
|
const domain = extractDomain(email);
|
||||||
|
const hostNameParts = hostname.split('.');
|
||||||
|
|
||||||
|
const groupTraits = {
|
||||||
|
name: orgName,
|
||||||
|
tenant_id: hostNameParts[0],
|
||||||
|
data_region: hostNameParts[1],
|
||||||
|
tenant_url: hostname,
|
||||||
|
company_domain: domain,
|
||||||
|
source: 'signoz-ui',
|
||||||
|
};
|
||||||
|
|
||||||
|
window.analytics.identify(email, sanitizedIdentifyPayload);
|
||||||
|
window.analytics.group(domain, groupTraits);
|
||||||
|
|
||||||
|
posthog?.identify(email, {
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
orgName,
|
||||||
|
tenant_id: hostNameParts[0],
|
||||||
|
data_region: hostNameParts[1],
|
||||||
|
tenant_url: hostname,
|
||||||
|
company_domain: domain,
|
||||||
|
source: 'signoz-ui',
|
||||||
|
isPaidUser: !!licenseData?.payload?.trialConvertedToSubscription,
|
||||||
|
});
|
||||||
|
|
||||||
|
posthog?.group('company', domain, {
|
||||||
|
name: orgName,
|
||||||
|
tenant_id: hostNameParts[0],
|
||||||
|
data_region: hostNameParts[1],
|
||||||
|
tenant_url: hostname,
|
||||||
|
company_domain: domain,
|
||||||
|
source: 'signoz-ui',
|
||||||
|
isPaidUser: !!licenseData?.payload?.trialConvertedToSubscription,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const isIdentifiedUser = getLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER);
|
||||||
|
|
||||||
|
if (
|
||||||
|
isLoggedInState &&
|
||||||
|
user &&
|
||||||
|
user.userId &&
|
||||||
|
user.email &&
|
||||||
|
!isIdentifiedUser
|
||||||
|
) {
|
||||||
|
setLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER, 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isOnBasicPlan ||
|
||||||
|
(isLoggedInState && role && role !== 'ADMIN') ||
|
||||||
|
!(isCloudUserVal || isEECloudUser())
|
||||||
|
) {
|
||||||
|
const newRoutes = routes.filter((route) => route?.path !== ROUTES.BILLING);
|
||||||
|
setRoutes(newRoutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCloudUserVal || isEECloudUser()) {
|
||||||
|
const newRoutes = [...routes, SUPPORT_ROUTE];
|
||||||
|
|
||||||
|
setRoutes(newRoutes);
|
||||||
|
} else {
|
||||||
|
const newRoutes = [...routes, LIST_LICENSES];
|
||||||
|
|
||||||
|
setRoutes(newRoutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [isLoggedInState, isOnBasicPlan, user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pathname === ROUTES.ONBOARDING) {
|
if (pathname === ROUTES.ONBOARDING) {
|
||||||
@@ -177,116 +237,99 @@ function App(): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
trackPageView(pathname);
|
trackPageView(pathname);
|
||||||
}, [pathname, trackPageView]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// feature flag shouldn't be loading and featureFlags or fetchError any one of this should be true indicating that req is complete
|
const showAddCreditCardModal =
|
||||||
// licenses should also be present. there is no check for licenses for loading and error as that is mandatory if not present then routing
|
!isPremiumSupportEnabled &&
|
||||||
// to something went wrong which would ideally need a reload.
|
!licenseData?.payload?.trialConvertedToSubscription;
|
||||||
if (
|
|
||||||
!isFetchingFeatureFlags &&
|
|
||||||
(featureFlags || featureFlagsFetchError) &&
|
|
||||||
licenses
|
|
||||||
) {
|
|
||||||
let isChatSupportEnabled = false;
|
|
||||||
let isPremiumSupportEnabled = false;
|
|
||||||
if (featureFlags && featureFlags.length > 0) {
|
|
||||||
isChatSupportEnabled =
|
|
||||||
featureFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)
|
|
||||||
?.active || false;
|
|
||||||
|
|
||||||
isPremiumSupportEnabled =
|
if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) {
|
||||||
featureFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)
|
window.Intercom('boot', {
|
||||||
?.active || false;
|
app_id: process.env.INTERCOM_APP_ID,
|
||||||
}
|
email: user?.email || '',
|
||||||
const showAddCreditCardModal =
|
name: user?.name || '',
|
||||||
!isPremiumSupportEnabled && !licenses.trialConvertedToSubscription;
|
});
|
||||||
|
|
||||||
if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) {
|
|
||||||
window.Intercom('boot', {
|
|
||||||
app_id: process.env.INTERCOM_APP_ID,
|
|
||||||
email: user?.email || '',
|
|
||||||
name: user?.name || '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
isLoggedInState,
|
isLoggedInState,
|
||||||
|
isChatSupportEnabled,
|
||||||
user,
|
user,
|
||||||
|
licenseData,
|
||||||
|
isPremiumSupportEnabled,
|
||||||
pathname,
|
pathname,
|
||||||
licenses?.trialConvertedToSubscription,
|
|
||||||
featureFlags,
|
|
||||||
isFetchingFeatureFlags,
|
|
||||||
featureFlagsFetchError,
|
|
||||||
licenses,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isFetchingUser && isCloudUserVal && user && user.email) {
|
if (user && user?.email && user?.userId && user?.name) {
|
||||||
|
try {
|
||||||
|
const isThemeAnalyticsSent = getLocalStorageApi(
|
||||||
|
LOCALSTORAGE.THEME_ANALYTICS_V1,
|
||||||
|
);
|
||||||
|
if (!isThemeAnalyticsSent) {
|
||||||
|
logEvent('Theme Analytics', {
|
||||||
|
theme: isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT,
|
||||||
|
user: pick(user, ['email', 'userId', 'name']),
|
||||||
|
org,
|
||||||
|
});
|
||||||
|
setLocalStorageApi(LOCALSTORAGE.THEME_ANALYTICS_V1, 'true');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.error('Failed to parse local storage theme analytics event');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCloudUserVal && user && user.email) {
|
||||||
enableAnalytics(user);
|
enableAnalytics(user);
|
||||||
}
|
}
|
||||||
}, [user, isFetchingUser, isCloudUserVal, enableAnalytics]);
|
|
||||||
|
|
||||||
// if the user is in logged in state
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
if (isLoggedInState) {
|
}, [user]);
|
||||||
if (pathname === ROUTES.HOME_PAGE) {
|
|
||||||
history.replace(ROUTES.APPLICATION);
|
|
||||||
}
|
|
||||||
// if the setup calls are loading then return a spinner
|
|
||||||
if (isFetchingLicenses || isFetchingUser || isFetchingFeatureFlags) {
|
|
||||||
return <Spinner tip="Loading..." />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the required calls fails then return a something went wrong error
|
useEffect(() => {
|
||||||
// this needs to be on top of data missing error because if there is an error, data will never be loaded and it will
|
console.info('We are hiring! https://jobs.gem.com/signoz');
|
||||||
// move to indefinitive loading
|
}, []);
|
||||||
if (userFetchError || licensesFetchError) {
|
|
||||||
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 (
|
||||||
<ConfigProvider theme={themeConfig}>
|
<AppProvider>
|
||||||
<Router history={history}>
|
<ConfigProvider theme={themeConfig}>
|
||||||
<CompatRouter>
|
<Router history={history}>
|
||||||
<NotificationProvider>
|
<CompatRouter>
|
||||||
<PrivateRoute>
|
<NotificationProvider>
|
||||||
<ResourceProvider>
|
<PrivateRoute>
|
||||||
<QueryBuilderProvider>
|
<ResourceProvider>
|
||||||
<DashboardProvider>
|
<QueryBuilderProvider>
|
||||||
<KeyboardHotkeysProvider>
|
<DashboardProvider>
|
||||||
<AlertRuleProvider>
|
<KeyboardHotkeysProvider>
|
||||||
<AppLayout>
|
<AlertRuleProvider>
|
||||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
<AppLayout>
|
||||||
<Switch>
|
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||||
{routes.map(({ path, component, exact }) => (
|
<Switch>
|
||||||
<Route
|
{routes.map(({ path, component, exact }) => (
|
||||||
key={`${path}`}
|
<Route
|
||||||
exact={exact}
|
key={`${path}`}
|
||||||
path={path}
|
exact={exact}
|
||||||
component={component}
|
path={path}
|
||||||
/>
|
component={component}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
<Route path="*" component={NotFound} />
|
<Route path="*" component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</AlertRuleProvider>
|
</AlertRuleProvider>
|
||||||
</KeyboardHotkeysProvider>
|
</KeyboardHotkeysProvider>
|
||||||
</DashboardProvider>
|
</DashboardProvider>
|
||||||
</QueryBuilderProvider>
|
</QueryBuilderProvider>
|
||||||
</ResourceProvider>
|
</ResourceProvider>
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</CompatRouter>
|
</CompatRouter>
|
||||||
</Router>
|
</Router>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
|
</AppProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -427,27 +427,24 @@ export const LIST_LICENSES: AppRoutes = {
|
|||||||
|
|
||||||
export const oldRoutes = [
|
export const oldRoutes = [
|
||||||
'/pipelines',
|
'/pipelines',
|
||||||
|
'/logs/old-logs-explorer',
|
||||||
'/logs-explorer',
|
'/logs-explorer',
|
||||||
'/logs-explorer/live',
|
'/logs-explorer/live',
|
||||||
'/logs-save-views',
|
'/logs-save-views',
|
||||||
'/traces-save-views',
|
'/traces-save-views',
|
||||||
'/settings/access-tokens',
|
'/settings/api-keys',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const oldNewRoutesMapping: Record<string, string> = {
|
export const oldNewRoutesMapping: Record<string, string> = {
|
||||||
'/pipelines': '/logs/pipelines',
|
'/pipelines': '/logs/pipelines',
|
||||||
|
'/logs/old-logs-explorer': '/logs/old-logs-explorer',
|
||||||
'/logs-explorer': '/logs/logs-explorer',
|
'/logs-explorer': '/logs/logs-explorer',
|
||||||
'/logs-explorer/live': '/logs/logs-explorer/live',
|
'/logs-explorer/live': '/logs/logs-explorer/live',
|
||||||
'/logs-save-views': '/logs/saved-views',
|
'/logs-save-views': '/logs/saved-views',
|
||||||
'/traces-save-views': '/traces/saved-views',
|
'/traces-save-views': '/traces/saved-views',
|
||||||
'/settings/access-tokens': '/settings/api-keys',
|
'/settings/api-keys': '/settings/access-tokens',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ROUTES_NOT_TO_BE_OVERRIDEN: string[] = [
|
|
||||||
ROUTES.WORKSPACE_LOCKED,
|
|
||||||
ROUTES.WORKSPACE_SUSPENDED,
|
|
||||||
];
|
|
||||||
|
|
||||||
export interface AppRoutes {
|
export interface AppRoutes {
|
||||||
component: RouteProps['component'];
|
component: RouteProps['component'];
|
||||||
path: RouteProps['path'];
|
path: RouteProps['path'];
|
||||||
|
|||||||
@@ -1,28 +1,92 @@
|
|||||||
|
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||||
|
import getUserApi from 'api/user/getUser';
|
||||||
|
import { Logout } from 'api/utils';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import store from 'store';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import {
|
||||||
|
LOGGED_IN,
|
||||||
|
UPDATE_USER,
|
||||||
|
UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN,
|
||||||
|
UPDATE_USER_IS_FETCH,
|
||||||
|
} from 'types/actions/app';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import { PayloadProps } from 'types/api/user/getUser';
|
||||||
|
|
||||||
const afterLogin = (
|
const afterLogin = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
authToken: string,
|
authToken: string,
|
||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
interceptorRejected?: boolean,
|
): Promise<SuccessResponse<PayloadProps> | undefined> => {
|
||||||
): void => {
|
|
||||||
setLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN, authToken);
|
setLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN, authToken);
|
||||||
setLocalStorageApi(LOCALSTORAGE.REFRESH_AUTH_TOKEN, refreshToken);
|
setLocalStorageApi(LOCALSTORAGE.REFRESH_AUTH_TOKEN, refreshToken);
|
||||||
setLocalStorageApi(LOCALSTORAGE.USER_ID, userId);
|
|
||||||
setLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN, 'true');
|
|
||||||
|
|
||||||
if (!interceptorRejected) {
|
store.dispatch<AppActions>({
|
||||||
window.dispatchEvent(
|
type: UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN,
|
||||||
new CustomEvent('AFTER_LOGIN', {
|
payload: {
|
||||||
detail: {
|
accessJwt: authToken,
|
||||||
accessJWT: authToken,
|
refreshJwt: refreshToken,
|
||||||
refreshJWT: refreshToken,
|
},
|
||||||
id: userId,
|
});
|
||||||
},
|
|
||||||
}),
|
const [getUserResponse] = await Promise.all([
|
||||||
);
|
getUserApi({
|
||||||
|
userId,
|
||||||
|
token: authToken,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (getUserResponse.statusCode === 200 && getUserResponse.payload) {
|
||||||
|
store.dispatch<AppActions>({
|
||||||
|
type: LOGGED_IN,
|
||||||
|
payload: {
|
||||||
|
isLoggedIn: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { payload } = getUserResponse;
|
||||||
|
|
||||||
|
store.dispatch<AppActions>({
|
||||||
|
type: UPDATE_USER,
|
||||||
|
payload: {
|
||||||
|
ROLE: payload.role,
|
||||||
|
email: payload.email,
|
||||||
|
name: payload.name,
|
||||||
|
orgName: payload.organization,
|
||||||
|
profilePictureURL: payload.profilePictureURL,
|
||||||
|
userId: payload.id,
|
||||||
|
orgId: payload.orgId,
|
||||||
|
userFlags: payload.flags,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isLoggedInLocalStorage = getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN);
|
||||||
|
|
||||||
|
if (isLoggedInLocalStorage === null) {
|
||||||
|
setLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN, 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
store.dispatch({
|
||||||
|
type: UPDATE_USER_IS_FETCH,
|
||||||
|
payload: {
|
||||||
|
isUserFetching: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return getUserResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
store.dispatch({
|
||||||
|
type: UPDATE_USER_IS_FETCH,
|
||||||
|
payload: {
|
||||||
|
isUserFetching: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Logout();
|
||||||
|
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default afterLogin;
|
export default afterLogin;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import afterLogin from 'AppRoutes/utils';
|
|||||||
import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||||
import { ENVIRONMENT } from 'constants/env';
|
import { ENVIRONMENT } from 'constants/env';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import store from 'store';
|
||||||
|
|
||||||
import apiV1, {
|
import apiV1, {
|
||||||
apiAlertManager,
|
apiAlertManager,
|
||||||
@@ -25,7 +26,10 @@ const interceptorsResponse = (
|
|||||||
const interceptorsRequestResponse = (
|
const interceptorsRequestResponse = (
|
||||||
value: InternalAxiosRequestConfig,
|
value: InternalAxiosRequestConfig,
|
||||||
): InternalAxiosRequestConfig => {
|
): InternalAxiosRequestConfig => {
|
||||||
const token = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) || '';
|
const token =
|
||||||
|
store.getState().app.user?.accessJwt ||
|
||||||
|
getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) ||
|
||||||
|
'';
|
||||||
|
|
||||||
if (value && value.headers) {
|
if (value && value.headers) {
|
||||||
value.headers.Authorization = token ? `Bearer ${token}` : '';
|
value.headers.Authorization = token ? `Bearer ${token}` : '';
|
||||||
@@ -43,36 +47,41 @@ const interceptorRejected = async (
|
|||||||
// reject the refresh token error
|
// reject the refresh token error
|
||||||
if (response.status === 401 && response.config.url !== '/login') {
|
if (response.status === 401 && response.config.url !== '/login') {
|
||||||
const response = await loginApi({
|
const response = await loginApi({
|
||||||
refreshToken: getLocalStorageApi(LOCALSTORAGE.REFRESH_AUTH_TOKEN) || '',
|
refreshToken: store.getState().app.user?.refreshJwt,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
if (response.statusCode === 200) {
|
||||||
afterLogin(
|
const user = await afterLogin(
|
||||||
response.payload.userId,
|
response.payload.userId,
|
||||||
response.payload.accessJwt,
|
response.payload.accessJwt,
|
||||||
response.payload.refreshJwt,
|
response.payload.refreshJwt,
|
||||||
true,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const reResponse = await axios(
|
if (user) {
|
||||||
`${value.config.baseURL}${value.config.url?.substring(1)}`,
|
const reResponse = await axios(
|
||||||
{
|
`${value.config.baseURL}${value.config.url?.substring(1)}`,
|
||||||
method: value.config.method,
|
{
|
||||||
headers: {
|
method: value.config.method,
|
||||||
...value.config.headers,
|
headers: {
|
||||||
Authorization: `Bearer ${response.payload.accessJwt}`,
|
...value.config.headers,
|
||||||
|
Authorization: `Bearer ${response.payload.accessJwt}`,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
...JSON.parse(value.config.data || '{}'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data: {
|
);
|
||||||
...JSON.parse(value.config.data || '{}'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (reResponse.status === 200) {
|
if (reResponse.status === 200) {
|
||||||
return await Promise.resolve(reResponse);
|
return await Promise.resolve(reResponse);
|
||||||
|
}
|
||||||
|
Logout();
|
||||||
|
|
||||||
|
return await Promise.reject(reResponse);
|
||||||
}
|
}
|
||||||
Logout();
|
Logout();
|
||||||
return await Promise.reject(reResponse);
|
|
||||||
|
return await Promise.reject(value);
|
||||||
}
|
}
|
||||||
Logout();
|
Logout();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
import { ApiV2Instance as axios } from 'api';
|
import { ApiV2Instance as axios } from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { PayloadProps } from 'types/api/licenses/getAll';
|
import { PayloadProps } from 'types/api/licenses/getAll';
|
||||||
|
|
||||||
const getAll = async (): Promise<
|
const getAll = async (): Promise<
|
||||||
SuccessResponse<PayloadProps> | ErrorResponse
|
SuccessResponse<PayloadProps> | ErrorResponse
|
||||||
> => {
|
> => {
|
||||||
const response = await axios.get('/licenses');
|
try {
|
||||||
|
const response = await axios.get('/licenses');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
error: null,
|
error: null,
|
||||||
message: response.data.status,
|
message: response.data.status,
|
||||||
payload: response.data.data,
|
payload: response.data.data,
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getAll;
|
export default getAll;
|
||||||
|
|||||||
@@ -1,18 +1,28 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { PayloadProps, Props } from 'types/api/user/getUser';
|
import { PayloadProps, Props } from 'types/api/user/getUser';
|
||||||
|
|
||||||
const getUser = async (
|
const getUser = async (
|
||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
const response = await axios.get(`/user/${props.userId}`);
|
try {
|
||||||
|
const response = await axios.get(`/user/${props.userId}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `bearer ${props.token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
error: null,
|
error: null,
|
||||||
message: 'Success',
|
message: 'Success',
|
||||||
payload: response.data,
|
payload: response.data,
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getUser;
|
export default getUser;
|
||||||
|
|||||||
@@ -2,6 +2,14 @@ import deleteLocalStorageKey from 'api/browser/localstorage/remove';
|
|||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
|
import store from 'store';
|
||||||
|
import {
|
||||||
|
LOGGED_IN,
|
||||||
|
UPDATE_ORG,
|
||||||
|
UPDATE_USER,
|
||||||
|
UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN,
|
||||||
|
UPDATE_USER_ORG_ROLE,
|
||||||
|
} from 'types/actions/app';
|
||||||
|
|
||||||
export const Logout = (): void => {
|
export const Logout = (): void => {
|
||||||
deleteLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN);
|
deleteLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN);
|
||||||
@@ -11,9 +19,50 @@ export const Logout = (): void => {
|
|||||||
deleteLocalStorageKey(LOCALSTORAGE.LOGGED_IN_USER_EMAIL);
|
deleteLocalStorageKey(LOCALSTORAGE.LOGGED_IN_USER_EMAIL);
|
||||||
deleteLocalStorageKey(LOCALSTORAGE.LOGGED_IN_USER_NAME);
|
deleteLocalStorageKey(LOCALSTORAGE.LOGGED_IN_USER_NAME);
|
||||||
deleteLocalStorageKey(LOCALSTORAGE.CHAT_SUPPORT);
|
deleteLocalStorageKey(LOCALSTORAGE.CHAT_SUPPORT);
|
||||||
deleteLocalStorageKey(LOCALSTORAGE.USER_ID);
|
|
||||||
|
|
||||||
window.dispatchEvent(new CustomEvent('LOGOUT'));
|
store.dispatch({
|
||||||
|
type: LOGGED_IN,
|
||||||
|
payload: {
|
||||||
|
isLoggedIn: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
store.dispatch({
|
||||||
|
type: UPDATE_USER_ORG_ROLE,
|
||||||
|
payload: {
|
||||||
|
org: null,
|
||||||
|
role: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
store.dispatch({
|
||||||
|
type: UPDATE_USER,
|
||||||
|
payload: {
|
||||||
|
ROLE: 'VIEWER',
|
||||||
|
email: '',
|
||||||
|
name: '',
|
||||||
|
orgId: '',
|
||||||
|
orgName: '',
|
||||||
|
profilePictureURL: '',
|
||||||
|
userId: '',
|
||||||
|
userFlags: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
store.dispatch({
|
||||||
|
type: UPDATE_USER_ACCESS_REFRESH_ACCESS_TOKEN,
|
||||||
|
payload: {
|
||||||
|
accessJwt: '',
|
||||||
|
refreshJwt: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
store.dispatch({
|
||||||
|
type: UPDATE_ORG,
|
||||||
|
payload: {
|
||||||
|
org: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { Button, Modal, Typography } from 'antd';
|
|||||||
import updateCreditCardApi from 'api/billing/checkout';
|
import updateCreditCardApi from 'api/billing/checkout';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
|
import useLicense from 'hooks/useLicense';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { CreditCard, X } from 'lucide-react';
|
import { CreditCard, X } from 'lucide-react';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useMutation } from 'react-query';
|
import { useMutation } from 'react-query';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
@@ -20,16 +20,16 @@ export default function ChatSupportGateway(): JSX.Element {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { licenses, isFetchingLicenses } = useAppContext();
|
const { data: licenseData, isFetching } = useLicense();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isFetchingLicenses && licenses) {
|
const activeValidLicense =
|
||||||
const activeValidLicense =
|
licenseData?.payload?.licenses?.find(
|
||||||
licenses.licenses?.find((license) => license.isCurrent === true) || null;
|
(license) => license.isCurrent === true,
|
||||||
|
) || null;
|
||||||
|
|
||||||
setActiveLicense(activeValidLicense);
|
setActiveLicense(activeValidLicense);
|
||||||
}
|
}, [licenseData, isFetching]);
|
||||||
}, [licenses, isFetchingLicenses]);
|
|
||||||
|
|
||||||
const handleBillingOnSuccess = (
|
const handleBillingOnSuccess = (
|
||||||
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
|
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import './CustomTimePicker.styles.scss';
|
import './CustomTimePicker.styles.scss';
|
||||||
|
|
||||||
import { Input, Popover, Tooltip, Typography } from 'antd';
|
import { Input, Popover, Tooltip, Typography } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
||||||
import {
|
import {
|
||||||
@@ -298,18 +297,6 @@ function CustomTimePicker({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
|
|
||||||
const handleTimezoneHintClick = (e: React.MouseEvent): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleViewChange('timezone');
|
|
||||||
setIsOpenedFromFooter(false);
|
|
||||||
logEvent(
|
|
||||||
'DateTimePicker: Timezone picker opened from time range input badge',
|
|
||||||
{
|
|
||||||
page: location.pathname,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="custom-time-picker">
|
<div className="custom-time-picker">
|
||||||
<Popover
|
<Popover
|
||||||
@@ -373,7 +360,14 @@ function CustomTimePicker({
|
|||||||
suffix={
|
suffix={
|
||||||
<>
|
<>
|
||||||
{!!isTimezoneOverridden && activeTimezoneOffset && (
|
{!!isTimezoneOverridden && activeTimezoneOffset && (
|
||||||
<div className="timezone-badge" onClick={handleTimezoneHintClick}>
|
<div
|
||||||
|
className="timezone-badge"
|
||||||
|
onClick={(e): void => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleViewChange('timezone');
|
||||||
|
setIsOpenedFromFooter(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span>{activeTimezoneOffset}</span>
|
<span>{activeTimezoneOffset}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import './CustomTimePicker.styles.scss';
|
|||||||
|
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
||||||
@@ -82,12 +81,6 @@ function CustomTimePickerPopoverContent({
|
|||||||
const handleTimezoneHintClick = (): void => {
|
const handleTimezoneHintClick = (): void => {
|
||||||
setActiveView('timezone');
|
setActiveView('timezone');
|
||||||
setIsOpenedFromFooter(true);
|
setIsOpenedFromFooter(true);
|
||||||
logEvent(
|
|
||||||
'DateTimePicker: Timezone picker opened from time range picker footer',
|
|
||||||
{
|
|
||||||
page: pathname,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (activeView === 'timezone') {
|
if (activeView === 'timezone') {
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import './RangePickerModal.styles.scss';
|
|||||||
import { DatePicker } from 'antd';
|
import { DatePicker } from 'antd';
|
||||||
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
||||||
import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config';
|
import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useTimezone } from 'providers/Timezone';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { Dispatch, SetStateAction, useMemo } from 'react';
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
@@ -53,32 +53,22 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
|
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { timezone } = useTimezone();
|
const { timezone } = useTimezone();
|
||||||
|
|
||||||
const rangeValue: [Dayjs, Dayjs] = useMemo(
|
|
||||||
() => [
|
|
||||||
dayjs(minTime / 1000_000).tz(timezone.value),
|
|
||||||
dayjs(maxTime / 1000_000).tz(timezone.value),
|
|
||||||
],
|
|
||||||
[maxTime, minTime, timezone.value],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="custom-date-picker">
|
<div className="custom-date-picker">
|
||||||
<RangePicker
|
<RangePicker
|
||||||
disabledDate={disabledDate}
|
disabledDate={disabledDate}
|
||||||
allowClear
|
allowClear
|
||||||
showTime={{
|
showTime
|
||||||
use12Hours: true,
|
format="YYYY-MM-DD hh:mm A"
|
||||||
format: 'hh:mm A',
|
|
||||||
}}
|
|
||||||
format={(date: Dayjs): string =>
|
|
||||||
date.tz(timezone.value).format('YYYY-MM-DD hh:mm A')
|
|
||||||
}
|
|
||||||
onOk={onModalOkHandler}
|
onOk={onModalOkHandler}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...(selectedTime === 'custom' && {
|
{...(selectedTime === 'custom' && {
|
||||||
value: rangeValue,
|
defaultValue: [
|
||||||
|
dayjs(minTime / 1000000).tz(timezone.value),
|
||||||
|
dayjs(maxTime / 1000000).tz(timezone.value),
|
||||||
|
],
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import './TimezonePicker.styles.scss';
|
|||||||
|
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Input } from 'antd';
|
import { Input } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { TimezonePickerShortcuts } from 'constants/shortcuts/TimezonePickerShortcuts';
|
import { TimezonePickerShortcuts } from 'constants/shortcuts/TimezonePickerShortcuts';
|
||||||
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||||
@@ -158,12 +157,6 @@ function TimezonePicker({
|
|||||||
updateTimezone(timezone);
|
updateTimezone(timezone);
|
||||||
handleCloseTimezonePicker();
|
handleCloseTimezonePicker();
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
logEvent('DateTimePicker: New Timezone Selected', {
|
|
||||||
timezone: {
|
|
||||||
name: timezone.name,
|
|
||||||
offset: timezone.offset,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[handleCloseTimezonePicker, setIsOpen, updateTimezone],
|
[handleCloseTimezonePicker, setIsOpen, updateTimezone],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import { Table } from 'antd';
|
import { Table } from 'antd';
|
||||||
|
import { matchMedia } from 'container/PipelinePage/tests/AddNewPipeline.test';
|
||||||
|
import { I18nextProvider } from 'react-i18next';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import i18n from 'ReactI18';
|
||||||
|
import store from 'store';
|
||||||
|
|
||||||
import DraggableTableRow from '..';
|
import DraggableTableRow from '..';
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
Object.defineProperty(window, 'matchMedia', {
|
matchMedia();
|
||||||
writable: true,
|
|
||||||
value: jest.fn().mockImplementation((query) => ({
|
|
||||||
matches: false,
|
|
||||||
media: query,
|
|
||||||
onchange: null,
|
|
||||||
addListener: jest.fn(),
|
|
||||||
removeListener: jest.fn(),
|
|
||||||
addEventListener: jest.fn(),
|
|
||||||
removeEventListener: jest.fn(),
|
|
||||||
dispatchEvent: jest.fn(),
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.mock('uplot', () => {
|
jest.mock('uplot', () => {
|
||||||
@@ -41,14 +34,18 @@ jest.mock('react-dnd', () => ({
|
|||||||
describe('DraggableTableRow Snapshot test', () => {
|
describe('DraggableTableRow Snapshot test', () => {
|
||||||
it('should render DraggableTableRow', async () => {
|
it('should render DraggableTableRow', async () => {
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
<Table
|
<Provider store={store}>
|
||||||
components={{
|
<I18nextProvider i18n={i18n}>
|
||||||
body: {
|
<Table
|
||||||
row: DraggableTableRow,
|
components={{
|
||||||
},
|
body: {
|
||||||
}}
|
row: DraggableTableRow,
|
||||||
pagination={false}
|
},
|
||||||
/>,
|
}}
|
||||||
|
pagination={false}
|
||||||
|
/>
|
||||||
|
</I18nextProvider>
|
||||||
|
</Provider>,
|
||||||
);
|
);
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -99,3 +99,5 @@ exports[`DraggableTableRow Snapshot test should render DraggableTableRow 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</DocumentFragment>
|
</DocumentFragment>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`PipelinePage container test should render AddNewPipeline section 1`] = `<DocumentFragment />`;
|
||||||
|
|||||||
@@ -5,16 +5,18 @@ import { Button, Typography } from 'antd';
|
|||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { CheckCircle2, HandPlatter } from 'lucide-react';
|
import { CheckCircle2, HandPlatter } from 'lucide-react';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
export default function WaitlistFragment({
|
export default function WaitlistFragment({
|
||||||
entityType,
|
entityType,
|
||||||
}: {
|
}: {
|
||||||
entityType: string;
|
entityType: string;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const { user } = useAppContext();
|
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
const { t } = useTranslation(['infraMonitoring']);
|
const { t } = useTranslation(['infraMonitoring']);
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import logEvent from 'api/common/logEvent';
|
|||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
|
import useFeatureFlags from 'hooks/useFeatureFlag';
|
||||||
|
import useLicense from 'hooks/useLicense';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { defaultTo } from 'lodash-es';
|
import { defaultTo } from 'lodash-es';
|
||||||
import { CreditCard, HelpCircle, X } from 'lucide-react';
|
import { CreditCard, HelpCircle, X } from 'lucide-react';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useEffect, useState } from 'react';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
|
||||||
import { useMutation } from 'react-query';
|
import { useMutation } from 'react-query';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
@@ -38,79 +39,31 @@ function LaunchChatSupport({
|
|||||||
onHoverText = '',
|
onHoverText = '',
|
||||||
intercomMessageDisabled = false,
|
intercomMessageDisabled = false,
|
||||||
}: LaunchChatSupportProps): JSX.Element | null {
|
}: LaunchChatSupportProps): JSX.Element | null {
|
||||||
|
const isChatSupportEnabled = useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active;
|
||||||
const isCloudUserVal = isCloudUser();
|
const isCloudUserVal = isCloudUser();
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
const {
|
const { data: licenseData, isFetching } = useLicense();
|
||||||
licenses,
|
|
||||||
isFetchingLicenses,
|
|
||||||
featureFlags,
|
|
||||||
isFetchingFeatureFlags,
|
|
||||||
featureFlagsFetchError,
|
|
||||||
isLoggedIn,
|
|
||||||
} = useAppContext();
|
|
||||||
const [activeLicense, setActiveLicense] = useState<License | null>(null);
|
const [activeLicense, setActiveLicense] = useState<License | null>(null);
|
||||||
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState(
|
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState(
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
const isPremiumChatSupportEnabled =
|
||||||
|
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
|
||||||
|
|
||||||
const isChatSupportEnabled = useMemo(() => {
|
const showAddCreditCardModal =
|
||||||
if (!isFetchingFeatureFlags && (featureFlags || featureFlagsFetchError)) {
|
!isPremiumChatSupportEnabled &&
|
||||||
let isChatSupportEnabled = false;
|
!licenseData?.payload?.trialConvertedToSubscription;
|
||||||
|
|
||||||
if (featureFlags && featureFlags.length > 0) {
|
|
||||||
isChatSupportEnabled =
|
|
||||||
featureFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)
|
|
||||||
?.active || false;
|
|
||||||
}
|
|
||||||
return isChatSupportEnabled;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}, [featureFlags, featureFlagsFetchError, isFetchingFeatureFlags]);
|
|
||||||
|
|
||||||
const showAddCreditCardModal = useMemo(() => {
|
|
||||||
if (
|
|
||||||
!isFetchingFeatureFlags &&
|
|
||||||
(featureFlags || featureFlagsFetchError) &&
|
|
||||||
licenses
|
|
||||||
) {
|
|
||||||
let isChatSupportEnabled = false;
|
|
||||||
let isPremiumSupportEnabled = false;
|
|
||||||
const isCloudUserVal = isCloudUser();
|
|
||||||
if (featureFlags && featureFlags.length > 0) {
|
|
||||||
isChatSupportEnabled =
|
|
||||||
featureFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)
|
|
||||||
?.active || false;
|
|
||||||
|
|
||||||
isPremiumSupportEnabled =
|
|
||||||
featureFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)
|
|
||||||
?.active || false;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
isLoggedIn &&
|
|
||||||
!isPremiumSupportEnabled &&
|
|
||||||
isChatSupportEnabled &&
|
|
||||||
!licenses.trialConvertedToSubscription &&
|
|
||||||
isCloudUserVal
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}, [
|
|
||||||
featureFlags,
|
|
||||||
featureFlagsFetchError,
|
|
||||||
isFetchingFeatureFlags,
|
|
||||||
isLoggedIn,
|
|
||||||
licenses,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isFetchingLicenses && licenses) {
|
const activeValidLicense =
|
||||||
const activeValidLicense =
|
licenseData?.payload?.licenses?.find(
|
||||||
licenses.licenses?.find((license) => license.isCurrent === true) || null;
|
(license) => license.isCurrent === true,
|
||||||
setActiveLicense(activeValidLicense);
|
) || null;
|
||||||
}
|
|
||||||
}, [isFetchingLicenses, licenses]);
|
setActiveLicense(activeValidLicense);
|
||||||
|
}, [licenseData, isFetching]);
|
||||||
|
|
||||||
const handleFacingIssuesClick = (): void => {
|
const handleFacingIssuesClick = (): void => {
|
||||||
if (showAddCreditCardModal) {
|
if (showAddCreditCardModal) {
|
||||||
|
|||||||
@@ -22,13 +22,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.state-indicator {
|
|
||||||
width: 15px;
|
|
||||||
.log-state-indicator {
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-timestamp {
|
.table-timestamp {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -36,6 +29,10 @@
|
|||||||
.ant-typography {
|
.ant-typography {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.log-state-indicator {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
|
|||||||
@@ -75,28 +75,12 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
|
||||||
// We do not need any title and data index for the log state indicator
|
|
||||||
title: '',
|
|
||||||
dataIndex: '',
|
|
||||||
key: 'state-indicator',
|
|
||||||
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
|
|
||||||
children: (
|
|
||||||
<div className={cx('state-indicator', fontSize)}>
|
|
||||||
<LogStateIndicator
|
|
||||||
type={getLogIndicatorTypeForTable(item)}
|
|
||||||
fontSize={fontSize}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'timestamp',
|
title: 'timestamp',
|
||||||
dataIndex: 'timestamp',
|
dataIndex: 'timestamp',
|
||||||
key: 'timestamp',
|
key: 'timestamp',
|
||||||
// https://github.com/ant-design/ant-design/discussions/36886
|
// https://github.com/ant-design/ant-design/discussions/36886
|
||||||
render: (field): ColumnTypeRender<Record<string, unknown>> => {
|
render: (field, item): ColumnTypeRender<Record<string, unknown>> => {
|
||||||
const date =
|
const date =
|
||||||
typeof field === 'string'
|
typeof field === 'string'
|
||||||
? formatTimezoneAdjustedTimestamp(field, 'YYYY-MM-DD HH:mm:ss.SSS')
|
? formatTimezoneAdjustedTimestamp(field, 'YYYY-MM-DD HH:mm:ss.SSS')
|
||||||
@@ -107,6 +91,10 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
return {
|
return {
|
||||||
children: (
|
children: (
|
||||||
<div className="table-timestamp">
|
<div className="table-timestamp">
|
||||||
|
<LogStateIndicator
|
||||||
|
type={getLogIndicatorTypeForTable(item)}
|
||||||
|
fontSize={fontSize}
|
||||||
|
/>
|
||||||
<Typography.Paragraph ellipsis className={cx('text', fontSize)}>
|
<Typography.Paragraph ellipsis className={cx('text', fontSize)}>
|
||||||
{date}
|
{date}
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
|
|||||||
@@ -1,10 +1,31 @@
|
|||||||
|
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||||
import NotFoundImage from 'assets/NotFound';
|
import NotFoundImage from 'assets/NotFound';
|
||||||
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { LOGGED_IN } from 'types/actions/app';
|
||||||
|
|
||||||
import { defaultText } from './constant';
|
import { defaultText } from './constant';
|
||||||
import { Button, Container, Text, TextContainer } from './styles';
|
import { Button, Container, Text, TextContainer } from './styles';
|
||||||
|
|
||||||
function NotFound({ text = defaultText }: Props): JSX.Element {
|
function NotFound({ text = defaultText }: Props): JSX.Element {
|
||||||
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
|
const isLoggedIn = getLocalStorageKey(LOCALSTORAGE.IS_LOGGED_IN);
|
||||||
|
|
||||||
|
const onClickHandler = useCallback(() => {
|
||||||
|
if (isLoggedIn) {
|
||||||
|
dispatch({
|
||||||
|
type: LOGGED_IN,
|
||||||
|
payload: {
|
||||||
|
isLoggedIn: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [dispatch, isLoggedIn]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<NotFoundImage />
|
<NotFoundImage />
|
||||||
@@ -14,7 +35,7 @@ function NotFound({ text = defaultText }: Props): JSX.Element {
|
|||||||
<Text>Page Not Found</Text>
|
<Text>Page Not Found</Text>
|
||||||
</TextContainer>
|
</TextContainer>
|
||||||
|
|
||||||
<Button to={ROUTES.APPLICATION} tabIndex={0}>
|
<Button onClick={onClickHandler} to={ROUTES.APPLICATION} tabIndex={0}>
|
||||||
Return To Services Page
|
Return To Services Page
|
||||||
</Button>
|
</Button>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -1,28 +1,40 @@
|
|||||||
import { Button, Space } from 'antd';
|
import { Button, Space } from 'antd';
|
||||||
import setFlags from 'api/user/setFlags';
|
import setFlags from 'api/user/setFlags';
|
||||||
import MessageTip from 'components/MessageTip';
|
import MessageTip from 'components/MessageTip';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { UPDATE_USER_FLAG } from 'types/actions/app';
|
||||||
import { UserFlags } from 'types/api/user/setFlags';
|
import { UserFlags } from 'types/api/user/setFlags';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
import ReleaseNoteProps from '../ReleaseNoteProps';
|
import ReleaseNoteProps from '../ReleaseNoteProps';
|
||||||
|
|
||||||
export default function ReleaseNote0120({
|
export default function ReleaseNote0120({
|
||||||
release,
|
release,
|
||||||
}: ReleaseNoteProps): JSX.Element | null {
|
}: ReleaseNoteProps): JSX.Element | null {
|
||||||
const { user, setUserFlags } = useAppContext();
|
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
|
|
||||||
const handleDontShow = useCallback(async (): Promise<void> => {
|
const handleDontShow = useCallback(async (): Promise<void> => {
|
||||||
const flags: UserFlags = { ReleaseNote0120Hide: 'Y' };
|
const flags: UserFlags = { ReleaseNote0120Hide: 'Y' };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setUserFlags(flags);
|
dispatch({
|
||||||
|
type: UPDATE_USER_FLAG,
|
||||||
|
payload: {
|
||||||
|
flags,
|
||||||
|
},
|
||||||
|
});
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// no user is set, so escape the routine
|
// no user is set, so escape the routine
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await setFlags({ userId: user.id, flags });
|
const response = await setFlags({ userId: user?.userId, flags });
|
||||||
|
|
||||||
if (response.statusCode !== 200) {
|
if (response.statusCode !== 200) {
|
||||||
console.log('failed to complete do not show status', response.error);
|
console.log('failed to complete do not show status', response.error);
|
||||||
@@ -32,7 +44,7 @@ export default function ReleaseNote0120({
|
|||||||
// the user can switch the do no show option again in the further.
|
// the user can switch the do no show option again in the further.
|
||||||
console.log('unexpected error: failed to complete do not show status', e);
|
console.log('unexpected error: failed to complete do not show status', e);
|
||||||
}
|
}
|
||||||
}, [setUserFlags, user]);
|
}, [dispatch, user]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageTip
|
<MessageTip
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import ReleaseNoteProps from 'components/ReleaseNote/ReleaseNoteProps';
|
import ReleaseNoteProps from 'components/ReleaseNote/ReleaseNoteProps';
|
||||||
import ReleaseNote0120 from 'components/ReleaseNote/Releases/ReleaseNote0120';
|
import ReleaseNote0120 from 'components/ReleaseNote/Releases/ReleaseNote0120';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { UserFlags } from 'types/api/user/setFlags';
|
import { UserFlags } from 'types/api/user/setFlags';
|
||||||
@@ -45,13 +44,12 @@ const allComponentMap: ComponentMapType[] = [
|
|||||||
// ReleaseNote prints release specific warnings and notes that
|
// ReleaseNote prints release specific warnings and notes that
|
||||||
// user needs to be aware of before using the upgraded version.
|
// user needs to be aware of before using the upgraded version.
|
||||||
function ReleaseNote({ path }: ReleaseNoteProps): JSX.Element | null {
|
function ReleaseNote({ path }: ReleaseNoteProps): JSX.Element | null {
|
||||||
const { user } = useAppContext();
|
const { userFlags, currentVersion } = useSelector<AppState, AppReducer>(
|
||||||
const { currentVersion } = useSelector<AppState, AppReducer>(
|
|
||||||
(state) => state.app,
|
(state) => state.app,
|
||||||
);
|
);
|
||||||
|
|
||||||
const c = allComponentMap.find((item) =>
|
const c = allComponentMap.find((item) =>
|
||||||
item.match(path, currentVersion, user.flags),
|
item.match(path, currentVersion, userFlags),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!c) {
|
if (!c) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ function TabLabel({
|
|||||||
isDisabled,
|
isDisabled,
|
||||||
tooltipText,
|
tooltipText,
|
||||||
}: TabLabelProps): JSX.Element {
|
}: TabLabelProps): JSX.Element {
|
||||||
const currentLabel = <span data-testid={`${label}`}>{label}</span>;
|
const currentLabel = <span>{label}</span>;
|
||||||
|
|
||||||
if (isDisabled) {
|
if (isDisabled) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -21,7 +21,5 @@ export enum LOCALSTORAGE {
|
|||||||
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
|
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
|
||||||
LAST_USED_SAVED_VIEWS = 'LAST_USED_SAVED_VIEWS',
|
LAST_USED_SAVED_VIEWS = 'LAST_USED_SAVED_VIEWS',
|
||||||
SHOW_LOGS_QUICK_FILTERS = 'SHOW_LOGS_QUICK_FILTERS',
|
SHOW_LOGS_QUICK_FILTERS = 'SHOW_LOGS_QUICK_FILTERS',
|
||||||
USER_ID = 'USER_ID',
|
|
||||||
PREFERRED_TIMEZONE = 'PREFERRED_TIMEZONE',
|
PREFERRED_TIMEZONE = 'PREFERRED_TIMEZONE',
|
||||||
UNAUTHENTICATED_ROUTE_HIT = 'UNAUTHENTICATED_ROUTE_HIT',
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/api-keys',
|
API_KEYS: '/settings/access-tokens',
|
||||||
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',
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ describe('APIKeys component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders APIKeys component without crashing', () => {
|
it('renders APIKeys component without crashing', () => {
|
||||||
expect(screen.getByText('API Keys')).toBeInTheDocument();
|
expect(screen.getByText('Access Tokens')).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByText('Create and manage API keys for the SigNoz API'),
|
screen.getByText('Create and manage access tokens for the SigNoz API'),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,16 +40,16 @@ describe('APIKeys component', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByText('No Expiry Key')).toBeInTheDocument();
|
expect(screen.getByText('No Expiry Token')).toBeInTheDocument();
|
||||||
expect(screen.getByText('1-5 of 18 keys')).toBeInTheDocument();
|
expect(screen.getByText('1-5 of 18 tokens')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('opens add new key modal on button click', async () => {
|
it('opens add new key modal on button click', async () => {
|
||||||
fireEvent.click(screen.getByText('New Key'));
|
fireEvent.click(screen.getByText('New Token'));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const createNewKeyBtn = screen.getByRole('button', {
|
const createNewKeyBtn = screen.getByRole('button', {
|
||||||
name: /Create new key/i,
|
name: /Create new token/i,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(createNewKeyBtn).toBeInTheDocument();
|
expect(createNewKeyBtn).toBeInTheDocument();
|
||||||
@@ -57,10 +57,10 @@ describe('APIKeys component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('closes add new key modal on cancel button click', async () => {
|
it('closes add new key modal on cancel button click', async () => {
|
||||||
fireEvent.click(screen.getByText('New Key'));
|
fireEvent.click(screen.getByText('New Token'));
|
||||||
|
|
||||||
const createNewKeyBtn = screen.getByRole('button', {
|
const createNewKeyBtn = screen.getByRole('button', {
|
||||||
name: /Create new key/i,
|
name: /Create new token/i,
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -79,10 +79,10 @@ describe('APIKeys component', () => {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('New Key'));
|
fireEvent.click(screen.getByText('New Token'));
|
||||||
|
|
||||||
const createNewKeyBtn = screen.getByRole('button', {
|
const createNewKeyBtn = screen.getByRole('button', {
|
||||||
name: /Create new key/i,
|
name: /Create new token/i,
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@@ -90,7 +90,7 @@ describe('APIKeys component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
const inputElement = screen.getByPlaceholderText('Enter Key Name');
|
const inputElement = screen.getByPlaceholderText('Enter Token Name');
|
||||||
fireEvent.change(inputElement, { target: { value: 'Top Secret' } });
|
fireEvent.change(inputElement, { target: { value: 'Top Secret' } });
|
||||||
fireEvent.click(screen.getByTestId('create-form-admin-role-btn'));
|
fireEvent.click(screen.getByTestId('create-form-admin-role-btn'));
|
||||||
fireEvent.click(createNewKeyBtn);
|
fireEvent.click(createNewKeyBtn);
|
||||||
|
|||||||
@@ -44,12 +44,14 @@ import {
|
|||||||
View,
|
View,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { ChangeEvent, useEffect, useState } from 'react';
|
import { ChangeEvent, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useMutation } from 'react-query';
|
import { useMutation } from 'react-query';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import { useCopyToClipboard } from 'react-use';
|
import { useCopyToClipboard } from 'react-use';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import { APIKeyProps } from 'types/api/pat/types';
|
import { APIKeyProps } from 'types/api/pat/types';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
import { USER_ROLES } from 'types/roles';
|
import { USER_ROLES } from 'types/roles';
|
||||||
|
|
||||||
export const showErrorNotification = (
|
export const showErrorNotification = (
|
||||||
@@ -97,7 +99,7 @@ export const getDateDifference = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
function APIKeys(): JSX.Element {
|
function APIKeys(): JSX.Element {
|
||||||
const { user } = useAppContext();
|
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
@@ -512,15 +514,15 @@ function APIKeys(): JSX.Element {
|
|||||||
<div className="api-key-container">
|
<div className="api-key-container">
|
||||||
<div className="api-key-content">
|
<div className="api-key-content">
|
||||||
<header>
|
<header>
|
||||||
<Typography.Title className="title">API Keys</Typography.Title>
|
<Typography.Title className="title">Access Tokens </Typography.Title>
|
||||||
<Typography.Text className="subtitle">
|
<Typography.Text className="subtitle">
|
||||||
Create and manage API keys for the SigNoz API
|
Create and manage access tokens for the SigNoz API
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="api-keys-search-add-new">
|
<div className="api-keys-search-add-new">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search for keys..."
|
placeholder="Search for token..."
|
||||||
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
|
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
@@ -531,7 +533,7 @@ function APIKeys(): JSX.Element {
|
|||||||
type="primary"
|
type="primary"
|
||||||
onClick={showAddModal}
|
onClick={showAddModal}
|
||||||
>
|
>
|
||||||
<Plus size={14} /> New Key
|
<Plus size={14} /> New Token
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -544,7 +546,7 @@ function APIKeys(): JSX.Element {
|
|||||||
pageSize: 5,
|
pageSize: 5,
|
||||||
hideOnSinglePage: true,
|
hideOnSinglePage: true,
|
||||||
showTotal: (total: number, range: number[]): string =>
|
showTotal: (total: number, range: number[]): string =>
|
||||||
`${range[0]}-${range[1]} of ${total} keys`,
|
`${range[0]}-${range[1]} of ${total} tokens`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -552,7 +554,7 @@ function APIKeys(): JSX.Element {
|
|||||||
{/* Delete Key Modal */}
|
{/* Delete Key Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
className="delete-api-key-modal"
|
className="delete-api-key-modal"
|
||||||
title={<span className="title">Delete Key</span>}
|
title={<span className="title">Delete Token</span>}
|
||||||
open={isDeleteModalOpen}
|
open={isDeleteModalOpen}
|
||||||
closable
|
closable
|
||||||
afterClose={handleModalClose}
|
afterClose={handleModalClose}
|
||||||
@@ -574,7 +576,7 @@ function APIKeys(): JSX.Element {
|
|||||||
onClick={onDeleteHandler}
|
onClick={onDeleteHandler}
|
||||||
className="delete-btn"
|
className="delete-btn"
|
||||||
>
|
>
|
||||||
Delete key
|
Delete Token
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -588,7 +590,7 @@ function APIKeys(): JSX.Element {
|
|||||||
{/* Edit Key Modal */}
|
{/* Edit Key Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
className="api-key-modal"
|
className="api-key-modal"
|
||||||
title="Edit key"
|
title="Edit token"
|
||||||
open={isEditModalOpen}
|
open={isEditModalOpen}
|
||||||
key="edit-api-key-modal"
|
key="edit-api-key-modal"
|
||||||
afterClose={handleModalClose}
|
afterClose={handleModalClose}
|
||||||
@@ -612,7 +614,7 @@ function APIKeys(): JSX.Element {
|
|||||||
icon={<Check size={14} />}
|
icon={<Check size={14} />}
|
||||||
onClick={onUpdateApiKey}
|
onClick={onUpdateApiKey}
|
||||||
>
|
>
|
||||||
Update key
|
Update Token
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
@@ -632,7 +634,7 @@ function APIKeys(): JSX.Element {
|
|||||||
label="Name"
|
label="Name"
|
||||||
rules={[{ required: true }, { type: 'string', min: 6 }]}
|
rules={[{ required: true }, { type: 'string', min: 6 }]}
|
||||||
>
|
>
|
||||||
<Input placeholder="Enter Key Name" autoFocus />
|
<Input placeholder="Enter Token Name" autoFocus />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="role" label="Role">
|
<Form.Item name="role" label="Role">
|
||||||
@@ -666,7 +668,7 @@ function APIKeys(): JSX.Element {
|
|||||||
{/* Create New Key Modal */}
|
{/* Create New Key Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
className="api-key-modal"
|
className="api-key-modal"
|
||||||
title="Create new key"
|
title="Create new token"
|
||||||
open={isAddModalOpen}
|
open={isAddModalOpen}
|
||||||
key="create-api-key-modal"
|
key="create-api-key-modal"
|
||||||
closable
|
closable
|
||||||
@@ -683,7 +685,7 @@ function APIKeys(): JSX.Element {
|
|||||||
onClick={handleCopyClose}
|
onClick={handleCopyClose}
|
||||||
icon={<Check size={12} />}
|
icon={<Check size={12} />}
|
||||||
>
|
>
|
||||||
Copy key and close
|
Copy token and close
|
||||||
</Button>,
|
</Button>,
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
@@ -704,7 +706,7 @@ function APIKeys(): JSX.Element {
|
|||||||
loading={isLoadingCreateAPIKey}
|
loading={isLoadingCreateAPIKey}
|
||||||
onClick={onCreateAPIKey}
|
onClick={onCreateAPIKey}
|
||||||
>
|
>
|
||||||
Create new key
|
Create new token
|
||||||
</Button>,
|
</Button>,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -728,7 +730,7 @@ function APIKeys(): JSX.Element {
|
|||||||
rules={[{ required: true }, { type: 'string', min: 6 }]}
|
rules={[{ required: true }, { type: 'string', min: 6 }]}
|
||||||
validateTrigger="onFinish"
|
validateTrigger="onFinish"
|
||||||
>
|
>
|
||||||
<Input placeholder="Enter Key Name" autoFocus />
|
<Input placeholder="Enter Token Name" autoFocus />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item name="role" label="Role">
|
<Form.Item name="role" label="Role">
|
||||||
@@ -769,7 +771,7 @@ function APIKeys(): JSX.Element {
|
|||||||
{showNewAPIKeyDetails && (
|
{showNewAPIKeyDetails && (
|
||||||
<div className="api-key-info-container">
|
<div className="api-key-info-container">
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={8}>Key</Col>
|
<Col span={8}>Token</Col>
|
||||||
<Col span={16}>
|
<Col span={16}>
|
||||||
<span className="copyable-text">
|
<span className="copyable-text">
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import ROUTES from 'constants/routes';
|
|||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import { generatePath } from 'react-router-dom';
|
import { generatePath } from 'react-router-dom';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import { Channels, PayloadProps } from 'types/api/channels/getAll';
|
import { Channels, PayloadProps } from 'types/api/channels/getAll';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
import Delete from './Delete';
|
import Delete from './Delete';
|
||||||
|
|
||||||
@@ -18,8 +20,8 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
|
|||||||
const { t } = useTranslation(['channels']);
|
const { t } = useTranslation(['channels']);
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
const [channels, setChannels] = useState<Channels[]>(allChannels);
|
const [channels, setChannels] = useState<Channels[]>(allChannels);
|
||||||
const { user } = useAppContext();
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
const [action] = useComponentPermission(['new_alert_action'], user.role);
|
const [action] = useComponentPermission(['new_alert_action'], role);
|
||||||
|
|
||||||
const onClickEditHandler = useCallback((id: string) => {
|
const onClickEditHandler = useCallback((id: string) => {
|
||||||
history.replace(
|
history.replace(
|
||||||
|
|||||||
@@ -31,6 +31,13 @@ jest.mock('hooks/useNotifications', () => ({
|
|||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('hooks/useFeatureFlag', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn().mockImplementation(() => ({
|
||||||
|
active: true,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('Create Alert Channel', () => {
|
describe('Create Alert Channel', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
@@ -355,7 +362,7 @@ describe('Create Alert Channel', () => {
|
|||||||
expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue);
|
expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('Email', () => {
|
describe('Opsgenie', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
render(<CreateAlertChannels preType={ChannelType.Email} />);
|
render(<CreateAlertChannels preType={ChannelType.Email} />);
|
||||||
});
|
});
|
||||||
@@ -378,9 +385,7 @@ describe('Create Alert Channel', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should check if the selected item in the type dropdown has text "msteams"', () => {
|
it('Should check if the selected item in the type dropdown has text "msteams"', () => {
|
||||||
expect(
|
expect(screen.getByText('msteams')).toBeInTheDocument();
|
||||||
screen.getByText('Microsoft Teams (Supported in Paid Plans Only)'),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should check if Webhook URL label and input are displayed properly ', () => {
|
it('Should check if Webhook URL label and input are displayed properly ', () => {
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ describe('Create Alert Channel (Normal User)', () => {
|
|||||||
expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue);
|
expect(priorityTextArea).toHaveValue(opsGeniePriorityDefaultValue);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('Email', () => {
|
describe('Opsgenie', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
render(<CreateAlertChannels preType={ChannelType.Email} />);
|
render(<CreateAlertChannels preType={ChannelType.Email} />);
|
||||||
});
|
});
|
||||||
@@ -314,8 +314,7 @@ describe('Create Alert Channel (Normal User)', () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO[vikrantgupta25]: check with Shaheer
|
it('Should check if the upgrade plan message is shown', () => {
|
||||||
it.skip('Should check if the upgrade plan message is shown', () => {
|
|
||||||
expect(screen.getByText('Upgrade to a Paid Plan')).toBeInTheDocument();
|
expect(screen.getByText('Upgrade to a Paid Plan')).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(/This feature is available for paid plans only./),
|
screen.getByText(/This feature is available for paid plans only./),
|
||||||
@@ -336,7 +335,7 @@ describe('Create Alert Channel (Normal User)', () => {
|
|||||||
screen.getByRole('button', { name: 'button_return' }),
|
screen.getByRole('button', { name: 'button_return' }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it.skip('Should check if save and test buttons are disabled', () => {
|
it('Should check if save and test buttons are disabled', () => {
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole('button', { name: 'button_save_channel' }),
|
screen.getByRole('button', { name: 'button_save_channel' }),
|
||||||
).toBeDisabled();
|
).toBeDisabled();
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ jest.mock('hooks/useNotifications', () => ({
|
|||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('hooks/useFeatureFlag', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn().mockImplementation(() => ({
|
||||||
|
active: true,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('Should check if the edit alert channel is properly displayed ', () => {
|
describe('Should check if the edit alert channel is properly displayed ', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
render(<EditAlertChannels initialValue={editAlertChannelInitialValue} />);
|
render(<EditAlertChannels initialValue={editAlertChannelInitialValue} />);
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import useComponentPermission from 'hooks/useComponentPermission';
|
|||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { isUndefined } from 'lodash-es';
|
import { isUndefined } from 'lodash-es';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
import AlertChannelsComponent from './AlertChannels';
|
import AlertChannelsComponent from './AlertChannels';
|
||||||
import { Button, ButtonContainer, RightActionContainer } from './styles';
|
import { Button, ButtonContainer, RightActionContainer } from './styles';
|
||||||
@@ -20,10 +22,10 @@ const { Paragraph } = Typography;
|
|||||||
|
|
||||||
function AlertChannels(): JSX.Element {
|
function AlertChannels(): JSX.Element {
|
||||||
const { t } = useTranslation(['channels']);
|
const { t } = useTranslation(['channels']);
|
||||||
const { user } = useAppContext();
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
const [addNewChannelPermission] = useComponentPermission(
|
const [addNewChannelPermission] = useComponentPermission(
|
||||||
['add_new_channel'],
|
['add_new_channel'],
|
||||||
user.role,
|
role,
|
||||||
);
|
);
|
||||||
const onToggleHandler = useCallback(() => {
|
const onToggleHandler = useCallback(() => {
|
||||||
history.push(ROUTES.CHANNELS_NEW);
|
history.push(ROUTES.CHANNELS_NEW);
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import SideNav from 'container/SideNav';
|
|||||||
import TopNav from 'container/TopNav';
|
import TopNav from 'container/TopNav';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import useFeatureFlags from 'hooks/useFeatureFlag';
|
||||||
|
import useLicense from 'hooks/useLicense';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { isNull } from 'lodash-es';
|
import { isNull } from 'lodash-es';
|
||||||
@@ -27,9 +29,10 @@ import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import { Helmet } from 'react-helmet-async';
|
import { Helmet } from 'react-helmet-async';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useMutation, useQueries } from 'react-query';
|
import { useMutation, useQueries } from 'react-query';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import {
|
import {
|
||||||
UPDATE_CURRENT_ERROR,
|
UPDATE_CURRENT_ERROR,
|
||||||
@@ -40,6 +43,7 @@ import {
|
|||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||||
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
import { isCloudUser } from 'utils/app';
|
import { isCloudUser } from 'utils/app';
|
||||||
import {
|
import {
|
||||||
getFormattedDate,
|
getFormattedDate,
|
||||||
@@ -52,18 +56,11 @@ import { getRouteKey } from './utils';
|
|||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
function AppLayout(props: AppLayoutProps): JSX.Element {
|
function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||||
const {
|
const { isLoggedIn, user, role } = useSelector<AppState, AppReducer>(
|
||||||
isLoggedIn,
|
(state) => state.app,
|
||||||
user,
|
);
|
||||||
licenses,
|
|
||||||
isFetchingLicenses,
|
|
||||||
activeLicenseV3,
|
|
||||||
isFetchingActiveLicenseV3,
|
|
||||||
featureFlags,
|
|
||||||
isFetchingFeatureFlags,
|
|
||||||
featureFlagsFetchError,
|
|
||||||
} = useAppContext();
|
|
||||||
|
|
||||||
|
const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext();
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
const [
|
const [
|
||||||
@@ -101,6 +98,23 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const { data: licenseData, isFetching } = useLicense();
|
||||||
|
|
||||||
|
const isPremiumChatSupportEnabled =
|
||||||
|
useFeatureFlags(FeatureKeys.PREMIUM_SUPPORT)?.active || false;
|
||||||
|
|
||||||
|
const isChatSupportEnabled =
|
||||||
|
useFeatureFlags(FeatureKeys.CHAT_SUPPORT)?.active || false;
|
||||||
|
|
||||||
|
const isCloudUserVal = isCloudUser();
|
||||||
|
|
||||||
|
const showAddCreditCardModal =
|
||||||
|
isLoggedIn &&
|
||||||
|
isChatSupportEnabled &&
|
||||||
|
isCloudUserVal &&
|
||||||
|
!isPremiumChatSupportEnabled &&
|
||||||
|
!licenseData?.payload?.trialConvertedToSubscription;
|
||||||
|
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const { t } = useTranslation(['titles']);
|
const { t } = useTranslation(['titles']);
|
||||||
|
|
||||||
@@ -234,16 +248,15 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!isFetchingLicenses &&
|
!isFetching &&
|
||||||
licenses &&
|
licenseData?.payload?.onTrial &&
|
||||||
licenses.onTrial &&
|
!licenseData?.payload?.trialConvertedToSubscription &&
|
||||||
!licenses.trialConvertedToSubscription &&
|
!licenseData?.payload?.workSpaceBlock &&
|
||||||
!licenses.workSpaceBlock &&
|
getRemainingDays(licenseData?.payload.trialEnd) < 7
|
||||||
getRemainingDays(licenses.trialEnd) < 7
|
|
||||||
) {
|
) {
|
||||||
setShowTrialExpiryBanner(true);
|
setShowTrialExpiryBanner(true);
|
||||||
}
|
}
|
||||||
}, [isFetchingLicenses, licenses]);
|
}, [licenseData, isFetching]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
@@ -259,12 +272,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
// after logging out hide the trial expiry banner
|
// after logging out hide the trial expiry banner
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
setShowTrialExpiryBanner(false);
|
setShowTrialExpiryBanner(false);
|
||||||
setShowPaymentFailedWarning(false);
|
|
||||||
}
|
}
|
||||||
}, [isLoggedIn]);
|
}, [isLoggedIn]);
|
||||||
|
|
||||||
const handleUpgrade = (): void => {
|
const handleUpgrade = (): void => {
|
||||||
if (user.role === 'ADMIN') {
|
if (role === 'ADMIN') {
|
||||||
history.push(ROUTES.BILLING);
|
history.push(ROUTES.BILLING);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -315,41 +327,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [isDarkMode]);
|
}, [isDarkMode]);
|
||||||
|
|
||||||
const showAddCreditCardModal = useMemo(() => {
|
|
||||||
if (
|
|
||||||
!isFetchingFeatureFlags &&
|
|
||||||
(featureFlags || featureFlagsFetchError) &&
|
|
||||||
licenses
|
|
||||||
) {
|
|
||||||
let isChatSupportEnabled = false;
|
|
||||||
let isPremiumSupportEnabled = false;
|
|
||||||
const isCloudUserVal = isCloudUser();
|
|
||||||
if (featureFlags && featureFlags.length > 0) {
|
|
||||||
isChatSupportEnabled =
|
|
||||||
featureFlags.find((flag) => flag.name === FeatureKeys.CHAT_SUPPORT)
|
|
||||||
?.active || false;
|
|
||||||
|
|
||||||
isPremiumSupportEnabled =
|
|
||||||
featureFlags.find((flag) => flag.name === FeatureKeys.PREMIUM_SUPPORT)
|
|
||||||
?.active || false;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
isLoggedIn &&
|
|
||||||
!isPremiumSupportEnabled &&
|
|
||||||
isChatSupportEnabled &&
|
|
||||||
!licenses.trialConvertedToSubscription &&
|
|
||||||
isCloudUserVal
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}, [
|
|
||||||
featureFlags,
|
|
||||||
featureFlagsFetchError,
|
|
||||||
isFetchingFeatureFlags,
|
|
||||||
isLoggedIn,
|
|
||||||
licenses,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout className={cx(isDarkMode ? 'darkMode' : 'lightMode')}>
|
<Layout className={cx(isDarkMode ? 'darkMode' : 'lightMode')}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@@ -359,8 +336,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
{showTrialExpiryBanner && !showPaymentFailedWarning && (
|
{showTrialExpiryBanner && !showPaymentFailedWarning && (
|
||||||
<div className="trial-expiry-banner">
|
<div className="trial-expiry-banner">
|
||||||
You are in free trial period. Your free trial will end on{' '}
|
You are in free trial period. Your free trial will end on{' '}
|
||||||
<span>{getFormattedDate(licenses?.trialEnd || Date.now())}.</span>
|
<span>
|
||||||
{user.role === 'ADMIN' ? (
|
{getFormattedDate(licenseData?.payload?.trialEnd || Date.now())}.
|
||||||
|
</span>
|
||||||
|
{role === 'ADMIN' ? (
|
||||||
<span>
|
<span>
|
||||||
{' '}
|
{' '}
|
||||||
Please{' '}
|
Please{' '}
|
||||||
@@ -383,7 +362,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
.
|
.
|
||||||
</span>
|
</span>
|
||||||
{user.role === 'ADMIN' ? (
|
{role === 'ADMIN' ? (
|
||||||
<span>
|
<span>
|
||||||
{' '}
|
{' '}
|
||||||
Please{' '}
|
Please{' '}
|
||||||
@@ -406,7 +385,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}>
|
<Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}>
|
||||||
{isToDisplayLayout && !renderFullScreen && <SideNav />}
|
{isToDisplayLayout && !renderFullScreen && (
|
||||||
|
<SideNav licenseData={licenseData} isFetching={isFetching} />
|
||||||
|
)}
|
||||||
<div className="app-content" data-overlayscrollbars-initialize>
|
<div className="app-content" data-overlayscrollbars-initialize>
|
||||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||||
<LayoutContent data-overlayscrollbars-initialize>
|
<LayoutContent data-overlayscrollbars-initialize>
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { billingSuccessResponse } from 'mocks-server/__mockdata__/billing';
|
import { billingSuccessResponse } from 'mocks-server/__mockdata__/billing';
|
||||||
import {
|
import {
|
||||||
licensesSuccessResponse,
|
|
||||||
notOfTrailResponse,
|
notOfTrailResponse,
|
||||||
trialConvertedToSubscriptionResponse,
|
trialConvertedToSubscriptionResponse,
|
||||||
} from 'mocks-server/__mockdata__/licenses';
|
} from 'mocks-server/__mockdata__/licenses';
|
||||||
import { act, render, screen, waitFor } from 'tests/test-utils';
|
import { server } from 'mocks-server/server';
|
||||||
|
import { rest } from 'msw';
|
||||||
|
import { act, render, screen } from 'tests/test-utils';
|
||||||
import { getFormattedDate } from 'utils/timeUtils';
|
import { getFormattedDate } from 'utils/timeUtils';
|
||||||
|
|
||||||
import BillingContainer from './BillingContainer';
|
import BillingContainer from './BillingContainer';
|
||||||
|
|
||||||
|
const lisenceUrl = 'http://localhost/api/v2/licenses';
|
||||||
|
|
||||||
jest.mock('uplot', () => {
|
jest.mock('uplot', () => {
|
||||||
const paths = {
|
const paths = {
|
||||||
spline: jest.fn(),
|
spline: jest.fn(),
|
||||||
@@ -35,7 +38,9 @@ window.ResizeObserver =
|
|||||||
|
|
||||||
describe('BillingContainer', () => {
|
describe('BillingContainer', () => {
|
||||||
test('Component should render', async () => {
|
test('Component should render', async () => {
|
||||||
render(<BillingContainer />);
|
act(() => {
|
||||||
|
render(<BillingContainer />);
|
||||||
|
});
|
||||||
|
|
||||||
const dataInjection = screen.getByRole('columnheader', {
|
const dataInjection = screen.getByRole('columnheader', {
|
||||||
name: /data ingested/i,
|
name: /data ingested/i,
|
||||||
@@ -50,18 +55,13 @@ describe('BillingContainer', () => {
|
|||||||
});
|
});
|
||||||
expect(cost).toBeInTheDocument();
|
expect(cost).toBeInTheDocument();
|
||||||
|
|
||||||
const dayRemainingInBillingPeriod = await screen.findByText(
|
|
||||||
/11 days_remaining/i,
|
|
||||||
);
|
|
||||||
expect(dayRemainingInBillingPeriod).toBeInTheDocument();
|
|
||||||
|
|
||||||
const manageBilling = screen.getByRole('button', {
|
const manageBilling = screen.getByRole('button', {
|
||||||
name: 'manage_billing',
|
name: 'manage_billing',
|
||||||
});
|
});
|
||||||
expect(manageBilling).toBeInTheDocument();
|
expect(manageBilling).toBeInTheDocument();
|
||||||
|
|
||||||
const dollar = screen.getByText(/\$1,278.3/i);
|
const dollar = screen.getByText(/\$0/i);
|
||||||
await waitFor(() => expect(dollar).toBeInTheDocument());
|
expect(dollar).toBeInTheDocument();
|
||||||
|
|
||||||
const currentBill = screen.getByText('billing');
|
const currentBill = screen.getByText('billing');
|
||||||
expect(currentBill).toBeInTheDocument();
|
expect(currentBill).toBeInTheDocument();
|
||||||
@@ -69,9 +69,7 @@ describe('BillingContainer', () => {
|
|||||||
|
|
||||||
test('OnTrail', async () => {
|
test('OnTrail', async () => {
|
||||||
act(() => {
|
act(() => {
|
||||||
render(<BillingContainer />, undefined, undefined, {
|
render(<BillingContainer />);
|
||||||
licenses: licensesSuccessResponse.data,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const freeTrailText = await screen.findByText('Free Trial');
|
const freeTrailText = await screen.findByText('Free Trial');
|
||||||
@@ -102,10 +100,14 @@ describe('BillingContainer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('OnTrail but trialConvertedToSubscription', async () => {
|
test('OnTrail but trialConvertedToSubscription', async () => {
|
||||||
|
server.use(
|
||||||
|
rest.get(lisenceUrl, (req, res, ctx) =>
|
||||||
|
res(ctx.status(200), ctx.json(trialConvertedToSubscriptionResponse)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
render(<BillingContainer />, undefined, undefined, {
|
render(<BillingContainer />);
|
||||||
licenses: trialConvertedToSubscriptionResponse.data,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentBill = screen.getByText('billing');
|
const currentBill = screen.getByText('billing');
|
||||||
@@ -136,9 +138,12 @@ describe('BillingContainer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Not on ontrail', async () => {
|
test('Not on ontrail', async () => {
|
||||||
const { findByText } = render(<BillingContainer />, undefined, undefined, {
|
server.use(
|
||||||
licenses: notOfTrailResponse.data,
|
rest.get(lisenceUrl, (req, res, ctx) =>
|
||||||
});
|
res(ctx.status(200), ctx.json(notOfTrailResponse)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const { findByText } = render(<BillingContainer />);
|
||||||
|
|
||||||
const billingPeriodText = `Your current billing period is from ${getFormattedDate(
|
const billingPeriodText = `Your current billing period is from ${getFormattedDate(
|
||||||
billingSuccessResponse.data.billingPeriodStart,
|
billingSuccessResponse.data.billingPeriodStart,
|
||||||
@@ -163,4 +168,17 @@ describe('BillingContainer', () => {
|
|||||||
});
|
});
|
||||||
expect(logRow).toBeInTheDocument();
|
expect(logRow).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Should render corrent day remaining in billing period', async () => {
|
||||||
|
server.use(
|
||||||
|
rest.get(lisenceUrl, (req, res, ctx) =>
|
||||||
|
res(ctx.status(200), ctx.json(notOfTrailResponse)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
render(<BillingContainer />);
|
||||||
|
const dayRemainingInBillingPeriod = await screen.findByText(
|
||||||
|
/11 days_remaining/i,
|
||||||
|
);
|
||||||
|
expect(dayRemainingInBillingPeriod).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,15 +24,18 @@ import Spinner from 'components/Spinner';
|
|||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import useAxiosError from 'hooks/useAxiosError';
|
import useAxiosError from 'hooks/useAxiosError';
|
||||||
|
import useLicense from 'hooks/useLicense';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { isEmpty, pick } from 'lodash-es';
|
import { isEmpty, pick } from 'lodash-es';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useMutation, useQuery } from 'react-query';
|
import { useMutation, useQuery } from 'react-query';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||||
import { License } from 'types/api/licenses/def';
|
import { License } from 'types/api/licenses/def';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
import { isCloudUser } from 'utils/app';
|
import { isCloudUser } from 'utils/app';
|
||||||
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
||||||
|
|
||||||
@@ -134,13 +137,9 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
Partial<UsageResponsePayloadProps>
|
Partial<UsageResponsePayloadProps>
|
||||||
>({});
|
>({});
|
||||||
|
|
||||||
const {
|
const { isFetching, data: licensesData, error: licenseError } = useLicense();
|
||||||
user,
|
|
||||||
org,
|
const { user, org } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
licenses,
|
|
||||||
isFetchingLicenses,
|
|
||||||
licensesFetchError,
|
|
||||||
} = useAppContext();
|
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
const handleError = useAxiosError();
|
const handleError = useAxiosError();
|
||||||
@@ -182,7 +181,7 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
|
|
||||||
setData(formattedUsageData);
|
setData(formattedUsageData);
|
||||||
|
|
||||||
if (!licenses?.onTrial) {
|
if (!licensesData?.payload?.onTrial) {
|
||||||
const remainingDays = getRemainingDays(billingPeriodEnd) - 1;
|
const remainingDays = getRemainingDays(billingPeriodEnd) - 1;
|
||||||
|
|
||||||
setHeaderText(
|
setHeaderText(
|
||||||
@@ -196,14 +195,14 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
|
|
||||||
setApiResponse(data?.payload || {});
|
setApiResponse(data?.payload || {});
|
||||||
},
|
},
|
||||||
[licenses?.onTrial],
|
[licensesData?.payload?.onTrial],
|
||||||
);
|
);
|
||||||
|
|
||||||
const isSubscriptionPastDue =
|
const isSubscriptionPastDue =
|
||||||
apiResponse.subscriptionStatus === SubscriptionStatus.PastDue;
|
apiResponse.subscriptionStatus === SubscriptionStatus.PastDue;
|
||||||
|
|
||||||
const { isLoading, isFetching: isFetchingBillingData } = useQuery(
|
const { isLoading, isFetching: isFetchingBillingData } = useQuery(
|
||||||
[REACT_QUERY_KEY.GET_BILLING_USAGE, user?.id],
|
[REACT_QUERY_KEY.GET_BILLING_USAGE, user?.userId],
|
||||||
{
|
{
|
||||||
queryFn: () => getUsage(activeLicense?.key || ''),
|
queryFn: () => getUsage(activeLicense?.key || ''),
|
||||||
onError: handleError,
|
onError: handleError,
|
||||||
@@ -214,29 +213,25 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const activeValidLicense =
|
const activeValidLicense =
|
||||||
licenses?.licenses?.find((license) => license.isCurrent === true) || null;
|
licensesData?.payload?.licenses?.find(
|
||||||
|
(license) => license.isCurrent === true,
|
||||||
|
) || null;
|
||||||
|
|
||||||
setActiveLicense(activeValidLicense);
|
setActiveLicense(activeValidLicense);
|
||||||
|
|
||||||
if (!isFetchingLicenses && licenses?.onTrial && !licensesFetchError) {
|
if (!isFetching && licensesData?.payload?.onTrial && !licenseError) {
|
||||||
const remainingDays = getRemainingDays(licenses?.trialEnd);
|
const remainingDays = getRemainingDays(licensesData?.payload?.trialEnd);
|
||||||
|
|
||||||
setIsFreeTrial(true);
|
setIsFreeTrial(true);
|
||||||
setBillAmount(0);
|
setBillAmount(0);
|
||||||
setDaysRemaining(remainingDays > 0 ? remainingDays : 0);
|
setDaysRemaining(remainingDays > 0 ? remainingDays : 0);
|
||||||
setHeaderText(
|
setHeaderText(
|
||||||
`You are in free trial period. Your free trial will end on ${getFormattedDate(
|
`You are in free trial period. Your free trial will end on ${getFormattedDate(
|
||||||
licenses?.trialEnd,
|
licensesData?.payload?.trialEnd,
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
}, [isFetching, licensesData?.payload, licenseError]);
|
||||||
licenses?.licenses,
|
|
||||||
licenses?.onTrial,
|
|
||||||
licenses?.trialEnd,
|
|
||||||
isFetchingLicenses,
|
|
||||||
licensesFetchError,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const columns: ColumnsType<DataType> = [
|
const columns: ColumnsType<DataType> = [
|
||||||
{
|
{
|
||||||
@@ -318,7 +313,7 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleBilling = useCallback(async () => {
|
const handleBilling = useCallback(async () => {
|
||||||
if (isFreeTrial && !licenses?.trialConvertedToSubscription) {
|
if (isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription) {
|
||||||
logEvent('Billing : Upgrade Plan', {
|
logEvent('Billing : Upgrade Plan', {
|
||||||
user: pick(user, ['email', 'userId', 'name']),
|
user: pick(user, ['email', 'userId', 'name']),
|
||||||
org,
|
org,
|
||||||
@@ -345,7 +340,7 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
}, [
|
}, [
|
||||||
activeLicense?.key,
|
activeLicense?.key,
|
||||||
isFreeTrial,
|
isFreeTrial,
|
||||||
licenses?.trialConvertedToSubscription,
|
licensesData?.payload?.trialConvertedToSubscription,
|
||||||
manageCreditCard,
|
manageCreditCard,
|
||||||
updateCreditCard,
|
updateCreditCard,
|
||||||
]);
|
]);
|
||||||
@@ -457,21 +452,22 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
onClick={handleBilling}
|
onClick={handleBilling}
|
||||||
>
|
>
|
||||||
{isFreeTrial && !licenses?.trialConvertedToSubscription
|
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription
|
||||||
? t('upgrade_plan')
|
? t('upgrade_plan')
|
||||||
: t('manage_billing')}
|
: t('manage_billing')}
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{licenses?.onTrial && licenses?.trialConvertedToSubscription && (
|
{licensesData?.payload?.onTrial &&
|
||||||
<Typography.Text
|
licensesData?.payload?.trialConvertedToSubscription && (
|
||||||
ellipsis
|
<Typography.Text
|
||||||
style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }}
|
ellipsis
|
||||||
>
|
style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }}
|
||||||
{t('card_details_recieved_and_billing_info')}
|
>
|
||||||
</Typography.Text>
|
{t('card_details_recieved_and_billing_info')}
|
||||||
)}
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isLoading && !isFetchingBillingData ? (
|
{!isLoading && !isFetchingBillingData ? (
|
||||||
headerText && (
|
headerText && (
|
||||||
@@ -514,7 +510,7 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
{(isLoading || isFetchingBillingData) && renderTableSkeleton()}
|
{(isLoading || isFetchingBillingData) && renderTableSkeleton()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isFreeTrial && !licenses?.trialConvertedToSubscription && (
|
{isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription && (
|
||||||
<div className="upgrade-plan-benefits">
|
<div className="upgrade-plan-benefits">
|
||||||
<Row
|
<Row
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Row, Tag, Typography } from 'antd';
|
|||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import useFeatureFlags from 'hooks/useFeatureFlag';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
@@ -13,11 +13,9 @@ import { OptionType } from './types';
|
|||||||
|
|
||||||
function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||||
const { t } = useTranslation(['alerts']);
|
const { t } = useTranslation(['alerts']);
|
||||||
const { featureFlags } = useAppContext();
|
|
||||||
|
|
||||||
const isAnomalyDetectionEnabled =
|
const isAnomalyDetectionEnabled =
|
||||||
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
|
useFeatureFlags(FeatureKeys.ANOMALY_DETECTION)?.active || false;
|
||||||
?.active || false;
|
|
||||||
|
|
||||||
const optionList = getOptionList(t, isAnomalyDetectionEnabled);
|
const optionList = getOptionList(t, isAnomalyDetectionEnabled);
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import {
|
import {
|
||||||
CSSProperties,
|
CSSProperties,
|
||||||
Dispatch,
|
Dispatch,
|
||||||
@@ -57,12 +56,15 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { ViewProps } from 'types/api/saveViews/types';
|
import { ViewProps } from 'types/api/saveViews/types';
|
||||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
import { USER_ROLES } from 'types/roles';
|
import { USER_ROLES } from 'types/roles';
|
||||||
|
|
||||||
import { PreservedViewsTypes } from './constants';
|
import { PreservedViewsTypes } from './constants';
|
||||||
@@ -131,7 +133,7 @@ function ExplorerOptions({
|
|||||||
setIsSaveModalOpen(false);
|
setIsSaveModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { user } = useAppContext();
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
const handleConditionalQueryModification = useCallback((): string => {
|
const handleConditionalQueryModification = useCallback((): string => {
|
||||||
if (
|
if (
|
||||||
@@ -470,7 +472,7 @@ function ExplorerOptions({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isEditDeleteSupported = allowedRoles.includes(user.role as string);
|
const isEditDeleteSupported = allowedRoles.includes(role as string);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
isRecentlyUsedSavedViewSelected,
|
isRecentlyUsedSavedViewSelected,
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import {
|
|||||||
SlackChannel,
|
SlackChannel,
|
||||||
WebhookChannel,
|
WebhookChannel,
|
||||||
} from 'container/CreateAlertChannels/config';
|
} from 'container/CreateAlertChannels/config';
|
||||||
|
import useFeatureFlags from 'hooks/useFeatureFlag';
|
||||||
|
import { isFeatureKeys } from 'hooks/useFeatureFlag/utils';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { Dispatch, ReactElement, SetStateAction } from 'react';
|
import { Dispatch, ReactElement, SetStateAction } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { isFeatureKeys } from 'utils/app';
|
|
||||||
|
|
||||||
import EmailSettings from './Settings/Email';
|
import EmailSettings from './Settings/Email';
|
||||||
import MsTeamsSettings from './Settings/MsTeams';
|
import MsTeamsSettings from './Settings/MsTeams';
|
||||||
@@ -39,22 +39,16 @@ function FormAlertChannels({
|
|||||||
editing = false,
|
editing = false,
|
||||||
}: FormAlertChannelsProps): JSX.Element {
|
}: FormAlertChannelsProps): JSX.Element {
|
||||||
const { t } = useTranslation('channels');
|
const { t } = useTranslation('channels');
|
||||||
const { featureFlags } = useAppContext();
|
const isUserOnEEPlan = useFeatureFlags(FeatureKeys.ENTERPRISE_PLAN);
|
||||||
const isUserOnEEPlan =
|
|
||||||
featureFlags?.find((flag) => flag.name === FeatureKeys.ENTERPRISE_PLAN)
|
|
||||||
?.active || false;
|
|
||||||
|
|
||||||
const feature = `ALERT_CHANNEL_${type.toUpperCase()}`;
|
const feature = `ALERT_CHANNEL_${type.toUpperCase()}`;
|
||||||
|
|
||||||
const featureKey = isFeatureKeys(feature)
|
const hasFeature = useFeatureFlags(
|
||||||
? feature
|
isFeatureKeys(feature) ? feature : FeatureKeys.ALERT_CHANNEL_SLACK,
|
||||||
: FeatureKeys.ALERT_CHANNEL_SLACK;
|
|
||||||
const hasFeature = featureFlags?.find((flag) => flag.name === featureKey);
|
|
||||||
|
|
||||||
const isOssFeature = featureFlags?.find(
|
|
||||||
(flag) => flag.name === FeatureKeys.OSS,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isOssFeature = useFeatureFlags(FeatureKeys.OSS);
|
||||||
|
|
||||||
const renderSettings = (): ReactElement | null => {
|
const renderSettings = (): ReactElement | null => {
|
||||||
if (
|
if (
|
||||||
// for ee plan
|
// for ee plan
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
|||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
import { AlertDef, Labels } from 'types/api/alerts/def';
|
import { AlertDef, Labels } from 'types/api/alerts/def';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
|
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
@@ -43,10 +45,10 @@ function BasicInfo({
|
|||||||
const { t } = useTranslation('alerts');
|
const { t } = useTranslation('alerts');
|
||||||
|
|
||||||
const channels = useFetch(getChannels);
|
const channels = useFetch(getChannels);
|
||||||
const { user } = useAppContext();
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
const [addNewChannelPermission] = useComponentPermission(
|
const [addNewChannelPermission] = useComponentPermission(
|
||||||
['add_new_channel'],
|
['add_new_channel'],
|
||||||
user.role,
|
role,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import { Select, Spin } from 'antd';
|
|||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import { State } from 'hooks/useFetch';
|
import { State } from 'hooks/useFetch';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import { PayloadProps } from 'types/api/channels/getAll';
|
import { PayloadProps } from 'types/api/channels/getAll';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
import { StyledCreateChannelOption, StyledSelect } from './styles';
|
import { StyledCreateChannelOption, StyledSelect } from './styles';
|
||||||
|
|
||||||
@@ -47,10 +49,10 @@ function ChannelSelect({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user } = useAppContext();
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
const [addNewChannelPermission] = useComponentPermission(
|
const [addNewChannelPermission] = useComponentPermission(
|
||||||
['add_new_channel'],
|
['add_new_channel'],
|
||||||
user.role,
|
role,
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderOptions = (): ReactNode[] => {
|
const renderOptions = (): ReactNode[] => {
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ import {
|
|||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useResizeObserver } from 'hooks/useDimensions';
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
|
import useFeatureFlags from 'hooks/useFeatureFlag';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import GetMinMax from 'lib/getMinMax';
|
import GetMinMax from 'lib/getMinMax';
|
||||||
import getTimeString from 'lib/getTimeString';
|
import getTimeString from 'lib/getTimeString';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
||||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useTimezone } from 'providers/Timezone';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -84,8 +84,6 @@ function ChartPreview({
|
|||||||
GlobalReducer
|
GlobalReducer
|
||||||
>((state) => state.globalTime);
|
>((state) => state.globalTime);
|
||||||
|
|
||||||
const { featureFlags } = useAppContext();
|
|
||||||
|
|
||||||
const handleBackNavigation = (): void => {
|
const handleBackNavigation = (): void => {
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
const startTime = searchParams.get(QueryParams.startTime);
|
const startTime = searchParams.get(QueryParams.startTime);
|
||||||
@@ -272,8 +270,7 @@ function ChartPreview({
|
|||||||
chartData && !queryResponse.isError && !queryResponse.isLoading;
|
chartData && !queryResponse.isError && !queryResponse.isLoading;
|
||||||
|
|
||||||
const isAnomalyDetectionEnabled =
|
const isAnomalyDetectionEnabled =
|
||||||
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
|
useFeatureFlags(FeatureKeys.ANOMALY_DETECTION)?.active || false;
|
||||||
?.active || false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="alert-chart-container" ref={graphRef}>
|
<div className="alert-chart-container" ref={graphRef}>
|
||||||
|
|||||||
@@ -14,9 +14,12 @@ 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';
|
||||||
@@ -35,9 +38,14 @@ function QuerySection({
|
|||||||
const { t } = useTranslation('alerts');
|
const { t } = useTranslation('alerts');
|
||||||
const [currentTab, setCurrentTab] = useState(queryCategory);
|
const [currentTab, setCurrentTab] = useState(queryCategory);
|
||||||
|
|
||||||
// TODO[vikrantgupta25] : check if this is still required ??
|
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||||
|
(state) => state.app,
|
||||||
|
);
|
||||||
|
|
||||||
const handleQueryCategoryChange = (queryType: string): void => {
|
const handleQueryCategoryChange = (queryType: string): void => {
|
||||||
setQueryCategory(queryType as EQueryType);
|
featureResponse.refetch().then(() => {
|
||||||
|
setQueryCategory(queryType as EQueryType);
|
||||||
|
});
|
||||||
setCurrentTab(queryType as EQueryType);
|
setCurrentTab(queryType as EQueryType);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import './FormAlertRules.styles.scss';
|
import './FormAlertRules.styles.scss';
|
||||||
|
|
||||||
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
||||||
import { Button, FormInstance, Modal, SelectProps, Typography } from 'antd';
|
import {
|
||||||
|
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';
|
||||||
@@ -16,6 +23,10 @@ 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';
|
||||||
@@ -24,7 +35,6 @@ 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';
|
||||||
@@ -86,7 +96,6 @@ 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,
|
||||||
@@ -467,9 +476,9 @@ function FormAlertRules({
|
|||||||
panelType,
|
panelType,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isAlertAvailable =
|
const isAlertAvailable = useIsFeatureDisabled(
|
||||||
!featureFlags?.find((flag) => flag.name === FeatureKeys.QUERY_BUILDER_ALERTS)
|
FeatureKeys.QUERY_BUILDER_ALERTS,
|
||||||
?.active || false;
|
);
|
||||||
|
|
||||||
const saveRule = useCallback(async () => {
|
const saveRule = useCallback(async () => {
|
||||||
if (!isFormValid()) {
|
if (!isFormValid()) {
|
||||||
@@ -757,8 +766,7 @@ function FormAlertRules({
|
|||||||
];
|
];
|
||||||
|
|
||||||
const isAnomalyDetectionEnabled =
|
const isAnomalyDetectionEnabled =
|
||||||
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
|
useFeatureFlag(FeatureKeys.ANOMALY_DETECTION)?.active || false;
|
||||||
?.active || false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -858,20 +866,22 @@ function FormAlertRules({
|
|||||||
{renderBasicInfo()}
|
{renderBasicInfo()}
|
||||||
</div>
|
</div>
|
||||||
<ButtonContainer>
|
<ButtonContainer>
|
||||||
<ActionButton
|
<Tooltip title={isAlertAvailableToSave ? MESSAGE.ALERT : ''}>
|
||||||
loading={loading || false}
|
<ActionButton
|
||||||
type="primary"
|
loading={loading || false}
|
||||||
onClick={onSaveHandler}
|
type="primary"
|
||||||
icon={<SaveOutlined />}
|
onClick={onSaveHandler}
|
||||||
disabled={
|
icon={<SaveOutlined />}
|
||||||
isAlertNameMissing ||
|
disabled={
|
||||||
isAlertAvailableToSave ||
|
isAlertNameMissing ||
|
||||||
!isChannelConfigurationValid ||
|
isAlertAvailableToSave ||
|
||||||
queryStatus === 'error'
|
!isChannelConfigurationValid ||
|
||||||
}
|
queryStatus === 'error'
|
||||||
>
|
}
|
||||||
{isNewRule ? t('button_createrule') : t('button_savechanges')}
|
>
|
||||||
</ActionButton>
|
{isNewRule ? t('button_createrule') : t('button_savechanges')}
|
||||||
|
</ActionButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
loading={loading || false}
|
loading={loading || false}
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ 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,
|
||||||
@@ -23,6 +24,7 @@ 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';
|
||||||
@@ -66,11 +68,11 @@ function GeneralSettings({
|
|||||||
logsTtlValuesPayload,
|
logsTtlValuesPayload,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { user } = useAppContext();
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
const [setRetentionPermission] = useComponentPermission(
|
const [setRetentionPermission] = useComponentPermission(
|
||||||
['set_retention_period'],
|
['set_retention_period'],
|
||||||
user.role,
|
role,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ 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';
|
||||||
|
|
||||||
@@ -17,7 +19,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 } = useAppContext();
|
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
getRetentionPeriodMetricsApiResponse,
|
getRetentionPeriodMetricsApiResponse,
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import { Button, Typography } from 'antd';
|
|||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import SettingsDrawer from 'container/NewDashboard/DashboardDescription/SettingsDrawer';
|
import SettingsDrawer from 'container/NewDashboard/DashboardDescription/SettingsDrawer';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
import { ROLES, USER_ROLES } from 'types/roles';
|
import { ROLES, USER_ROLES } from 'types/roles';
|
||||||
import { ComponentTypes } from 'utils/permission';
|
import { ComponentTypes } from 'utils/permission';
|
||||||
|
|
||||||
@@ -19,7 +21,7 @@ export default function DashboardEmptyState(): JSX.Element {
|
|||||||
handleToggleDashboardSlider,
|
handleToggleDashboardSlider,
|
||||||
} = useDashboard();
|
} = useDashboard();
|
||||||
|
|
||||||
const { user } = useAppContext();
|
const { user, role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
let permissions: ComponentTypes[] = ['add_panel'];
|
let permissions: ComponentTypes[] = ['add_panel'];
|
||||||
|
|
||||||
if (isDashboardLocked) {
|
if (isDashboardLocked) {
|
||||||
@@ -29,7 +31,7 @@ export default function DashboardEmptyState(): JSX.Element {
|
|||||||
const userRole: ROLES | null =
|
const userRole: ROLES | null =
|
||||||
selectedDashboard?.created_by === user?.email
|
selectedDashboard?.created_by === user?.email
|
||||||
? (USER_ROLES.AUTHOR as ROLES)
|
? (USER_ROLES.AUTHOR as ROLES)
|
||||||
: user.role;
|
: role;
|
||||||
|
|
||||||
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,11 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import WidgetHeader from '../WidgetHeader';
|
import WidgetHeader from '../WidgetHeader';
|
||||||
@@ -74,6 +77,10 @@ function WidgetGraphComponent({
|
|||||||
|
|
||||||
const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard();
|
const { setLayouts, selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||||
|
|
||||||
|
const featureResponse = useSelector<AppState, AppReducer['featureResponse']>(
|
||||||
|
(state) => state.app.featureResponse,
|
||||||
|
);
|
||||||
|
|
||||||
const onToggleModal = useCallback(
|
const onToggleModal = useCallback(
|
||||||
(func: Dispatch<SetStateAction<boolean>>) => {
|
(func: Dispatch<SetStateAction<boolean>>) => {
|
||||||
func((value) => !value);
|
func((value) => !value);
|
||||||
@@ -110,6 +117,7 @@ function WidgetGraphComponent({
|
|||||||
setSelectedDashboard(updatedDashboard.payload);
|
setSelectedDashboard(updatedDashboard.payload);
|
||||||
}
|
}
|
||||||
setDeleteModal(false);
|
setDeleteModal(false);
|
||||||
|
featureResponse.refetch();
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
|
|||||||
@@ -26,16 +26,17 @@ import {
|
|||||||
LockKeyhole,
|
LockKeyhole,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { sortLayout } from 'providers/Dashboard/util';
|
import { sortLayout } from 'providers/Dashboard/util';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { FullScreen, FullScreenHandle } from 'react-full-screen';
|
import { FullScreen, FullScreenHandle } from 'react-full-screen';
|
||||||
import { ItemCallback, Layout } from 'react-grid-layout';
|
import { ItemCallback, Layout } from 'react-grid-layout';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { UpdateTimeInterval } from 'store/actions';
|
import { UpdateTimeInterval } from 'store/actions';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
import { ROLES, USER_ROLES } from 'types/roles';
|
import { ROLES, USER_ROLES } from 'types/roles';
|
||||||
import { ComponentTypes } from 'utils/permission';
|
import { ComponentTypes } from 'utils/permission';
|
||||||
|
|
||||||
@@ -68,7 +69,9 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
const { widgets, variables } = data || {};
|
const { widgets, variables } = data || {};
|
||||||
|
|
||||||
const { user } = useAppContext();
|
const { featureResponse, role, user } = useSelector<AppState, AppReducer>(
|
||||||
|
(state) => state.app,
|
||||||
|
);
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
@@ -108,7 +111,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
|||||||
const userRole: ROLES | null =
|
const userRole: ROLES | null =
|
||||||
selectedDashboard?.created_by === user?.email
|
selectedDashboard?.created_by === user?.email
|
||||||
? (USER_ROLES.AUTHOR as ROLES)
|
? (USER_ROLES.AUTHOR as ROLES)
|
||||||
: user.role;
|
: role;
|
||||||
|
|
||||||
const [saveLayoutPermission, addPanelPermission] = useComponentPermission(
|
const [saveLayoutPermission, addPanelPermission] = useComponentPermission(
|
||||||
permissions,
|
permissions,
|
||||||
@@ -117,7 +120,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
const [deleteWidget, editWidget] = useComponentPermission(
|
const [deleteWidget, editWidget] = useComponentPermission(
|
||||||
['delete_widget', 'edit_widget'],
|
['delete_widget', 'edit_widget'],
|
||||||
user.role,
|
role,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -157,6 +160,8 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
|||||||
setSelectedDashboard(updatedDashboard.payload);
|
setSelectedDashboard(updatedDashboard.payload);
|
||||||
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
featureResponse.refetch();
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: () => {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
@@ -253,6 +258,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
|||||||
form.setFieldValue('title', '');
|
form.setFieldValue('title', '');
|
||||||
setIsSettingsModalOpen(false);
|
setIsSettingsModalOpen(false);
|
||||||
setCurrentSelectRowId(null);
|
setCurrentSelectRowId(null);
|
||||||
|
featureResponse.refetch();
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||||
onError: () => {
|
onError: () => {
|
||||||
@@ -415,6 +421,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
|||||||
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
setCurrentSelectRowId(null);
|
setCurrentSelectRowId(null);
|
||||||
|
featureResponse.refetch();
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||||
onError: () => {
|
onError: () => {
|
||||||
|
|||||||
@@ -24,12 +24,14 @@ import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
|||||||
import { isEmpty } from 'lodash-es';
|
import { isEmpty } from 'lodash-es';
|
||||||
import { CircleX, X } from 'lucide-react';
|
import { CircleX, X } from 'lucide-react';
|
||||||
import { unparse } from 'papaparse';
|
import { unparse } from 'papaparse';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { ReactNode, useCallback, useMemo, useState } from 'react';
|
import { ReactNode, useCallback, useMemo, useState } from 'react';
|
||||||
import { UseQueryResult } from 'react-query';
|
import { UseQueryResult } 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 { Widgets } from 'types/api/dashboard/getAll';
|
import { Widgets } from 'types/api/dashboard/getAll';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
import { errorTooltipPosition, WARNING_MESSAGE } from './config';
|
import { errorTooltipPosition, WARNING_MESSAGE } from './config';
|
||||||
import { MENUITEM_KEYS_VS_LABELS, MenuItemKeys } from './contants';
|
import { MENUITEM_KEYS_VS_LABELS, MenuItemKeys } from './contants';
|
||||||
@@ -128,11 +130,11 @@ function WidgetHeader({
|
|||||||
},
|
},
|
||||||
[keyMethodMapping],
|
[keyMethodMapping],
|
||||||
);
|
);
|
||||||
const { user } = useAppContext();
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
const [deleteWidget, editWidget] = useComponentPermission(
|
const [deleteWidget, editWidget] = useComponentPermission(
|
||||||
['delete_widget', 'edit_widget'],
|
['delete_widget', 'edit_widget'],
|
||||||
user.role,
|
role,
|
||||||
);
|
);
|
||||||
|
|
||||||
const actions = useMemo(
|
const actions = useMemo(
|
||||||
|
|||||||
78
frontend/src/container/Header/CurrentOrganization/index.tsx
Normal file
78
frontend/src/container/Header/CurrentOrganization/index.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { PlusSquareOutlined } from '@ant-design/icons';
|
||||||
|
import { Avatar, Typography } from 'antd';
|
||||||
|
import { INVITE_MEMBERS_HASH } from 'constants/app';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
|
import {
|
||||||
|
InviteMembersContainer,
|
||||||
|
OrganizationContainer,
|
||||||
|
OrganizationWrapper,
|
||||||
|
} from '../styles';
|
||||||
|
|
||||||
|
function CurrentOrganization({
|
||||||
|
onToggle,
|
||||||
|
}: CurrentOrganizationProps): JSX.Element {
|
||||||
|
const { org, role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
const [currentOrgSettings, inviteMembers] = useComponentPermission(
|
||||||
|
['current_org_settings', 'invite_members'],
|
||||||
|
role,
|
||||||
|
);
|
||||||
|
|
||||||
|
// just to make sure role and org are present in the reducer
|
||||||
|
if (!org || !role) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgName = org[0].name;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography>CURRENT ORGANIZATION</Typography>
|
||||||
|
|
||||||
|
<OrganizationContainer>
|
||||||
|
<OrganizationWrapper>
|
||||||
|
<Avatar shape="square" size="large">
|
||||||
|
{orgName}
|
||||||
|
</Avatar>
|
||||||
|
<Typography>{orgName}</Typography>
|
||||||
|
</OrganizationWrapper>
|
||||||
|
|
||||||
|
{currentOrgSettings && (
|
||||||
|
<Typography.Link
|
||||||
|
onClick={(): void => {
|
||||||
|
onToggle();
|
||||||
|
history.push(ROUTES.ORG_SETTINGS);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</Typography.Link>
|
||||||
|
)}
|
||||||
|
</OrganizationContainer>
|
||||||
|
|
||||||
|
{inviteMembers && (
|
||||||
|
<InviteMembersContainer>
|
||||||
|
<PlusSquareOutlined />
|
||||||
|
<Typography.Link
|
||||||
|
onClick={(): void => {
|
||||||
|
onToggle();
|
||||||
|
history.push(`${ROUTES.ORG_SETTINGS}${INVITE_MEMBERS_HASH}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Invite Members
|
||||||
|
</Typography.Link>
|
||||||
|
</InviteMembersContainer>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CurrentOrganizationProps {
|
||||||
|
onToggle: VoidFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CurrentOrganization;
|
||||||
25
frontend/src/container/Header/Header.styles.scss
Normal file
25
frontend/src/container/Header/Header.styles.scss
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
.trial-expiry-banner {
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #f25733;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upgrade-link {
|
||||||
|
padding: 0px;
|
||||||
|
padding-right: 4px;
|
||||||
|
display: inline !important;
|
||||||
|
color: white;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: white;
|
||||||
|
text-decoration-thickness: 2px;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: white;
|
||||||
|
text-decoration-thickness: 2px;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
frontend/src/container/Header/ManageLicense/index.tsx
Normal file
49
frontend/src/container/Header/ManageLicense/index.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Spin, Typography } from 'antd';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense';
|
||||||
|
import history from 'lib/history';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FreePlanIcon,
|
||||||
|
ManageLicenseContainer,
|
||||||
|
ManageLicenseWrapper,
|
||||||
|
} from './styles';
|
||||||
|
|
||||||
|
function ManageLicense({ onToggle }: ManageLicenseProps): JSX.Element {
|
||||||
|
const { data, isLoading } = useLicense();
|
||||||
|
|
||||||
|
const onManageLicense = (): void => {
|
||||||
|
onToggle();
|
||||||
|
history.push(ROUTES.LIST_LICENSES);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading || data?.payload === undefined) {
|
||||||
|
return <Spin />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEnterprise = data?.payload?.licenses?.some(
|
||||||
|
(license) =>
|
||||||
|
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.ENTERPRISE_PLAN,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Typography>SIGNOZ STATUS</Typography>
|
||||||
|
|
||||||
|
<ManageLicenseContainer>
|
||||||
|
<ManageLicenseWrapper>
|
||||||
|
<FreePlanIcon />
|
||||||
|
<Typography>{!isEnterprise ? 'Free Plan' : 'Enterprise Plan'} </Typography>
|
||||||
|
</ManageLicenseWrapper>
|
||||||
|
|
||||||
|
<Typography.Link onClick={onManageLicense}>Manage Licenses</Typography.Link>
|
||||||
|
</ManageLicenseContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ManageLicenseProps {
|
||||||
|
onToggle: VoidFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManageLicense;
|
||||||
19
frontend/src/container/Header/ManageLicense/styles.ts
Normal file
19
frontend/src/container/Header/ManageLicense/styles.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { MinusSquareOutlined } from '@ant-design/icons';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const ManageLicenseContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ManageLicenseWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FreePlanIcon = styled(MinusSquareOutlined)`
|
||||||
|
background-color: hsla(0, 0%, 100%, 0.3);
|
||||||
|
`;
|
||||||
50
frontend/src/container/Header/SignedIn/index.tsx
Normal file
50
frontend/src/container/Header/SignedIn/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Avatar, Typography } from 'antd';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
|
import { AvatarContainer, ManageAccountLink, Wrapper } from '../styles';
|
||||||
|
|
||||||
|
function SignedIn({ onToggle }: SignedInProps): JSX.Element {
|
||||||
|
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
|
const onManageAccountClick = useCallback(() => {
|
||||||
|
onToggle();
|
||||||
|
history.push(ROUTES.MY_SETTINGS);
|
||||||
|
}, [onToggle]);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, email } = user;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Typography>SIGNED IN AS</Typography>
|
||||||
|
<Wrapper>
|
||||||
|
<AvatarContainer>
|
||||||
|
<Avatar shape="circle" size="large">
|
||||||
|
{name[0]}
|
||||||
|
</Avatar>
|
||||||
|
<div>
|
||||||
|
<Typography>{name}</Typography>
|
||||||
|
<Typography>{email}</Typography>
|
||||||
|
</div>
|
||||||
|
</AvatarContainer>
|
||||||
|
<ManageAccountLink onClick={onManageAccountClick}>
|
||||||
|
Manage Account
|
||||||
|
</ManageAccountLink>
|
||||||
|
</Wrapper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SignedInProps {
|
||||||
|
onToggle: VoidFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SignedIn;
|
||||||
215
frontend/src/container/Header/index.tsx
Normal file
215
frontend/src/container/Header/index.tsx
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
|
import './Header.styles.scss';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CaretDownFilled,
|
||||||
|
CaretUpFilled,
|
||||||
|
LogoutOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Button, Divider, MenuProps, Space, Typography } from 'antd';
|
||||||
|
import { Logout } from 'api/utils';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import Config from 'container/ConfigDropdown';
|
||||||
|
import { useIsDarkMode, useThemeMode } from 'hooks/useDarkMode';
|
||||||
|
import useLicense, { LICENSE_PLAN_STATUS } from 'hooks/useLicense';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import {
|
||||||
|
Dispatch,
|
||||||
|
KeyboardEvent,
|
||||||
|
SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { License } from 'types/api/licenses/def';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
||||||
|
|
||||||
|
import CurrentOrganization from './CurrentOrganization';
|
||||||
|
import ManageLicense from './ManageLicense';
|
||||||
|
import SignedIn from './SignedIn';
|
||||||
|
import {
|
||||||
|
AvatarWrapper,
|
||||||
|
Container,
|
||||||
|
Header,
|
||||||
|
IconContainer,
|
||||||
|
LogoutContainer,
|
||||||
|
NavLinkWrapper,
|
||||||
|
ToggleButton,
|
||||||
|
UserDropdown,
|
||||||
|
} from './styles';
|
||||||
|
|
||||||
|
function HeaderContainer(): JSX.Element {
|
||||||
|
const { user, role, currentVersion } = useSelector<AppState, AppReducer>(
|
||||||
|
(state) => state.app,
|
||||||
|
);
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
const { toggleTheme } = useThemeMode();
|
||||||
|
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);
|
||||||
|
const [homeRoute, setHomeRoute] = useState<string>(ROUTES.APPLICATION);
|
||||||
|
|
||||||
|
const [isUserDropDownOpen, setIsUserDropDownOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const onToggleHandler = useCallback(
|
||||||
|
(functionToExecute: Dispatch<SetStateAction<boolean>>) => (): void => {
|
||||||
|
functionToExecute((state) => !state);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onLogoutKeyDown = useCallback((e: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (e.key === 'Enter' || e.key === 'Space') {
|
||||||
|
Logout();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const menu: MenuProps = useMemo(
|
||||||
|
() => ({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'main-menu',
|
||||||
|
label: (
|
||||||
|
<div>
|
||||||
|
<SignedIn onToggle={onToggleHandler(setIsUserDropDownOpen)} />
|
||||||
|
<Divider />
|
||||||
|
<CurrentOrganization onToggle={onToggleHandler(setIsUserDropDownOpen)} />
|
||||||
|
<Divider />
|
||||||
|
<ManageLicense onToggle={onToggleHandler(setIsUserDropDownOpen)} />
|
||||||
|
<Divider />
|
||||||
|
<LogoutContainer>
|
||||||
|
<LogoutOutlined />
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={onLogoutKeyDown}
|
||||||
|
role="button"
|
||||||
|
onClick={Logout}
|
||||||
|
>
|
||||||
|
<Typography.Link>Logout</Typography.Link>
|
||||||
|
</div>
|
||||||
|
</LogoutContainer>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[onToggleHandler, onLogoutKeyDown],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClickSignozCloud = (): void => {
|
||||||
|
window.open(
|
||||||
|
'https://signoz.io/oss-to-cloud/?utm_source=product_navbar&utm_medium=frontend&utm_campaign=oss_users',
|
||||||
|
'_blank',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: licenseData, isFetching, status: licenseStatus } = useLicense();
|
||||||
|
|
||||||
|
const licensesStatus: string =
|
||||||
|
licenseData?.payload?.licenses?.find((e: License) => e.isCurrent)?.status ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
const isLicenseActive =
|
||||||
|
licensesStatus?.toLocaleLowerCase() ===
|
||||||
|
LICENSE_PLAN_STATUS.VALID.toLocaleLowerCase();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!isFetching &&
|
||||||
|
licenseData?.payload?.onTrial &&
|
||||||
|
!licenseData?.payload?.trialConvertedToSubscription &&
|
||||||
|
getRemainingDays(licenseData?.payload.trialEnd) < 7
|
||||||
|
) {
|
||||||
|
setShowTrialExpiryBanner(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFetching && licenseData?.payload?.workSpaceBlock) {
|
||||||
|
setHomeRoute(ROUTES.WORKSPACE_LOCKED);
|
||||||
|
}
|
||||||
|
}, [licenseData, isFetching]);
|
||||||
|
|
||||||
|
const handleUpgrade = (): void => {
|
||||||
|
if (role === 'ADMIN') {
|
||||||
|
history.push(ROUTES.BILLING);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showTrialExpiryBanner && (
|
||||||
|
<div className="trial-expiry-banner">
|
||||||
|
You are in free trial period. Your free trial will end on{' '}
|
||||||
|
<span>
|
||||||
|
{getFormattedDate(licenseData?.payload?.trialEnd || Date.now())}.
|
||||||
|
</span>
|
||||||
|
{role === 'ADMIN' ? (
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
Please{' '}
|
||||||
|
<a className="upgrade-link" onClick={handleUpgrade}>
|
||||||
|
upgrade
|
||||||
|
</a>
|
||||||
|
to continue using SigNoz features.
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'Please contact your administrator for upgrading to a paid plan.'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Header>
|
||||||
|
<Container>
|
||||||
|
<NavLink to={homeRoute}>
|
||||||
|
<NavLinkWrapper>
|
||||||
|
<img src={`/signoz.svg?currentVersion=${currentVersion}`} alt="SigNoz" />
|
||||||
|
<Typography.Title
|
||||||
|
style={{ margin: 0, color: 'rgb(219, 219, 219)' }}
|
||||||
|
level={4}
|
||||||
|
>
|
||||||
|
SigNoz
|
||||||
|
</Typography.Title>
|
||||||
|
</NavLinkWrapper>
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
|
<Space size="middle" align="center">
|
||||||
|
{licenseStatus === 'success' && !isLicenseActive && (
|
||||||
|
<Button onClick={onClickSignozCloud} type="primary">
|
||||||
|
Try Signoz Cloud
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Config frontendId="tooltip" />
|
||||||
|
|
||||||
|
<ToggleButton
|
||||||
|
checked={isDarkMode}
|
||||||
|
onChange={toggleTheme}
|
||||||
|
defaultChecked={isDarkMode}
|
||||||
|
checkedChildren="🌜"
|
||||||
|
unCheckedChildren="🌞"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UserDropdown
|
||||||
|
onOpenChange={onToggleHandler(setIsUserDropDownOpen)}
|
||||||
|
trigger={['click']}
|
||||||
|
menu={menu}
|
||||||
|
open={isUserDropDownOpen}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
<AvatarWrapper shape="circle">{user?.name[0]}</AvatarWrapper>
|
||||||
|
<IconContainer>
|
||||||
|
{!isUserDropDownOpen ? <CaretDownFilled /> : <CaretUpFilled />}
|
||||||
|
</IconContainer>
|
||||||
|
</Space>
|
||||||
|
</UserDropdown>
|
||||||
|
</Space>
|
||||||
|
</Container>
|
||||||
|
</Header>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HeaderContainer;
|
||||||
90
frontend/src/container/Header/styles.ts
Normal file
90
frontend/src/container/Header/styles.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { Avatar, Dropdown, Layout, Switch, Typography } from 'antd';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const Header = styled(Layout.Header)`
|
||||||
|
background: #1f1f1f !important;
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 4rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const AvatarContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Wrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ManageAccountLink = styled(Typography.Link)`
|
||||||
|
width: 6rem;
|
||||||
|
text-align: end;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const OrganizationWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const OrganizationContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const InviteMembersContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LogoutContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export interface DarkModeProps {
|
||||||
|
checked?: boolean;
|
||||||
|
defaultChecked?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToggleButton = styled(Switch)<DarkModeProps>`
|
||||||
|
&&& {
|
||||||
|
background: ${({ checked }): string => (checked === false ? 'grey' : '')};
|
||||||
|
}
|
||||||
|
.ant-switch-inner {
|
||||||
|
font-size: 1rem !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const IconContainer = styled.div`
|
||||||
|
color: white;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const NavLinkWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
gap: 0.5rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const AvatarWrapper = styled(Avatar)`
|
||||||
|
background-color: rgba(255, 255, 255, 0.25);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UserDropdown = styled(Dropdown)`
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
@@ -3,16 +3,18 @@ import './IngestionSettings.styles.scss';
|
|||||||
import { Skeleton, Table, Typography } from 'antd';
|
import { Skeleton, Table, Typography } from 'antd';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
import getIngestionData from 'api/settings/getIngestionData';
|
import getIngestionData from 'api/settings/getIngestionData';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import { IngestionDataType } from 'types/api/settings/ingestion';
|
import { IngestionDataType } from 'types/api/settings/ingestion';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
export default function IngestionSettings(): JSX.Element {
|
export default function IngestionSettings(): JSX.Element {
|
||||||
const { user } = useAppContext();
|
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
const { data: ingestionData, isFetching } = useQuery({
|
const { data: ingestionData, isFetching } = useQuery({
|
||||||
queryFn: getIngestionData,
|
queryFn: getIngestionData,
|
||||||
queryKey: ['getIngestionData', user?.id],
|
queryKey: ['getIngestionData', user?.userId],
|
||||||
});
|
});
|
||||||
|
|
||||||
const columns: ColumnsType<IngestionDataType> = [
|
const columns: ColumnsType<IngestionDataType> = [
|
||||||
|
|||||||
@@ -51,18 +51,20 @@ import {
|
|||||||
Trash2,
|
Trash2,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useTimezone } from 'providers/Timezone';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
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 { ErrorResponse } from 'types/api';
|
import { ErrorResponse } from 'types/api';
|
||||||
import { LimitProps } from 'types/api/ingestionKeys/limits/types';
|
import { LimitProps } from 'types/api/ingestionKeys/limits/types';
|
||||||
import {
|
import {
|
||||||
IngestionKeyProps,
|
IngestionKeyProps,
|
||||||
PaginationProps,
|
PaginationProps,
|
||||||
} from 'types/api/ingestionKeys/types';
|
} from 'types/api/ingestionKeys/types';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
import { USER_ROLES } from 'types/roles';
|
import { USER_ROLES } from 'types/roles';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
@@ -102,7 +104,7 @@ export const API_KEY_EXPIRY_OPTIONS: ExpiryOption[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function MultiIngestionSettings(): JSX.Element {
|
function MultiIngestionSettings(): JSX.Element {
|
||||||
const { user } = useAppContext();
|
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [isDeleteLimitModalOpen, setIsDeleteLimitModalOpen] = useState(false);
|
const [isDeleteLimitModalOpen, setIsDeleteLimitModalOpen] = useState(false);
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ import apply from 'api/licenses/apply';
|
|||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { QueryObserverResult, RefetchOptions } from 'react-query';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { PayloadProps } from 'types/api/licenses/getAll';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
|
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +24,9 @@ function ApplyLicenseForm({
|
|||||||
const { t } = useTranslation(['licenses']);
|
const { t } = useTranslation(['licenses']);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [form] = Form.useForm<FormValues>();
|
const [form] = Form.useForm<FormValues>();
|
||||||
|
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||||
|
(state) => state.app,
|
||||||
|
);
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
const key = Form.useWatch('key', form);
|
const key = Form.useWatch('key', form);
|
||||||
@@ -41,7 +50,7 @@ function ApplyLicenseForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
if (response.statusCode === 200) {
|
||||||
await Promise.all([licenseRefetch()]);
|
await Promise.all([featureResponse?.refetch(), licenseRefetch()]);
|
||||||
|
|
||||||
notifications.success({
|
notifications.success({
|
||||||
message: 'Success',
|
message: 'Success',
|
||||||
@@ -93,7 +102,11 @@ function ApplyLicenseForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ApplyLicenseFormProps {
|
interface ApplyLicenseFormProps {
|
||||||
licenseRefetch: () => void;
|
licenseRefetch: (
|
||||||
|
options?: RefetchOptions,
|
||||||
|
) => Promise<
|
||||||
|
QueryObserverResult<SuccessResponse<PayloadProps> | ErrorResponse, unknown>
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Tabs } from 'antd';
|
import { Tabs, Typography } from 'antd';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import useLicense from 'hooks/useLicense';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import ApplyLicenseForm from './ApplyLicenseForm';
|
import ApplyLicenseForm from './ApplyLicenseForm';
|
||||||
@@ -8,20 +8,24 @@ import ListLicenses from './ListLicenses';
|
|||||||
|
|
||||||
function Licenses(): JSX.Element {
|
function Licenses(): JSX.Element {
|
||||||
const { t, ready: translationsReady } = useTranslation(['licenses']);
|
const { t, ready: translationsReady } = useTranslation(['licenses']);
|
||||||
const { licenses, licensesRefetch } = useAppContext();
|
const { data, isError, isLoading, refetch } = useLicense();
|
||||||
|
|
||||||
if (!translationsReady) {
|
if (isError || data?.error) {
|
||||||
|
return <Typography>{data?.error}</Typography>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading || data?.payload === undefined || !translationsReady) {
|
||||||
return <Spinner tip={t('loading_licenses')} height="90vh" />;
|
return <Spinner tip={t('loading_licenses')} height="90vh" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allValidLicense =
|
const allValidLicense =
|
||||||
licenses?.licenses?.filter((license) => license.isCurrent) || [];
|
data?.payload?.licenses?.filter((license) => license.isCurrent) || [];
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: t('tab_current_license'),
|
label: t('tab_current_license'),
|
||||||
key: 'licenses',
|
key: 'licenses',
|
||||||
children: <ApplyLicenseForm licenseRefetch={licensesRefetch} />,
|
children: <ApplyLicenseForm licenseRefetch={refetch} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('tab_license_history'),
|
label: t('tab_license_history'),
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ import { Button, Divider, Typography } from 'antd';
|
|||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
|
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 { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
import AlertInfoCard from './AlertInfoCard';
|
import AlertInfoCard from './AlertInfoCard';
|
||||||
import { ALERT_CARDS, ALERT_INFO_LINKS } from './alertLinks';
|
import { ALERT_CARDS, ALERT_INFO_LINKS } from './alertLinks';
|
||||||
@@ -28,18 +32,36 @@ const alertLogEvents = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function AlertsEmptyState(): JSX.Element {
|
export function AlertsEmptyState(): JSX.Element {
|
||||||
const { user } = useAppContext();
|
const { t } = useTranslation('common');
|
||||||
|
const { role, featureResponse } = useSelector<AppState, AppReducer>(
|
||||||
|
(state) => state.app,
|
||||||
|
);
|
||||||
const [addNewAlert] = useComponentPermission(
|
const [addNewAlert] = useComponentPermission(
|
||||||
['add_new_alert', 'action'],
|
['add_new_alert', 'action'],
|
||||||
user.role,
|
role,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { notifications: notificationsApi } = useNotifications();
|
||||||
|
|
||||||
|
const handleError = useCallback((): void => {
|
||||||
|
notificationsApi.error({
|
||||||
|
message: t('something_went_wrong'),
|
||||||
|
});
|
||||||
|
}, [notificationsApi, t]);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const onClickNewAlertHandler = useCallback(() => {
|
const onClickNewAlertHandler = useCallback(() => {
|
||||||
setLoading(false);
|
setLoading(true);
|
||||||
history.push(ROUTES.ALERTS_NEW);
|
featureResponse
|
||||||
}, []);
|
.refetch()
|
||||||
|
.then(() => {
|
||||||
|
setLoading(false);
|
||||||
|
history.push(ROUTES.ALERTS_NEW);
|
||||||
|
})
|
||||||
|
.catch(handleError)
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [featureResponse, handleError]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="alert-list-container">
|
<div className="alert-list-container">
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ import { NotificationInstance } from 'antd/es/notification/interface';
|
|||||||
import deleteAlerts from 'api/alerts/delete';
|
import deleteAlerts from 'api/alerts/delete';
|
||||||
import { State } from 'hooks/useFetch';
|
import { State } from 'hooks/useFetch';
|
||||||
import { Dispatch, SetStateAction, useState } from 'react';
|
import { Dispatch, SetStateAction, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import { PayloadProps as DeleteAlertPayloadProps } from 'types/api/alerts/delete';
|
import { PayloadProps as DeleteAlertPayloadProps } from 'types/api/alerts/delete';
|
||||||
import { GettableAlert } from 'types/api/alerts/get';
|
import { GettableAlert } from 'types/api/alerts/get';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
import { ColumnButton } from './styles';
|
import { ColumnButton } from './styles';
|
||||||
|
|
||||||
@@ -22,6 +25,10 @@ function DeleteAlert({
|
|||||||
payload: undefined,
|
payload: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||||
|
(state) => state.app,
|
||||||
|
);
|
||||||
|
|
||||||
const defaultErrorMessage = 'Something went wrong';
|
const defaultErrorMessage = 'Something went wrong';
|
||||||
|
|
||||||
const onDeleteHandler = async (id: number): Promise<void> => {
|
const onDeleteHandler = async (id: number): Promise<void> => {
|
||||||
@@ -72,7 +79,20 @@ function DeleteAlert({
|
|||||||
...state,
|
...state,
|
||||||
loading: true,
|
loading: true,
|
||||||
}));
|
}));
|
||||||
onDeleteHandler(id);
|
featureResponse
|
||||||
|
.refetch()
|
||||||
|
.then(() => {
|
||||||
|
onDeleteHandler(id);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setDeleteAlertState((state) => ({
|
||||||
|
...state,
|
||||||
|
loading: false,
|
||||||
|
}));
|
||||||
|
notifications.error({
|
||||||
|
message: defaultErrorMessage,
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -23,12 +23,14 @@ 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';
|
||||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||||
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 { UseQueryResult } from 'react-query';
|
import { UseQueryResult } 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 { GettableAlert } from 'types/api/alerts/get';
|
import { GettableAlert } from 'types/api/alerts/get';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
import DeleteAlert from './DeleteAlert';
|
import DeleteAlert from './DeleteAlert';
|
||||||
import { Button, ColumnButton, SearchContainer } from './styles';
|
import { Button, ColumnButton, SearchContainer } from './styles';
|
||||||
@@ -40,11 +42,12 @@ const { Search } = Input;
|
|||||||
|
|
||||||
function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
const { user } = useAppContext();
|
const { role, featureResponse } = useSelector<AppState, AppReducer>(
|
||||||
// TODO[vikrantgupta25]: check with sagar on cleanup
|
(state) => state.app,
|
||||||
|
);
|
||||||
const [addNewAlert, action] = useComponentPermission(
|
const [addNewAlert, action] = useComponentPermission(
|
||||||
['add_new_alert', 'action'],
|
['add_new_alert', 'action'],
|
||||||
user.role,
|
role,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [editLoader, setEditLoader] = useState<boolean>(false);
|
const [editLoader, setEditLoader] = useState<boolean>(false);
|
||||||
@@ -102,23 +105,38 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
logEvent('Alert: New alert button clicked', {
|
logEvent('Alert: New alert button clicked', {
|
||||||
number: allAlertRules?.length,
|
number: allAlertRules?.length,
|
||||||
});
|
});
|
||||||
history.push(ROUTES.ALERTS_NEW);
|
featureResponse
|
||||||
|
.refetch()
|
||||||
|
.then(() => {
|
||||||
|
history.push(ROUTES.ALERTS_NEW);
|
||||||
|
})
|
||||||
|
.catch(handleError);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, [featureResponse, handleError]);
|
||||||
|
|
||||||
const onEditHandler = (record: GettableAlert) => (): void => {
|
const onEditHandler = (record: GettableAlert) => (): void => {
|
||||||
const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery);
|
setEditLoader(true);
|
||||||
params.set(
|
featureResponse
|
||||||
QueryParams.compositeQuery,
|
.refetch()
|
||||||
encodeURIComponent(JSON.stringify(compositeQuery)),
|
.then(() => {
|
||||||
);
|
const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery);
|
||||||
|
params.set(
|
||||||
|
QueryParams.compositeQuery,
|
||||||
|
encodeURIComponent(JSON.stringify(compositeQuery)),
|
||||||
|
);
|
||||||
|
|
||||||
params.set(QueryParams.panelTypes, record.condition.compositeQuery.panelType);
|
params.set(
|
||||||
|
QueryParams.panelTypes,
|
||||||
|
record.condition.compositeQuery.panelType,
|
||||||
|
);
|
||||||
|
|
||||||
params.set(QueryParams.ruleId, record.id.toString());
|
params.set(QueryParams.ruleId, record.id.toString());
|
||||||
|
|
||||||
setEditLoader(false);
|
setEditLoader(false);
|
||||||
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
|
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
|
||||||
|
})
|
||||||
|
.catch(handleError)
|
||||||
|
.finally(() => setEditLoader(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCloneHandler = (
|
const onCloneHandler = (
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ import {
|
|||||||
// #TODO: lucide will be removing brand icons like Github in future, in that case we can use simple icons
|
// #TODO: lucide will be removing brand icons like Github in future, in that case we can use simple icons
|
||||||
// see more: https://github.com/lucide-icons/lucide/issues/94
|
// see more: https://github.com/lucide-icons/lucide/issues/94
|
||||||
import { handleContactSupport } from 'pages/Integrations/utils';
|
import { handleContactSupport } from 'pages/Integrations/utils';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { useTimezone } from 'providers/Timezone';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import {
|
import {
|
||||||
@@ -73,14 +72,17 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { Layout } from 'react-grid-layout';
|
import { Layout } from 'react-grid-layout';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import { generatePath, Link } from 'react-router-dom';
|
import { generatePath, Link } from 'react-router-dom';
|
||||||
import { useCopyToClipboard } from 'react-use';
|
import { useCopyToClipboard } from 'react-use';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import {
|
import {
|
||||||
Dashboard,
|
Dashboard,
|
||||||
IDashboardVariable,
|
IDashboardVariable,
|
||||||
WidgetRow,
|
WidgetRow,
|
||||||
Widgets,
|
Widgets,
|
||||||
} from 'types/api/dashboard/getAll';
|
} from 'types/api/dashboard/getAll';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
import { isCloudUser } from 'utils/app';
|
import { isCloudUser } from 'utils/app';
|
||||||
|
|
||||||
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
|
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
|
||||||
@@ -103,7 +105,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
refetch: refetchDashboardList,
|
refetch: refetchDashboardList,
|
||||||
} = useGetAllDashboard();
|
} = useGetAllDashboard();
|
||||||
|
|
||||||
const { user } = useAppContext();
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
listSortOrder: sortOrder,
|
listSortOrder: sortOrder,
|
||||||
@@ -115,7 +117,7 @@ function DashboardsList(): JSX.Element {
|
|||||||
);
|
);
|
||||||
const [action, createNewDashboard] = useComponentPermission(
|
const [action, createNewDashboard] = useComponentPermission(
|
||||||
['action', 'create_new_dashboards'],
|
['action', 'create_new_dashboards'],
|
||||||
user.role,
|
role,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import logEvent from 'api/common/logEvent';
|
|||||||
import createDashboard from 'api/dashboard/create';
|
import createDashboard from 'api/dashboard/create';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { MESSAGE } from 'hooks/useFeatureFlag';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
@@ -39,6 +40,7 @@ function ImportJSON({
|
|||||||
const [isCreateDashboardError, setIsCreateDashboardError] = useState<boolean>(
|
const [isCreateDashboardError, setIsCreateDashboardError] = useState<boolean>(
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
const [isFeatureAlert, setIsFeatureAlert] = useState<boolean>(false);
|
||||||
|
|
||||||
const [dashboardCreating, setDashboardCreating] = useState<boolean>(false);
|
const [dashboardCreating, setDashboardCreating] = useState<boolean>(false);
|
||||||
|
|
||||||
@@ -106,6 +108,15 @@ function ImportJSON({
|
|||||||
dashboardId: response.payload?.uuid,
|
dashboardId: response.payload?.uuid,
|
||||||
dashboardName: response.payload?.data?.title,
|
dashboardName: response.payload?.data?.title,
|
||||||
});
|
});
|
||||||
|
} else if (response.error === 'feature usage exceeded') {
|
||||||
|
setIsFeatureAlert(true);
|
||||||
|
notifications.error({
|
||||||
|
message:
|
||||||
|
response.error ||
|
||||||
|
t('something_went_wrong', {
|
||||||
|
ns: 'common',
|
||||||
|
}),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setIsCreateDashboardError(true);
|
setIsCreateDashboardError(true);
|
||||||
notifications.error({
|
notifications.error({
|
||||||
@@ -119,6 +130,8 @@ function ImportJSON({
|
|||||||
setDashboardCreating(false);
|
setDashboardCreating(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setDashboardCreating(false);
|
setDashboardCreating(false);
|
||||||
|
setIsFeatureAlert(false);
|
||||||
|
|
||||||
setIsCreateDashboardError(true);
|
setIsCreateDashboardError(true);
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: error instanceof Error ? error.message : t('error_loading_json'),
|
message: error instanceof Error ? error.message : t('error_loading_json'),
|
||||||
@@ -136,6 +149,7 @@ function ImportJSON({
|
|||||||
const onCancelHandler = (): void => {
|
const onCancelHandler = (): void => {
|
||||||
setIsUploadJSONError(false);
|
setIsUploadJSONError(false);
|
||||||
setIsCreateDashboardError(false);
|
setIsCreateDashboardError(false);
|
||||||
|
setIsFeatureAlert(false);
|
||||||
onModalHandler();
|
onModalHandler();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -225,6 +239,12 @@ function ImportJSON({
|
|||||||
>
|
>
|
||||||
{t('import_and_next')} <MoveRight size={14} />
|
{t('import_and_next')} <MoveRight size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{isFeatureAlert && (
|
||||||
|
<Typography.Text type="danger">
|
||||||
|
{MESSAGE.CREATE_DASHBOARD}
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import ROUTES from 'constants/routes';
|
|||||||
import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard';
|
import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard';
|
||||||
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 } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQueryClient } from 'react-query';
|
import { useQueryClient } from 'react-query';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
import { USER_ROLES } from 'types/roles';
|
import { USER_ROLES } from 'types/roles';
|
||||||
|
|
||||||
import { Data } from '../DashboardsList';
|
import { Data } from '../DashboardsList';
|
||||||
@@ -32,7 +34,7 @@ export function DeleteButton({
|
|||||||
routeToListPage,
|
routeToListPage,
|
||||||
}: DeleteButtonProps): JSX.Element {
|
}: DeleteButtonProps): JSX.Element {
|
||||||
const [modal, contextHolder] = Modal.useModal();
|
const [modal, contextHolder] = Modal.useModal();
|
||||||
const { user } = useAppContext();
|
const { role, user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
const isAuthor = user?.email === createdBy;
|
const isAuthor = user?.email === createdBy;
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -90,7 +92,7 @@ export function DeleteButton({
|
|||||||
|
|
||||||
const getDeleteTooltipContent = (): string => {
|
const getDeleteTooltipContent = (): string => {
|
||||||
if (isLocked) {
|
if (isLocked) {
|
||||||
if (user.role === USER_ROLES.ADMIN || isAuthor) {
|
if (role === USER_ROLES.ADMIN || isAuthor) {
|
||||||
return t('dashboard:locked_dashboard_delete_tooltip_admin_author');
|
return t('dashboard:locked_dashboard_delete_tooltip_admin_author');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +115,7 @@ export function DeleteButton({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="delete-btn"
|
className="delete-btn"
|
||||||
disabled={isLocked || (user.role === USER_ROLES.VIEWER && !isAuthor)}
|
disabled={isLocked || (role === USER_ROLES.VIEWER && !isAuthor)}
|
||||||
>
|
>
|
||||||
<DeleteOutlined /> Delete dashboard
|
<DeleteOutlined /> Delete dashboard
|
||||||
</TableLinkText>
|
</TableLinkText>
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import { Button, Form, Input, Space, Tooltip, Typography } from 'antd';
|
import { Button, Form, Input, Space, Tooltip, Typography } from 'antd';
|
||||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
|
||||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
|
||||||
import getUserVersion from 'api/user/getVersion';
|
import getUserVersion from 'api/user/getVersion';
|
||||||
import loginApi from 'api/user/login';
|
import loginApi from 'api/user/login';
|
||||||
import loginPrecheckApi from 'api/user/loginPrecheck';
|
import loginPrecheckApi from 'api/user/loginPrecheck';
|
||||||
import afterLogin from 'AppRoutes/utils';
|
import afterLogin from 'AppRoutes/utils';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
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 { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import { PayloadProps as PrecheckResultType } from 'types/api/user/loginPrecheck';
|
import { PayloadProps as PrecheckResultType } from 'types/api/user/loginPrecheck';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
import { FormContainer, FormWrapper, Label, ParentContainer } from './styles';
|
import { FormContainer, FormWrapper, Label, ParentContainer } from './styles';
|
||||||
|
|
||||||
@@ -38,7 +37,7 @@ function Login({
|
|||||||
}: LoginProps): JSX.Element {
|
}: LoginProps): JSX.Element {
|
||||||
const { t } = useTranslation(['login']);
|
const { t } = useTranslation(['login']);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const { user } = useAppContext();
|
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
const [precheckResult, setPrecheckResult] = useState<PrecheckResultType>({
|
const [precheckResult, setPrecheckResult] = useState<PrecheckResultType>({
|
||||||
sso: false,
|
sso: false,
|
||||||
@@ -86,15 +85,7 @@ function Login({
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await afterLogin(userId, jwt, refreshjwt);
|
await afterLogin(userId, jwt, refreshjwt);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
const fromPathname = getLocalStorageApi(
|
history.push(ROUTES.APPLICATION);
|
||||||
LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT,
|
|
||||||
);
|
|
||||||
if (fromPathname) {
|
|
||||||
history.push(fromPathname);
|
|
||||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, '');
|
|
||||||
} else {
|
|
||||||
history.push(ROUTES.APPLICATION);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
processJwt();
|
processJwt();
|
||||||
@@ -167,11 +158,20 @@ function Login({
|
|||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
if (response.statusCode === 200) {
|
if (response.statusCode === 200) {
|
||||||
afterLogin(
|
await afterLogin(
|
||||||
response.payload.userId,
|
response.payload.userId,
|
||||||
response.payload.accessJwt,
|
response.payload.accessJwt,
|
||||||
response.payload.refreshJwt,
|
response.payload.refreshJwt,
|
||||||
);
|
);
|
||||||
|
if (history?.location?.state) {
|
||||||
|
const historyState = history?.location?.state as any;
|
||||||
|
|
||||||
|
if (historyState?.from) {
|
||||||
|
history.push(historyState?.from);
|
||||||
|
} else {
|
||||||
|
history.push(ROUTES.APPLICATION);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: response.error || t('unexpected_error'),
|
message: response.error || t('unexpected_error'),
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ export default function TableRow({
|
|||||||
return (
|
return (
|
||||||
<TableCellStyled
|
<TableCellStyled
|
||||||
$isDragColumn={false}
|
$isDragColumn={false}
|
||||||
$isLogIndicator={column.key === 'state-indicator'}
|
|
||||||
$isDarkMode={isDarkMode}
|
$isDarkMode={isDarkMode}
|
||||||
key={column.key}
|
key={column.key}
|
||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TableHeaderCellStyled
|
<TableHeaderCellStyled
|
||||||
$isLogIndicator={column.key === 'state-indicator'}
|
$isTimestamp={column.key === 'timestamp'}
|
||||||
$isDarkMode={isDarkMode}
|
$isDarkMode={isDarkMode}
|
||||||
$isDragColumn={isDragColumn}
|
$isDragColumn={isDragColumn}
|
||||||
key={column.key}
|
key={column.key}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { getActiveLogBackground } from 'utils/logs';
|
|||||||
interface TableHeaderCellStyledProps {
|
interface TableHeaderCellStyledProps {
|
||||||
$isDragColumn: boolean;
|
$isDragColumn: boolean;
|
||||||
$isDarkMode: boolean;
|
$isDarkMode: boolean;
|
||||||
$isLogIndicator?: boolean;
|
$isTimestamp?: boolean;
|
||||||
fontSize?: FontSize;
|
fontSize?: FontSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,12 +28,11 @@ export const TableCellStyled = styled.td<TableHeaderCellStyledProps>`
|
|||||||
background-color: ${(props): string =>
|
background-color: ${(props): string =>
|
||||||
props.$isDarkMode ? 'inherit' : themeColors.whiteCream};
|
props.$isDarkMode ? 'inherit' : themeColors.whiteCream};
|
||||||
|
|
||||||
${({ $isLogIndicator }): string =>
|
|
||||||
$isLogIndicator ? 'padding: 0 0 0 8px;' : ''}
|
|
||||||
color: ${(props): string =>
|
color: ${(props): string =>
|
||||||
props.$isDarkMode ? themeColors.white : themeColors.bckgGrey};
|
props.$isDarkMode ? themeColors.white : themeColors.bckgGrey};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// handle the light theme here
|
||||||
export const TableRowStyled = styled.tr<{
|
export const TableRowStyled = styled.tr<{
|
||||||
$isActiveLog: boolean;
|
$isActiveLog: boolean;
|
||||||
$isDarkMode: boolean;
|
$isDarkMode: boolean;
|
||||||
@@ -86,7 +85,7 @@ export const TableHeaderCellStyled = styled.th<TableHeaderCellStyledProps>`
|
|||||||
: fontSize === FontSize.LARGE
|
: fontSize === FontSize.LARGE
|
||||||
? `font-size:14px; line-height:24px; padding: 0.5rem;`
|
? `font-size:14px; line-height:24px; padding: 0.5rem;`
|
||||||
: ``};
|
: ``};
|
||||||
${({ $isLogIndicator }): string => ($isLogIndicator ? 'padding: 0px; ' : '')}
|
${({ $isTimestamp }): string => ($isTimestamp ? 'padding-left: 24px;' : '')}
|
||||||
color: ${(props): string =>
|
color: ${(props): string =>
|
||||||
props.$isDarkMode ? 'var(--bg-vanilla-100, #fff)' : themeColors.bckgGrey};
|
props.$isDarkMode ? 'var(--bg-vanilla-100, #fff)' : themeColors.bckgGrey};
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
import ROUTES from 'constants/routes';
|
import { render, RenderResult } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||||
import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
|
import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
|
||||||
import { server } from 'mocks-server/server';
|
import { server } from 'mocks-server/server';
|
||||||
import { rest } from 'msw';
|
import { rest } from 'msw';
|
||||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||||
|
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||||
|
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||||
|
import TimezoneProvider from 'providers/Timezone';
|
||||||
|
import { I18nextProvider } from 'react-i18next';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||||
import { fireEvent, render, RenderResult } from 'tests/test-utils';
|
import i18n from 'ReactI18';
|
||||||
|
import store from 'store';
|
||||||
|
|
||||||
import LogsExplorerViews from '..';
|
import LogsExplorerViews from '..';
|
||||||
import { logsQueryRangeSuccessNewFormatResponse } from './mock';
|
import { logsQueryRangeSuccessNewFormatResponse } from './mock';
|
||||||
|
|
||||||
const queryRangeURL = 'http://localhost/api/v3/query_range';
|
const logExplorerRoute = '/logs/logs-explorer';
|
||||||
|
|
||||||
jest.mock('react-router-dom', () => ({
|
const queryRangeURL = 'http://localhost/api/v3/query_range';
|
||||||
...jest.requireActual('react-router-dom'),
|
|
||||||
useLocation: (): { pathname: string } => ({
|
|
||||||
pathname: `${ROUTES.LOGS_EXPLORER}`,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const lodsQueryServerRequest = (): void =>
|
const lodsQueryServerRequest = (): void =>
|
||||||
server.use(
|
server.use(
|
||||||
@@ -84,17 +87,29 @@ beforeEach(() => {
|
|||||||
|
|
||||||
const renderer = (): RenderResult =>
|
const renderer = (): RenderResult =>
|
||||||
render(
|
render(
|
||||||
<VirtuosoMockContext.Provider
|
<MemoryRouter initialEntries={[logExplorerRoute]}>
|
||||||
value={{ viewportHeight: 300, itemHeight: 100 }}
|
<Provider store={store}>
|
||||||
>
|
<I18nextProvider i18n={i18n}>
|
||||||
<LogsExplorerViews
|
<MockQueryClientProvider>
|
||||||
selectedView={SELECTED_VIEWS.SEARCH}
|
<QueryBuilderProvider>
|
||||||
showFrequencyChart
|
<TimezoneProvider>
|
||||||
setIsLoadingQueries={(): void => {}}
|
<VirtuosoMockContext.Provider
|
||||||
listQueryKeyRef={{ current: {} }}
|
value={{ viewportHeight: 300, itemHeight: 100 }}
|
||||||
chartQueryKeyRef={{ current: {} }}
|
>
|
||||||
/>
|
<LogsExplorerViews
|
||||||
</VirtuosoMockContext.Provider>,
|
selectedView={SELECTED_VIEWS.SEARCH}
|
||||||
|
showFrequencyChart
|
||||||
|
setIsLoadingQueries={(): void => {}}
|
||||||
|
listQueryKeyRef={{ current: {} }}
|
||||||
|
chartQueryKeyRef={{ current: {} }}
|
||||||
|
/>
|
||||||
|
</VirtuosoMockContext.Provider>
|
||||||
|
</TimezoneProvider>
|
||||||
|
</QueryBuilderProvider>
|
||||||
|
</MockQueryClientProvider>
|
||||||
|
</I18nextProvider>
|
||||||
|
</Provider>
|
||||||
|
</MemoryRouter>,
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('LogsExplorerViews -', () => {
|
describe('LogsExplorerViews -', () => {
|
||||||
@@ -103,7 +118,7 @@ describe('LogsExplorerViews -', () => {
|
|||||||
const { queryByText, queryByTestId } = renderer();
|
const { queryByText, queryByTestId } = renderer();
|
||||||
|
|
||||||
expect(queryByTestId('periscope-btn')).toBeInTheDocument();
|
expect(queryByTestId('periscope-btn')).toBeInTheDocument();
|
||||||
fireEvent.click(queryByTestId('periscope-btn') as HTMLElement);
|
await userEvent.click(queryByTestId('periscope-btn') as HTMLElement);
|
||||||
|
|
||||||
expect(document.querySelector('.menu-container')).toBeInTheDocument();
|
expect(document.querySelector('.menu-container')).toBeInTheDocument();
|
||||||
|
|
||||||
@@ -112,7 +127,7 @@ describe('LogsExplorerViews -', () => {
|
|||||||
|
|
||||||
// switch to table view
|
// switch to table view
|
||||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||||
fireEvent.click(queryByTestId('table-view') as HTMLElement);
|
await userEvent.click(queryByTestId('table-view') as HTMLElement);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
queryByText(
|
queryByText(
|
||||||
@@ -131,7 +146,7 @@ describe('LogsExplorerViews -', () => {
|
|||||||
const { queryByText, queryByTestId } = renderer();
|
const { queryByText, queryByTestId } = renderer();
|
||||||
|
|
||||||
// switch to table view
|
// switch to table view
|
||||||
fireEvent.click(queryByTestId('table-view') as HTMLElement);
|
await userEvent.click(queryByTestId('table-view') as HTMLElement);
|
||||||
expect(queryByText('pending_data_placeholder')).toBeInTheDocument();
|
expect(queryByText('pending_data_placeholder')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -150,7 +165,7 @@ describe('LogsExplorerViews -', () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
|
||||||
// switch to table view
|
// switch to table view
|
||||||
fireEvent.click(queryByTestId('table-view') as HTMLElement);
|
await userEvent.click(queryByTestId('table-view') as HTMLElement);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
queryByText('Something went wrong. Please try again or contact support.'),
|
queryByText('Something went wrong. Please try again or contact support.'),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
|||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { routeConfig } from 'container/SideNav/config';
|
import { routeConfig } from 'container/SideNav/config';
|
||||||
import { getQueryString } from 'container/SideNav/helper';
|
import { getQueryString } from 'container/SideNav/helper';
|
||||||
|
import useFeatureFlag from 'hooks/useFeatureFlag';
|
||||||
import useResourceAttribute from 'hooks/useResourceAttribute';
|
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||||
import {
|
import {
|
||||||
convertRawQueriesToTraceSelectedTags,
|
convertRawQueriesToTraceSelectedTags,
|
||||||
@@ -18,7 +19,6 @@ import getStep from 'lib/getStep';
|
|||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||||
import { defaultTo } from 'lodash-es';
|
import { defaultTo } from 'lodash-es';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
@@ -68,10 +68,8 @@ function Application(): JSX.Element {
|
|||||||
const { queries } = useResourceAttribute();
|
const { queries } = useResourceAttribute();
|
||||||
const urlQuery = useUrlQuery();
|
const urlQuery = useUrlQuery();
|
||||||
|
|
||||||
const { featureFlags } = useAppContext();
|
const isSpanMetricEnabled = useFeatureFlag(FeatureKeys.USE_SPAN_METRICS)
|
||||||
const isSpanMetricEnabled =
|
?.active;
|
||||||
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_SPAN_METRICS)
|
|
||||||
?.active || false;
|
|
||||||
|
|
||||||
const handleSetTimeStamp = useCallback((selectTime: number) => {
|
const handleSetTimeStamp = useCallback((selectTime: number) => {
|
||||||
setSelectedTimeStamp(selectTime);
|
setSelectedTimeStamp(selectTime);
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import {
|
|||||||
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
||||||
import { latency } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
|
import { latency } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
|
||||||
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
|
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
|
||||||
|
import useFeatureFlag from 'hooks/useFeatureFlag';
|
||||||
import useResourceAttribute from 'hooks/useResourceAttribute';
|
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||||
import { resourceAttributesToTagFilterItems } from 'hooks/useResourceAttribute/utils';
|
import { resourceAttributesToTagFilterItems } from 'hooks/useResourceAttribute/utils';
|
||||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
@@ -40,10 +40,8 @@ function ServiceOverview({
|
|||||||
const { servicename: encodedServiceName } = useParams<IServiceName>();
|
const { servicename: encodedServiceName } = useParams<IServiceName>();
|
||||||
const servicename = decodeURIComponent(encodedServiceName);
|
const servicename = decodeURIComponent(encodedServiceName);
|
||||||
|
|
||||||
const { featureFlags } = useAppContext();
|
const isSpanMetricEnable = useFeatureFlag(FeatureKeys.USE_SPAN_METRICS)
|
||||||
const isSpanMetricEnable =
|
?.active;
|
||||||
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_SPAN_METRICS)
|
|
||||||
?.active || false;
|
|
||||||
|
|
||||||
const { queries } = useResourceAttribute();
|
const { queries } = useResourceAttribute();
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import changeMyPassword from 'api/user/changeMyPassword';
|
|||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { Save } from 'lucide-react';
|
import { Save } from 'lucide-react';
|
||||||
import { isPasswordNotValidMessage, isPasswordValid } from 'pages/SignUp/utils';
|
import { isPasswordNotValidMessage, isPasswordValid } from 'pages/SignUp/utils';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, 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';
|
||||||
|
|
||||||
import { Password } from '../styles';
|
import { Password } from '../styles';
|
||||||
|
|
||||||
@@ -13,7 +15,7 @@ function PasswordContainer(): JSX.Element {
|
|||||||
const [currentPassword, setCurrentPassword] = useState<string>('');
|
const [currentPassword, setCurrentPassword] = useState<string>('');
|
||||||
const [updatePassword, setUpdatePassword] = useState<string>('');
|
const [updatePassword, setUpdatePassword] = useState<string>('');
|
||||||
const { t } = useTranslation(['routes', 'settings', 'common']);
|
const { t } = useTranslation(['routes', 'settings', 'common']);
|
||||||
const { user } = useAppContext();
|
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [isPasswordPolicyError, setIsPasswordPolicyError] = useState<boolean>(
|
const [isPasswordPolicyError, setIsPasswordPolicyError] = useState<boolean>(
|
||||||
false,
|
false,
|
||||||
@@ -48,7 +50,7 @@ function PasswordContainer(): JSX.Element {
|
|||||||
const { statusCode, error } = await changeMyPassword({
|
const { statusCode, error } = await changeMyPassword({
|
||||||
newPassword: updatePassword,
|
newPassword: updatePassword,
|
||||||
oldPassword: currentPassword,
|
oldPassword: currentPassword,
|
||||||
userId: user.id,
|
userId: user.userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (statusCode === 200) {
|
if (statusCode === 200) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import './TimezoneAdaptation.styles.scss';
|
|||||||
|
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Switch } from 'antd';
|
import { Switch } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
|
||||||
import { Delete } from 'lucide-react';
|
import { Delete } from 'lucide-react';
|
||||||
import { useTimezone } from 'providers/Timezone';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
@@ -28,18 +27,6 @@ function TimezoneAdaptation(): JSX.Element {
|
|||||||
|
|
||||||
const handleOverrideClear = (): void => {
|
const handleOverrideClear = (): void => {
|
||||||
updateTimezone(browserTimezone);
|
updateTimezone(browserTimezone);
|
||||||
logEvent('Settings: Timezone override cleared', {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSwitchChange = (): void => {
|
|
||||||
setIsAdaptationEnabled((prev) => {
|
|
||||||
const isEnabled = !prev;
|
|
||||||
logEvent(
|
|
||||||
`Settings: Timezone adaptation ${isEnabled ? 'enabled' : 'disabled'}`,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
return isEnabled;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -48,7 +35,7 @@ function TimezoneAdaptation(): JSX.Element {
|
|||||||
<h2 className="timezone-adaption__title">Adapt to my timezone</h2>
|
<h2 className="timezone-adaption__title">Adapt to my timezone</h2>
|
||||||
<Switch
|
<Switch
|
||||||
checked={isAdaptationEnabled}
|
checked={isAdaptationEnabled}
|
||||||
onChange={handleSwitchChange}
|
onChange={setIsAdaptationEnabled}
|
||||||
style={getSwitchStyles()}
|
style={getSwitchStyles()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,15 +5,23 @@ import { Button, Card, Flex, Input, Space, Typography } from 'antd';
|
|||||||
import editUser from 'api/user/editUser';
|
import editUser from 'api/user/editUser';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { PencilIcon } from 'lucide-react';
|
import { PencilIcon } 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 { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import AppActions from 'types/actions';
|
||||||
|
import { UPDATE_USER } from 'types/actions/app';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
import { NameInput } from '../styles';
|
import { NameInput } from '../styles';
|
||||||
|
|
||||||
function UserInfo(): JSX.Element {
|
function UserInfo(): JSX.Element {
|
||||||
const { user, org, updateUser } = useAppContext();
|
const { user, role, org, userFlags } = useSelector<AppState, AppReducer>(
|
||||||
|
(state) => state.app,
|
||||||
|
);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
|
|
||||||
const [changedName, setChangedName] = useState<string>(user?.name || '');
|
const [changedName, setChangedName] = useState<string>(user?.name || '');
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
@@ -29,7 +37,7 @@ function UserInfo(): JSX.Element {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const { statusCode } = await editUser({
|
const { statusCode } = await editUser({
|
||||||
name: changedName,
|
name: changedName,
|
||||||
userId: user.id,
|
userId: user.userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (statusCode === 200) {
|
if (statusCode === 200) {
|
||||||
@@ -38,9 +46,16 @@ function UserInfo(): JSX.Element {
|
|||||||
ns: 'common',
|
ns: 'common',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
updateUser({
|
dispatch({
|
||||||
...user,
|
type: UPDATE_USER,
|
||||||
name: changedName,
|
payload: {
|
||||||
|
...user,
|
||||||
|
name: changedName,
|
||||||
|
ROLE: role || 'ADMIN',
|
||||||
|
orgId: org[0].id,
|
||||||
|
orgName: org[0].name,
|
||||||
|
userFlags: userFlags || {},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
@@ -117,7 +132,7 @@ function UserInfo(): JSX.Element {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Input
|
<Input
|
||||||
className="userInfo-value"
|
className="userInfo-value"
|
||||||
value={user.role || ''}
|
value={role || ''}
|
||||||
disabled
|
disabled
|
||||||
data-testid="role-textbox"
|
data-testid="role-textbox"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -36,19 +36,21 @@ import {
|
|||||||
PenLine,
|
PenLine,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
import { sortLayout } from 'providers/Dashboard/util';
|
import { sortLayout } from 'providers/Dashboard/util';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { FullScreenHandle } from 'react-full-screen';
|
import { FullScreenHandle } from 'react-full-screen';
|
||||||
import { Layout } from 'react-grid-layout';
|
import { Layout } from 'react-grid-layout';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import { useCopyToClipboard } from 'react-use';
|
import { useCopyToClipboard } from 'react-use';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import {
|
import {
|
||||||
Dashboard,
|
Dashboard,
|
||||||
DashboardData,
|
DashboardData,
|
||||||
IDashboardVariable,
|
IDashboardVariable,
|
||||||
} from 'types/api/dashboard/getAll';
|
} from 'types/api/dashboard/getAll';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
import { ROLES, USER_ROLES } from 'types/roles';
|
import { ROLES, USER_ROLES } from 'types/roles';
|
||||||
import { ComponentTypes } from 'utils/permission';
|
import { ComponentTypes } from 'utils/permission';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
@@ -121,8 +123,10 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
|
|
||||||
const urlQuery = useUrlQuery();
|
const urlQuery = useUrlQuery();
|
||||||
|
|
||||||
const { user } = useAppContext();
|
const { featureResponse, user, role } = useSelector<AppState, AppReducer>(
|
||||||
const [editDashboard] = useComponentPermission(['edit_dashboard'], user.role);
|
(state) => state.app,
|
||||||
|
);
|
||||||
|
const [editDashboard] = useComponentPermission(['edit_dashboard'], role);
|
||||||
const [isDashboardSettingsOpen, setIsDashbordSettingsOpen] = useState<boolean>(
|
const [isDashboardSettingsOpen, setIsDashbordSettingsOpen] = useState<boolean>(
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
@@ -152,7 +156,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
const userRole: ROLES | null =
|
const userRole: ROLES | null =
|
||||||
selectedDashboard?.created_by === user?.email
|
selectedDashboard?.created_by === user?.email
|
||||||
? (USER_ROLES.AUTHOR as ROLES)
|
? (USER_ROLES.AUTHOR as ROLES)
|
||||||
: user.role;
|
: role;
|
||||||
|
|
||||||
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
const [addPanelPermission] = useComponentPermission(permissions, userRole);
|
||||||
|
|
||||||
@@ -289,6 +293,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
featureResponse.refetch();
|
||||||
setIsPanelNameModalOpen(false);
|
setIsPanelNameModalOpen(false);
|
||||||
setSectionName(DEFAULT_ROW_NAME);
|
setSectionName(DEFAULT_ROW_NAME);
|
||||||
},
|
},
|
||||||
@@ -358,7 +363,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
|||||||
content={
|
content={
|
||||||
<div className="menu-content">
|
<div className="menu-content">
|
||||||
<section className="section-1">
|
<section className="section-1">
|
||||||
{(isAuthor || user.role === USER_ROLES.ADMIN) && (
|
{(isAuthor || role === USER_ROLES.ADMIN) && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
selectedDashboard?.created_by === 'integration' &&
|
selectedDashboard?.created_by === 'integration' &&
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user