Compare commits

...

69 Commits

Author SHA1 Message Date
primus-bot[bot]
64a4606275 chore(release): bump to v0.67.0 (#6772)
#### Summary
 - Release SigNoz v0.67.0
 - Bump SigNoz OTel Collector to v0.111.22

 Created by [Primus-Bot](https://github.com/apps/primus-bot)
2025-01-08 15:30:00 +05:30
Shivanshu Raj Shrivastava
505757b971 [feat] Add overview APIs for Celery and Kafka in messaging queues integration (#6756)
* feat: added queue overview api for generic messaging system
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-01-08 06:50:11 +00:00
Vikrant Gupta
80740f646c fix(portal): use window.location.origin instead of the window.location.href (#6770)
* fix(portal): use window.location.origin instead of the whole URL

* fix(portal): use window.location.origin instead of the whole URL
2025-01-08 05:56:18 +00:00
Yunus M
e92d055c30 feat: enable billing and org-settings in workspace blocked state (#6761)
* feat: enable billing and org-settings in workspace blocked state

* feat: disable sidebar items in workspace blocked state
2025-01-08 05:46:41 +00:00
Yunus M
5c546e8efd feat: return is data is fetching (#6765) 2025-01-07 19:28:49 +05:30
Vikrant Gupta
ecd50f7232 feat(timeline): add new timeline v2 component (#6760)
* feat(timeline): base commit for timeline v2

* feat(timeline): svg rendering for timeline v2

* feat(timeline): dynamic scale based on screen size

* feat(timeline): cleanup code

* feat(timeline): make position functioning of timeline height
2025-01-07 14:09:06 +05:30
Prashant Shahi
15f85a645f ci(prereleaser): verify user membership for running workflow (#6734)
* ci(prereleaser): verify user membership for running workflow
* ci(prereleaser): use primus github script for verify user
* ci(github): update verify and trigger primus workflow
* ci(github): use main branch for primus.workflows

---------

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2025-01-06 15:51:49 +05:30
Vikrant Gupta
366ca3bb3e feat(generics): add a new performant resizable table (#6751)
* feat(generics): add a new performant resizble table
2025-01-05 14:02:23 +05:30
Yunus M
43b0cdbb6a feat: support updating subdomain for tenants (#6745)
* feat: support updating subdomain for tenants

* feat: enable subdomain update only for cloud accounts

* chore: code cleanup
2025-01-04 08:37:39 +00:00
Vikrant Gupta
4967696da8 feat: added new cache package for query service (#6733)
* feat: added new cache package for query service

* feat: handle type checking for inmemory

* feat: some copy corrections

* feat: added inmemory test cases

* chore: some renaming

* feat: added redis handling

* chore: add redis tests

* feat(cache): refactor the code

* feat(cache): refactor the code

* feat(cache): added defaults for redis config

* feat(cache): update makefile to run all tetss

* feat(cache): update tests and docs

* feat(cache): update tests and docs

* feat(cache): handle signoz web flag

* feat(cache): handle signoz web flag

* feat(cache): handle signoz web flag
2025-01-04 01:28:54 +05:30
aniketio-ctrl
c5938b6c10 fix: added backticks for tags containing dots while generating query … (#6727) 2025-01-03 05:22:55 +00:00
Srikanth Chekuri
9feee6ff46 chore: add option skip web (#6736) 2025-01-03 04:06:52 +00:00
Vikrant Gupta
d48cdbfc4a feat: enable the new where clause for all the log based queries (#6671)
* feat: enable the new where clause for logs dashboards
2025-01-02 22:55:30 +05:30
SagarRajput-7
dad72dd295 feat: updated the logic for variable update queue (#6586)
* feat: updated the logic for variable update queue

* feat: added API limiting to reduce unnecessary api call for dashboard variables (#6609)

* feat: added API limiting to reduce unneccesary api call for dashboard variables

* feat: fixed dropdown open triggering the api calls for single-select and misc

* feat: add jest test cases for new logic's utils, functions and processors - dashboardVariables (#6621)

* feat: added API limiting to reduce unneccesary api call for dashboard variables

* feat: fixed dropdown open triggering the api calls for single-select and misc

* feat: add jest test cases for new logic's utils, functions and processors - dashboardVariables

* feat: added test for checkAPIInvocation

* feat: refactor code

* feat: added more test on graph utilities

* feat: resolved comments and removed mount related handlings

* feat: fixed test cases and added multiple variable formats

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-01-02 15:45:01 +05:30
Vikrant Gupta
28d27bc5c1 fix(frontend): do not use redirects outside the react router (#6739)
* fix(frontend): use history.replace to something went wrong instead of redirects

* fix(frontend): update the something went wrong page to error boundary fallback
2025-01-02 13:30:31 +05:30
primus-bot[bot]
3e675bb9a5 chore(release): bump to v0.66.0 (#6737)
#### Summary
 - Release SigNoz v0.66.0
 - Bump SigNoz OTel Collector to v0.111.21

 Created by [Primus-Bot](https://github.com/apps/primus-bot)
2025-01-01 15:51:52 +05:30
Vikrant Gupta
05c9dd68dd fix(ci): fix jest coverage since github workflow (#6735) 2024-12-31 18:43:29 +05:30
Eng Zer Jun
03fb388cd1 chore(deps): update cespare/xxhash to v2 version (#6714) 2024-12-30 11:13:14 +00:00
Prashant Shahi
196b17dd1e ci(releaser): trigger charts releaser workflow on new release (#6732)
### Summary

- GH workflow to trigger releaser workflow in charts repository on new SigNoz release

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-12-30 04:49:09 +00:00
aniketio-ctrl
93e9d15004 fix: removed caching for all other panel type for expression except time series (#6720)
Co-authored-by: Aniket <aniket@Anikets-MacBook-Pro-2.local>
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-12-28 16:55:19 +00:00
primus-bot[bot]
f11161ddb8 chore(release): bump to v0.65.1 (#6731)
#### Summary
 - Release SigNoz v0.65.1
 - Bump SigNoz OTel Collector to v0.111.18

 Created by [Primus-Bot](https://github.com/apps/primus-bot)
2024-12-28 01:07:49 +05:30
Prashant Shahi
50db3cc39f feat(releaser): update releaser workflow based on new enhancements (#6729)
* feat(releaser): update releaser workflow based on new enhancements
* ci(releaser): set release type to minor if run by cron schedule
* feat(releaser): pass signoz project name for releaser

---------

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-12-27 19:09:24 +00:00
SagarRajput-7
6e27df9dcb chore: restructure Kafka codebase (#6611)
* chore: restructure Kafka codebase

* chore: fixed eslint errors
2024-12-27 16:12:53 +05:30
SagarRajput-7
7f6bad67d5 chore: changed droprate view options values (#6693) 2024-12-27 12:22:32 +05:30
SagarRajput-7
825d2dfcbb fix: added handling for new key - duration_nano in traces (#6658)
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-12-27 12:06:18 +05:30
primus-bot[bot]
9f6419c2f8 chore(release): bump to v0.65.0 (#6725)
#### Summary
 - Release SigNoz v0.65.0

 Created by [Primus-Bot](https://github.com/apps/primus-bot)
2024-12-26 14:28:44 +00:00
Prashant Shahi
421879cf7a ci(releaser): update branch reference to use latest main (#6724)
### Summary

- update branch reference to use the latest main
2024-12-26 14:17:09 +00:00
Shaheer Kochai
00abadd429 fix: update API key expiration display logic in MultiIngestionSettings component (#6717)
- display 'No Expiry' for invalid or zero date expiration dates.
2024-12-26 19:10:53 +05:30
Prashant Shahi
14096f8d53 ci(releaser): github workflow for signoz releases (#6719)
### Summary

- github workflow for automated SigNoz releases

Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-12-26 16:46:55 +05:30
Shaheer Kochai
d2aa1cf06e fix: fix the issue of saved view overriding query for logs and traces (#6678)
* fix: fix the issue of saved view overriding query for logs and traces for builder type query

* chore: refactored isDefaultQuery to use a function to extract relevant keys and use lodash's isEqual

* fix: add check for multiple queries in isDefaultQuery logic

* chore: moved extractRelevantKeys outside isDefaultQuery

* fix: fix the failing tests
2024-12-24 15:39:48 +00:00
Shaheer Kochai
838192cf5c fix(Traces Explorer): prevent duplicate API calls to query_range in traces explorer (#6677)
* fix(Traces Explorer): prevent duplicate API calls to query_range in traces explorer

* fix(QueryBuilder): fix the race condition causing duplicate triggering of initQueryBuilderData

* chore: address review comments

* fix: fix the failing tests

---------

Co-authored-by: Vikrant Gupta <vikrant.thomso@gmail.com>
2024-12-24 15:25:07 +00:00
Vikrant Gupta
5dfe245f2d chore: fix cross spawn vulnerability (#6709) 2024-12-24 19:07:20 +05:30
Vibhu Pandey
53b86e4b5c feat(ee): serve frontend pages with query-service (#6696)
Serve frontend pages with query-service
2024-12-23 16:44:48 +05:30
Vikrant Gupta
5d9a2571df feat: add k8s pod name as quick filter for logs (#6694) 2024-12-21 13:19:20 +05:30
Vikrant Gupta
bef6cc945a fix: do not try to route to org onboarding when workspace locked or suspended (#6692) 2024-12-20 22:28:53 +05:30
Vikrant Gupta
2c2e248c95 fix: add support route to the base route for private routes (#6688)
* fix: add support route to the base route for private routes

* fix: early return from useEffect if it's an old route

* fix: same old explorer routes in old mapping
2024-12-20 17:37:44 +05:30
Yunus M
2f62a9d36d feat: rename access tokens to api keys (#6687)
* feat: rename access tokens to api keys

* feat: update test cases

* feat: update delete token to delete key
2024-12-20 17:28:43 +05:30
Vikrant Gupta
04778b9641 fix: route permissions for billing and alerts (#6686) 2024-12-20 15:26:47 +05:30
Vikrant Gupta
26fe5e49e7 chore: revamp the frontend architecture (#6598)
* feat: setup the app context to fetch users,licenses and feature flags

* feat: added global event listeners for after_login event

* feat: remove redux from app state and private route

* feat: syncronize the approutes file

* feat: cleanup the private routes

* feat: handle login and logout

* feat: cleanup the app layout file

* feat: cleanup and syncronize side nav item

* fix: minor small re-render issue

* feat: parallel processing for sync calls for faster bootup of application

* feat: some refactoring for private routes

* fix: entire application too much re-rendering

* fix: remove redux

* feat: some more corrections

* feat: fix all the files except signup

* feat: add app provider to the test-utils

* feat: should fix a lot of tests

* chore: fix more tests

* chore: fix more tests

* feat: fix some tests and corrected the redux mock

* feat: delete snapshot

* fix: test cases

* fix: pipeline actions test cases

* fix: billing test cases

* feat: update the signup API to accept isAnonymous and hasOptedUpdates

* chore: cleanup the console logs

* fix: indefinite loading on manage licenses screen

* fix: better handling and route to something_went_wrong in case of qs down

* fix: signup for subsequent users

* chore: update test-utils

* fix: jerky behaviour on entering the home page

* feat: handle the retention for login context flow

* fix: do not let users workaround workspace blocked screen
2024-12-20 14:00:02 +05:30
Shaheer Kochai
accafbc3ec fix: fix the mismatch between time range picker placeholder and timerange popover values (#6675)
* fix: fix the mismatch between time range picker placeholder and timerange popover values

* fix: fix the stale value issue in range picker

---------

Co-authored-by: Vikrant Gupta <vikrant.thomso@gmail.com>
2024-12-20 13:35:40 +05:30
Vikrant Gupta
8e7c78e1b1 fix: detach the log indicator from timestamp column (#6681) 2024-12-20 13:25:52 +05:30
Shaheer Kochai
53ebd39f41 chore: add log events for timezone interactions in date/time picker and timezone adaptation (#6676)
* chore: add log events for timezone interactions in date/time picker and timezone adaptation

* refactor: modified the log event messages for timezone picker to follow the conventions

* chore: improve timezone picker event messages
2024-12-20 11:38:36 +04:30
Srikanth Chekuri
b36ef944cc chore: remove data migration (#6683) 2024-12-20 10:52:36 +07:00
Srikanth Chekuri
fa90fad373 chore: add pvcs list (#6654) 2024-12-19 12:01:12 +00:00
Srikanth Chekuri
77420b9d3a chore: address some gaps in k8s monitoring (#6653) 2024-12-19 17:22:39 +05:30
Prashant Shahi
cecc57e72d Merge pull request #6668 from SigNoz/chore/deprecate-develop
chore: develop deprecation and related changes
2024-12-19 13:48:29 +05:30
Prashant Shahi
512adc6471 Merge branch 'main' into chore/deprecate-develop 2024-12-19 13:35:27 +05:30
Prashant Shahi
42fefc65be chore: deprecate develop branch - use main
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-12-19 13:33:09 +05:30
Prashant Shahi
dcc659907a chore(signoz): pin versions: SigNoz 0.64.0
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-12-19 13:33:09 +05:30
Prashant Shahi
b90ed375c2 chore(signoz): pin versions: SigNoz 0.63.0, SigNoz OtelCollector 0.111.16
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-12-19 13:33:09 +05:30
Prashant Shahi
a8a3bd3f7d chore(signoz): pin versions: SigNoz 0.62.0, SigNoz OtelCollector 0.111.15
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-12-19 13:33:09 +05:30
SagarRajput-7
7405bfbbee feat: changed start and end time logic for consumer lag details (#6605) 2024-12-19 13:01:13 +05:30
Nityananda Gohain
67e822e23e feat: api for trace materialization (#6646)
* feat: api for trace materialization

* fix: minor changes and cleanup

* fix: minor fixes

* fix: update errors

* fix: address comments

* fix: address comments
2024-12-19 11:52:20 +07:00
Shivanshu Raj Shrivastava
60dc479a19 fix: add bucketing (#6669)
Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2024-12-18 19:57:33 +05:30
Nityananda Gohain
85cf4f4e2e fix: use placehold in limit and use proper exists (#6667) 2024-12-18 21:07:31 +07:00
Shivanshu Raj Shrivastava
83aa48c721 update service.instance.id (#6665)
* nit: update resource id and revert the flag
2024-12-18 19:06:22 +05:30
Prashant Shahi
823f84f857 Merge pull request #6664 from SigNoz/release/v0.64.x
Release/v0.64.x
2024-12-18 18:29:05 +05:30
Prashant Shahi
8a4d45084d chore(signoz): pin versions: SigNoz 0.64.0
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-12-18 17:57:42 +05:30
Prashant Shahi
5bc6c33899 Merge branch 'main' into release/v0.64.x 2024-12-18 17:55:57 +05:30
Shivanshu Raj Shrivastava
83f6dea2db Add support for trace_v3 schema in messaging queues (#6663)
feat: support trace v3 queries
2024-12-18 17:04:01 +05:30
Nityananda Gohain
7031c866e8 fix: add flags for using trace new schema (#6651) 2024-12-18 17:55:22 +07:00
Prashant Shahi
46bc7c7a21 Merge pull request #6662 from SigNoz/release/v0.63.x
Release/v0.63.x
2024-12-18 15:41:24 +05:30
Prashant Shahi
6d9741c3a4 chore(signoz): pin versions: SigNoz 0.63.0, SigNoz OtelCollector 0.111.16
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-12-18 15:25:20 +05:30
Prashant Shahi
610a8ec704 Merge branch 'main' into release/v0.63.x 2024-12-18 15:07:57 +05:30
Raj Kamal Singh
cd9f27ab08 Fix: QS: logs pipelines: better validation of pipelines being saved (#6652)
* chore: add test validating invalid field paths in pipeline operators are rejected

* chore: refactor posted pipelines validation to use a controller method

* fix: run a collector simulation to validate pipeline config being saved

* chore: minor cleanup
2024-12-18 10:42:14 +05:30
Prashant Shahi
2b5a0ec496 Merge pull request #6625 from SigNoz/release/v0.62.x
Release/v0.62.x
2024-12-12 21:02:17 +05:30
Prashant Shahi
a9440c010c chore(signoz): pin versions: SigNoz 0.62.0, SigNoz OtelCollector 0.111.15
Signed-off-by: Prashant Shahi <prashant@signoz.io>
2024-12-12 15:28:09 +05:30
Prashant Shahi
f9e7eff357 Merge branch 'main' into release/v0.62.x 2024-12-12 15:22:47 +05:30
Prashant Shahi
47d8c9e3e7 Merge pull request #6593 from SigNoz/release-sync/v0.61.x
Release Sync/v0.61.x
2024-12-04 21:28:47 +05:30
308 changed files with 8039 additions and 6674 deletions

View File

@@ -3,7 +3,6 @@ name: build-pipeline
on: on:
pull_request: pull_request:
branches: branches:
- develop
- main - main
- release/v* - release/v*

View File

@@ -3,7 +3,7 @@ name: "Update PR labels and Block PR until related docs are shipped for the feat
on: on:
pull_request: pull_request:
branches: branches:
- develop - main
types: [opened, edited, labeled, unlabeled] types: [opened, edited, labeled, unlabeled]
permissions: permissions:

View File

@@ -42,7 +42,7 @@ jobs:
kubectl create ns sample-application kubectl create ns sample-application
# apply hotrod k8s manifest file # apply hotrod k8s manifest file
kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/develop/sample-apps/hotrod/hotrod.yaml kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/main/sample-apps/hotrod/hotrod.yaml
# wait for all deployments in sample-application namespace to be READY # wait for all deployments in sample-application namespace to be READY
kubectl -n sample-application get deploy --output name | xargs -r -n1 -t kubectl -n sample-application rollout status --timeout=300s kubectl -n sample-application get deploy --output name | xargs -r -n1 -t kubectl -n sample-application rollout status --timeout=300s

View File

@@ -2,7 +2,8 @@ name: Jest Coverage - changed files
on: on:
pull_request: pull_request:
branches: develop branches:
- main
jobs: jobs:
build: build:
@@ -11,7 +12,7 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: "refs/heads/develop" ref: "refs/heads/main"
token: ${{ secrets.GITHUB_TOKEN }} # Provide the GitHub token for authentication token: ${{ secrets.GITHUB_TOKEN }} # Provide the GitHub token for authentication
- name: Fetch branch - name: Fetch branch

36
.github/workflows/prereleaser.yaml vendored Normal file
View File

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

View File

@@ -4,7 +4,6 @@ on:
push: push:
branches: branches:
- main - main
- develop
tags: tags:
- v* - v*
@@ -58,6 +57,17 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Create .env file
run: |
echo 'INTERCOM_APP_ID="${{ secrets.INTERCOM_APP_ID }}"' > frontend/.env
echo 'SEGMENT_ID="${{ secrets.SEGMENT_ID }}"' >> frontend/.env
echo 'SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}"' >> frontend/.env
echo 'SENTRY_ORG="${{ secrets.SENTRY_ORG }}"' >> frontend/.env
echo 'SENTRY_PROJECT_ID="${{ secrets.SENTRY_PROJECT_ID }}"' >> frontend/.env
echo 'SENTRY_DSN="${{ secrets.SENTRY_DSN }}"' >> frontend/.env
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
- name: Setup golang - name: Setup golang
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:

32
.github/workflows/releaser.yaml vendored Normal file
View File

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

View File

@@ -3,7 +3,6 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- develop
paths: paths:
- 'frontend/**' - 'frontend/**'
defaults: defaults:

View File

@@ -1,12 +1,12 @@
name: staging-deployment name: staging-deployment
# Trigger deployment only on push to develop branch # Trigger deployment only on push to main branch
on: on:
push: push:
branches: branches:
- develop - main
jobs: jobs:
deploy: deploy:
name: Deploy latest develop branch to staging name: Deploy latest main branch to staging
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: staging environment: staging
permissions: permissions:

View File

@@ -44,7 +44,7 @@ jobs:
git add . git add .
git stash push -m "stashed on $(date --iso-8601=seconds)" git stash push -m "stashed on $(date --iso-8601=seconds)"
git fetch origin git fetch origin
git checkout develop git checkout main
git pull git pull
# This is added to include the scenerio when new commit in PR is force-pushed # This is added to include the scenerio when new commit in PR is force-pushed
git branch -D ${GITHUB_BRANCH} git branch -D ${GITHUB_BRANCH}

View File

@@ -339,7 +339,7 @@ to make SigNoz UI available at [localhost:3301](http://localhost:3301)
**5.1.1 To install the HotROD sample app:** **5.1.1 To install the HotROD sample app:**
```bash ```bash
curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-install.sh \ curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-install.sh \
| HELM_RELEASE=my-release SIGNOZ_NAMESPACE=platform bash | HELM_RELEASE=my-release SIGNOZ_NAMESPACE=platform bash
``` ```
@@ -362,7 +362,7 @@ kubectl -n sample-application run strzal --image=djbingham/curl \
**5.1.4 To delete the HotROD sample app:** **5.1.4 To delete the HotROD sample app:**
```bash ```bash
curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-delete.sh \ curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-delete.sh \
| HOTROD_NAMESPACE=sample-application bash | HOTROD_NAMESPACE=sample-application bash
``` ```

View File

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

32
conf/defaults.yaml Normal file
View File

@@ -0,0 +1,32 @@
##################### SigNoz Configuration Defaults #####################
#
# Do not modify this file
#
##################### Web #####################
web:
# The prefix to serve web on
prefix: /
# The directory containing the static build files.
directory: /etc/signoz/web
##################### Cache #####################
cache:
# specifies the caching provider to use.
provider: memory
# memory: Uses in-memory caching.
memory:
# Time-to-live for cache entries in memory. Specify the duration in ns
ttl: 60000000000
# The interval at which the cache will be cleaned up
cleanupInterval:
# redis: Uses Redis as the caching backend.
redis:
# The hostname or IP address of the Redis server.
host: localhost
# The port on which the Redis server is running. Default is usually 6379.
port: 6379
# The password for authenticating with the Redis server, if required.
password:
# The Redis database number to use
db: 0

View File

@@ -58,7 +58,7 @@ from the HotROD application, you should see the data generated from hotrod in Si
```sh ```sh
kubectl create ns sample-application kubectl create ns sample-application
kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/develop/sample-apps/hotrod/hotrod.yaml kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/main/sample-apps/hotrod/hotrod.yaml
``` ```
To generate load: To generate load:

View File

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

View File

@@ -110,6 +110,7 @@ exporters:
clickhousetraces: clickhousetraces:
datasource: tcp://clickhouse:9000/signoz_traces datasource: tcp://clickhouse:9000/signoz_traces
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING} low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
use_new_schema: true
clickhousemetricswrite: clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/signoz_metrics endpoint: tcp://clickhouse:9000/signoz_metrics
resource_to_telemetry_conversion: resource_to_telemetry_conversion:

View File

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

View File

@@ -25,7 +25,8 @@ services:
command: command:
[ [
"-config=/root/config/prometheus.yml", "-config=/root/config/prometheus.yml",
"--use-logs-new-schema=true" "--use-logs-new-schema=true",
"--use-trace-new-schema=true"
] ]
ports: ports:
- "6060:6060" - "6060:6060"

View File

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

View File

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

View File

@@ -119,6 +119,7 @@ exporters:
clickhousetraces: clickhousetraces:
datasource: tcp://clickhouse:9000/signoz_traces datasource: tcp://clickhouse:9000/signoz_traces
low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING} low_cardinal_exception_grouping: ${env:LOW_CARDINAL_EXCEPTION_GROUPING}
use_new_schema: true
clickhousemetricswrite: clickhousemetricswrite:
endpoint: tcp://clickhouse:9000/signoz_metrics endpoint: tcp://clickhouse:9000/signoz_metrics
resource_to_telemetry_conversion: resource_to_telemetry_conversion:

View File

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

View File

@@ -32,6 +32,8 @@ import (
baseauth "go.signoz.io/signoz/pkg/query-service/auth" baseauth "go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/migrate" "go.signoz.io/signoz/pkg/query-service/migrate"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3" v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/signoz"
"go.signoz.io/signoz/pkg/web"
licensepkg "go.signoz.io/signoz/ee/query-service/license" licensepkg "go.signoz.io/signoz/ee/query-service/license"
"go.signoz.io/signoz/ee/query-service/usage" "go.signoz.io/signoz/ee/query-service/usage"
@@ -61,6 +63,7 @@ import (
const AppDbEngine = "sqlite" const AppDbEngine = "sqlite"
type ServerOptions struct { type ServerOptions struct {
SigNoz *signoz.SigNoz
PromConfigPath string PromConfigPath string
SkipTopLvlOpsPath string SkipTopLvlOpsPath string
HTTPHostPort string HTTPHostPort string
@@ -78,6 +81,7 @@ type ServerOptions struct {
GatewayUrl string GatewayUrl string
UseLogsNewSchema bool UseLogsNewSchema bool
UseTraceNewSchema bool UseTraceNewSchema bool
SkipWebFrontend bool
} }
// Server runs HTTP api service // Server runs HTTP api service
@@ -289,7 +293,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
usageManager: usageManager, usageManager: usageManager,
} }
httpServer, err := s.createPublicServer(apiHandler) httpServer, err := s.createPublicServer(apiHandler, serverOptions.SigNoz.Web)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -338,7 +342,7 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
}, nil }, nil
} }
func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, error) { func (s *Server) createPublicServer(apiHandler *api.APIHandler, web *web.Web) (*http.Server, error) {
r := baseapp.NewRouter() r := baseapp.NewRouter()
@@ -382,6 +386,13 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
handler = handlers.CompressHandler(handler) handler = handlers.CompressHandler(handler)
if !s.serverOptions.SkipWebFrontend {
err := web.AddToRouter(r)
if err != nil {
return nil, err
}
}
return &http.Server{ return &http.Server{
Handler: handler, Handler: handler,
}, nil }, nil

View File

@@ -10,13 +10,17 @@ import (
"syscall" "syscall"
"time" "time"
"go.opentelemetry.io/collector/confmap"
"go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0" semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.signoz.io/signoz/ee/query-service/app" "go.signoz.io/signoz/ee/query-service/app"
signozconfig "go.signoz.io/signoz/pkg/config"
"go.signoz.io/signoz/pkg/confmap/provider/signozenvprovider"
"go.signoz.io/signoz/pkg/query-service/auth" "go.signoz.io/signoz/pkg/query-service/auth"
baseconst "go.signoz.io/signoz/pkg/query-service/constants" baseconst "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/migrate" "go.signoz.io/signoz/pkg/query-service/migrate"
"go.signoz.io/signoz/pkg/query-service/version" "go.signoz.io/signoz/pkg/query-service/version"
"go.signoz.io/signoz/pkg/signoz"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
@@ -104,6 +108,7 @@ func main() {
var dialTimeout time.Duration var dialTimeout time.Duration
var gatewayUrl string var gatewayUrl string
var useLicensesV3 bool var useLicensesV3 bool
var skipWebFrontend bool
flag.BoolVar(&useLogsNewSchema, "use-logs-new-schema", false, "use logs_v2 schema for logs") flag.BoolVar(&useLogsNewSchema, "use-logs-new-schema", false, "use logs_v2 schema for logs")
flag.BoolVar(&useTraceNewSchema, "use-trace-new-schema", false, "use new schema for traces") flag.BoolVar(&useTraceNewSchema, "use-trace-new-schema", false, "use new schema for traces")
@@ -121,7 +126,7 @@ func main() {
flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')") flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')")
flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)") flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)")
flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses") flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses")
flag.BoolVar(&skipWebFrontend, "skip-web-frontend", false, "skip web frontend")
flag.Parse() flag.Parse()
loggerMgr := initZapLog(enableQueryServiceLogOTLPExport) loggerMgr := initZapLog(enableQueryServiceLogOTLPExport)
@@ -131,7 +136,25 @@ func main() {
version.PrintVersion() version.PrintVersion()
config, err := signozconfig.New(context.Background(), signozconfig.ProviderSettings{
ResolverSettings: confmap.ResolverSettings{
URIs: []string{"signozenv:"},
ProviderFactories: []confmap.ProviderFactory{
signozenvprovider.NewFactory(),
},
},
})
if err != nil {
zap.L().Fatal("Failed to create config", zap.Error(err))
}
signoz, err := signoz.New(config, skipWebFrontend)
if err != nil {
zap.L().Fatal("Failed to create signoz struct", zap.Error(err))
}
serverOptions := &app.ServerOptions{ serverOptions := &app.ServerOptions{
SigNoz: signoz,
HTTPHostPort: baseconst.HTTPHostPort, HTTPHostPort: baseconst.HTTPHostPort,
PromConfigPath: promConfigPath, PromConfigPath: promConfigPath,
SkipTopLvlOpsPath: skipTopLvlOpsPath, SkipTopLvlOpsPath: skipTopLvlOpsPath,
@@ -148,6 +171,7 @@ func main() {
GatewayUrl: gatewayUrl, GatewayUrl: gatewayUrl,
UseLogsNewSchema: useLogsNewSchema, UseLogsNewSchema: useLogsNewSchema,
UseTraceNewSchema: useTraceNewSchema, UseTraceNewSchema: useTraceNewSchema,
SkipWebFrontend: skipWebFrontend,
} }
// Read the jwt secret key // Read the jwt secret key

View File

@@ -13,8 +13,3 @@ if [ "$branch" = "main" ]; then
echo "${color_red}${bold}You can't commit directly to the main branch${reset}" echo "${color_red}${bold}You can't commit directly to the main branch${reset}"
exit 1 exit 1
fi fi
if [ "$branch" = "develop" ]; then
echo "${color_red}${bold}You can't commit directly to the develop branch${reset}"
exit 1
fi

View File

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

View File

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

View File

@@ -26,7 +26,8 @@
"MY_SETTINGS": "SigNoz | My Settings", "MY_SETTINGS": "SigNoz | My Settings",
"ORG_SETTINGS": "SigNoz | Organization Settings", "ORG_SETTINGS": "SigNoz | Organization Settings",
"INGESTION_SETTINGS": "SigNoz | Ingestion Settings", "INGESTION_SETTINGS": "SigNoz | Ingestion Settings",
"API_KEYS": "SigNoz | Access Tokens", "API_KEYS": "SigNoz | API Keys",
"CUSTOM_DOMAIN_SETTINGS": "SigNoz | Custom Domain Settings",
"SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong", "SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong",
"UN_AUTHORIZED": "SigNoz | Unauthorized", "UN_AUTHORIZED": "SigNoz | Unauthorized",
"NOT_FOUND": "SigNoz | Page Not Found", "NOT_FOUND": "SigNoz | Page Not Found",

View File

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

View File

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

View File

@@ -32,7 +32,8 @@
"MY_SETTINGS": "SigNoz | My Settings", "MY_SETTINGS": "SigNoz | My Settings",
"ORG_SETTINGS": "SigNoz | Organization Settings", "ORG_SETTINGS": "SigNoz | Organization Settings",
"INGESTION_SETTINGS": "SigNoz | Ingestion Settings", "INGESTION_SETTINGS": "SigNoz | Ingestion Settings",
"API_KEYS": "SigNoz | Access Tokens", "API_KEYS": "SigNoz | API Keys",
"CUSTOM_DOMAIN_SETTINGS": "SigNoz | Custom Domain Settings",
"SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong", "SOMETHING_WENT_WRONG": "SigNoz | Something Went Wrong",
"UN_AUTHORIZED": "SigNoz | Unauthorized", "UN_AUTHORIZED": "SigNoz | Unauthorized",
"NOT_FOUND": "SigNoz | Page Not Found", "NOT_FOUND": "SigNoz | Page Not Found",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import axios from 'api'; import axios from 'api';
import { DropRateAPIResponse } from 'pages/MessagingQueues/MQDetails/DropRateView/dropRateViewUtils';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
import { DropRateAPIResponse } from '../DropRateView/dropRateViewUtils';
import { MessagingQueueServicePayload } from './getConsumerLagDetails'; import { MessagingQueueServicePayload } from './getConsumerLagDetails';
export const getKafkaSpanEval = async ( export const getKafkaSpanEval = async (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ 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';
@@ -81,6 +82,12 @@ function CustomTimePickerPopoverContent({
const handleTimezoneHintClick = (): void => { const handleTimezoneHintClick = (): void => {
setActiveView('timezone'); setActiveView('timezone');
setIsOpenedFromFooter(true); setIsOpenedFromFooter(true);
logEvent(
'DateTimePicker: Timezone picker opened from time range picker footer',
{
page: pathname,
},
);
}; };
if (activeView === 'timezone') { if (activeView === 'timezone') {

View File

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

View File

@@ -2,6 +2,7 @@ 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';
@@ -126,7 +127,6 @@ function TimezonePicker({
setIsOpen, setIsOpen,
isOpenedFromFooter, isOpenedFromFooter,
}: TimezonePickerProps): JSX.Element { }: TimezonePickerProps): JSX.Element {
console.log({ isOpenedFromFooter });
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const { timezone, updateTimezone } = useTimezone(); const { timezone, updateTimezone } = useTimezone();
const [selectedTimezone, setSelectedTimezone] = useState<string>( const [selectedTimezone, setSelectedTimezone] = useState<string>(
@@ -157,6 +157,12 @@ function TimezonePicker({
updateTimezone(timezone); updateTimezone(timezone);
handleCloseTimezonePicker(); handleCloseTimezonePicker();
setIsOpen(false); setIsOpen(false);
logEvent('DateTimePicker: New Timezone Selected', {
timezone: {
name: timezone.name,
offset: timezone.offset,
},
});
}, },
[handleCloseTimezonePicker, setIsOpen, updateTimezone], [handleCloseTimezonePicker, setIsOpen, updateTimezone],
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,16 +17,15 @@ import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { History } from 'history'; import { History } from 'history';
import { Bolt, Check, OctagonAlert, X } from 'lucide-react'; import { Bolt, Check, OctagonAlert, X } from 'lucide-react';
import {
KAFKA_SETUP_DOC_LINK,
MessagingQueueHealthCheckService,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { ReactNode, useEffect, useState } from 'react'; import { ReactNode, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { isCloudUser } from 'utils/app'; import { isCloudUser } from 'utils/app';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import {
KAFKA_SETUP_DOC_LINK,
MessagingQueueHealthCheckService,
} from '../MessagingQueuesUtils';
interface AttributeCheckListProps { interface AttributeCheckListProps {
visible: boolean; visible: boolean;
onClose: () => void; onClose: () => void;

View File

@@ -5,9 +5,9 @@ import { Button } from 'antd';
import cx from 'classnames'; import cx from 'classnames';
import { useOnboardingStatus } from 'hooks/messagingQueue/useOnboardingStatus'; import { useOnboardingStatus } from 'hooks/messagingQueue/useOnboardingStatus';
import { Bolt, FolderTree } from 'lucide-react'; import { Bolt, FolderTree } from 'lucide-react';
import { MessagingQueueHealthCheckService } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { MessagingQueueHealthCheckService } from '../MessagingQueuesUtils';
import AttributeCheckList from './AttributeCheckList'; import AttributeCheckList from './AttributeCheckList';
interface MessagingQueueHealthCheckProps { interface MessagingQueueHealthCheckProps {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,7 +34,8 @@ const ROUTES = {
MY_SETTINGS: '/my-settings', MY_SETTINGS: '/my-settings',
SETTINGS: '/settings', SETTINGS: '/settings',
ORG_SETTINGS: '/settings/org-settings', ORG_SETTINGS: '/settings/org-settings',
API_KEYS: '/settings/access-tokens', CUSTOM_DOMAIN_SETTINGS: '/settings/custom-domain-settings',
API_KEYS: '/settings/api-keys',
INGESTION_SETTINGS: '/settings/ingestion-settings', INGESTION_SETTINGS: '/settings/ingestion-settings',
SOMETHING_WENT_WRONG: '/something-went-wrong', SOMETHING_WENT_WRONG: '/something-went-wrong',
UN_AUTHORIZED: '/un-authorized', UN_AUTHORIZED: '/un-authorized',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,12 +14,9 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { Atom, Play, Terminal } from 'lucide-react'; import { Atom, Play, Terminal } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { AlertTypes } from 'types/api/alerts/alertTypes'; import { AlertTypes } from 'types/api/alerts/alertTypes';
import { AlertDef } from 'types/api/alerts/def'; import { AlertDef } from 'types/api/alerts/def';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import AppReducer from 'types/reducer/app';
import ChQuerySection from './ChQuerySection'; import ChQuerySection from './ChQuerySection';
import PromqlSection from './PromqlSection'; import PromqlSection from './PromqlSection';
@@ -38,14 +35,9 @@ function QuerySection({
const { t } = useTranslation('alerts'); const { t } = useTranslation('alerts');
const [currentTab, setCurrentTab] = useState(queryCategory); const [currentTab, setCurrentTab] = useState(queryCategory);
const { featureResponse } = useSelector<AppState, AppReducer>( // TODO[vikrantgupta25] : check if this is still required ??
(state) => state.app,
);
const handleQueryCategoryChange = (queryType: string): void => { const handleQueryCategoryChange = (queryType: string): void => {
featureResponse.refetch().then(() => { setQueryCategory(queryType as EQueryType);
setQueryCategory(queryType as EQueryType);
});
setCurrentTab(queryType as EQueryType); setCurrentTab(queryType as EQueryType);
}; };

View File

@@ -1,14 +1,7 @@
import './FormAlertRules.styles.scss'; import './FormAlertRules.styles.scss';
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons'; import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
import { import { Button, FormInstance, Modal, SelectProps, Typography } from 'antd';
Button,
FormInstance,
Modal,
SelectProps,
Tooltip,
Typography,
} from 'antd';
import saveAlertApi from 'api/alerts/save'; import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert'; import testAlertApi from 'api/alerts/testAlert';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
@@ -23,10 +16,6 @@ import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters'; import { BuilderUnitsFilter } from 'container/QueryBuilder/filters';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import useFeatureFlag, {
MESSAGE,
useIsFeatureDisabled,
} from 'hooks/useFeatureFlag';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history'; import history from 'lib/history';
@@ -35,6 +24,7 @@ import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQu
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { BellDot, ExternalLink } from 'lucide-react'; import { BellDot, ExternalLink } from 'lucide-react';
import Tabs2 from 'periscope/components/Tabs2'; import Tabs2 from 'periscope/components/Tabs2';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query'; import { useQueryClient } from 'react-query';
@@ -96,6 +86,7 @@ function FormAlertRules({
}: FormAlertRuleProps): JSX.Element { }: FormAlertRuleProps): JSX.Element {
// init namespace for translations // init namespace for translations
const { t } = useTranslation('alerts'); const { t } = useTranslation('alerts');
const { featureFlags } = useAppContext();
const { selectedTime: globalSelectedInterval } = useSelector< const { selectedTime: globalSelectedInterval } = useSelector<
AppState, AppState,
@@ -476,9 +467,9 @@ function FormAlertRules({
panelType, panelType,
]); ]);
const isAlertAvailable = useIsFeatureDisabled( const isAlertAvailable =
FeatureKeys.QUERY_BUILDER_ALERTS, !featureFlags?.find((flag) => flag.name === FeatureKeys.QUERY_BUILDER_ALERTS)
); ?.active || false;
const saveRule = useCallback(async () => { const saveRule = useCallback(async () => {
if (!isFormValid()) { if (!isFormValid()) {
@@ -766,7 +757,8 @@ function FormAlertRules({
]; ];
const isAnomalyDetectionEnabled = const isAnomalyDetectionEnabled =
useFeatureFlag(FeatureKeys.ANOMALY_DETECTION)?.active || false; featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
?.active || false;
return ( return (
<> <>
@@ -866,22 +858,20 @@ function FormAlertRules({
{renderBasicInfo()} {renderBasicInfo()}
</div> </div>
<ButtonContainer> <ButtonContainer>
<Tooltip title={isAlertAvailableToSave ? MESSAGE.ALERT : ''}> <ActionButton
<ActionButton loading={loading || false}
loading={loading || false} type="primary"
type="primary" onClick={onSaveHandler}
onClick={onSaveHandler} icon={<SaveOutlined />}
icon={<SaveOutlined />} disabled={
disabled={ isAlertNameMissing ||
isAlertNameMissing || isAlertAvailableToSave ||
isAlertAvailableToSave || !isChannelConfigurationValid ||
!isChannelConfigurationValid || queryStatus === 'error'
queryStatus === 'error' }
} >
> {isNewRule ? t('button_createrule') : t('button_savechanges')}
{isNewRule ? t('button_createrule') : t('button_savechanges')} </ActionButton>
</ActionButton>
</Tooltip>
<ActionButton <ActionButton
loading={loading || false} loading={loading || false}

View File

@@ -7,12 +7,11 @@ import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
import useComponentPermission from 'hooks/useComponentPermission'; import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import find from 'lodash-es/find'; import find from 'lodash-es/find';
import { useAppContext } from 'providers/App/App';
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query'; import { UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
import { useInterval } from 'react-use'; import { useInterval } from 'react-use';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
import { import {
IDiskType, IDiskType,
@@ -24,7 +23,6 @@ import {
PayloadPropsMetrics as GetRetentionPeriodMetricsPayload, PayloadPropsMetrics as GetRetentionPeriodMetricsPayload,
PayloadPropsTraces as GetRetentionPeriodTracesPayload, PayloadPropsTraces as GetRetentionPeriodTracesPayload,
} from 'types/api/settings/getRetention'; } from 'types/api/settings/getRetention';
import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app'; import { isCloudUser } from 'utils/app';
import Retention from './Retention'; import Retention from './Retention';
@@ -68,11 +66,11 @@ function GeneralSettings({
logsTtlValuesPayload, logsTtlValuesPayload,
); );
const { role } = useSelector<AppState, AppReducer>((state) => state.app); const { user } = useAppContext();
const [setRetentionPermission] = useComponentPermission( const [setRetentionPermission] = useComponentPermission(
['set_retention_period'], ['set_retention_period'],
role, user.role,
); );
const [ const [

View File

@@ -2,14 +2,12 @@ import { Typography } from 'antd';
import getDisks from 'api/disks/getDisks'; import getDisks from 'api/disks/getDisks';
import getRetentionPeriodApi from 'api/settings/getRetention'; import getRetentionPeriodApi from 'api/settings/getRetention';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { useAppContext } from 'providers/App/App';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQueries } from 'react-query'; import { useQueries } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
import { TTTLType } from 'types/api/settings/common'; import { TTTLType } from 'types/api/settings/common';
import { PayloadProps as GetRetentionPeriodAPIPayloadProps } from 'types/api/settings/getRetention'; import { PayloadProps as GetRetentionPeriodAPIPayloadProps } from 'types/api/settings/getRetention';
import AppReducer from 'types/reducer/app';
import GeneralSettingsContainer from './GeneralSettings'; import GeneralSettingsContainer from './GeneralSettings';
@@ -19,7 +17,7 @@ type TRetentionAPIReturn<T extends TTTLType> = Promise<
function GeneralSettings(): JSX.Element { function GeneralSettings(): JSX.Element {
const { t } = useTranslation('common'); const { t } = useTranslation('common');
const { user } = useSelector<AppState, AppReducer>((state) => state.app); const { user } = useAppContext();
const [ const [
getRetentionPeriodMetricsApiResponse, getRetentionPeriodMetricsApiResponse,

View File

@@ -6,11 +6,9 @@ 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';
@@ -21,7 +19,7 @@ export default function DashboardEmptyState(): JSX.Element {
handleToggleDashboardSlider, handleToggleDashboardSlider,
} = useDashboard(); } = useDashboard();
const { user, role } = useSelector<AppState, AppReducer>((state) => state.app); const { user } = useAppContext();
let permissions: ComponentTypes[] = ['add_panel']; let permissions: ComponentTypes[] = ['add_panel'];
if (isDashboardLocked) { if (isDashboardLocked) {
@@ -31,7 +29,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)
: role; : user.role;
const [addPanelPermission] = useComponentPermission(permissions, userRole); const [addPanelPermission] = useComponentPermission(permissions, userRole);

View File

@@ -22,11 +22,8 @@ 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';
@@ -77,10 +74,6 @@ 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);
@@ -117,7 +110,6 @@ function WidgetGraphComponent({
setSelectedDashboard(updatedDashboard.payload); setSelectedDashboard(updatedDashboard.payload);
} }
setDeleteModal(false); setDeleteModal(false);
featureResponse.refetch();
}, },
onError: () => { onError: () => {
notifications.error({ notifications.error({

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