Compare commits

..

77 Commits

Author SHA1 Message Date
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
Vibhu Pandey
14fbb1fcda fix(flag): add missing flag back (#6647) 2024-12-17 01:12:31 +05:30
Vikrant Gupta
96da21df05 fix: correlation time stamps for APM to traces and logs (#6632) 2024-12-17 00:00:15 +05:30
Vikrant Gupta
8608f02263 fix: timerange selected for traces to logs (#6634) 2024-12-16 22:35:25 +05:30
Vikrant Gupta
2701ae5c34 fix: unable to remove query tags from the beginning when similar tags present (#6645)
* fix: unable to remove query tags from the begining

* fix: focus only when the filters dropdown is opened

* fix: used auto focus prop
2024-12-16 22:25:37 +05:30
Vibhu Pandey
951593b0a3 feat(licenses): deprecate licenses v2 (#6626)
deprecate licenses v2
2024-12-16 10:23:23 +00:00
Srikanth Chekuri
e6766023dd chore: use tag attributes v2 table (#6616) 2024-12-16 09:59:16 +00:00
Srikanth Chekuri
bef5b96c5c chore: add queries with tag attributes v1 (#6643) 2024-12-16 13:04:55 +05:30
Shaheer Kochai
b29359dee0 fix: improve traces table ux by making the table scrollable (#6423)
* fix: improve traces table ux by making the table scrollable

* chore: remove explicit scroll.x and modify the existing scroll.x from true to max-content

* fix(Traces Explorer): make trace explorer card full-width when collapsed, resizable when expanded
2024-12-16 07:00:40 +00:00
Amlan Kumar Nandy
9a1cd65b73 feat: add functionality to export dashboard as json from listing page 2024-12-16 12:10:21 +05:30
Amlan Kumar Nandy
8ab0c066d6 Merge branch 'develop' into dashboard-list-export-json 2024-12-16 11:39:18 +05:30
Shaheer Kochai
b333aa3775 Feat: Timezone picker feature (#6474)
* feat: time picker hint and timezone picker UI with basic functionality + helper to get timezones

* feat: add support for esc keypress to close the timezone picker

* chore: add the selected timezone as url param and close timezone picker on select

* fix: overall improvement + add searchIndex to timezone

* feat: timezone preferences UI

* chore: improve timezone utils

* chore: change timezone item from div to button

* feat: display timezone in timepicker input

* chore: fix the typo

* fix: don't focus on time picker when timezone is clicked

* fix: fix the issue of timezone breaking for browser and utc timezones

* fix: display the timezone in timepicker hint 'You are at'

* feat: timezone basic functionality (#6492)

* chore: change div to fragment + change type to any as the ESLint complains otherwise

* chore: manage etc timezone filtering with an arg

* chore: update timezone wrapper class name

* fix: add timezone support to downloaded logs

* feat: add current timezone to dashboard list and configure metadata modal

* fix: add pencil icon next to timezone hint + change the copy to Current timezone

* fix: properly handle the escape button behavior for timezone picker

* chore: replace @vvo/tzdb with native Intl API for timezones

* feat: lightmode for timezone picker and timezone adaptation components

* fix: use normald tz in browser timezone

* fix: timezone picker lightmode fixes

* feat: display selected time range in 12 hour format

* chore: remove unnecessary optional chaining

* fix: fix the typo in css variable

* chore: add em dash and change icon for timezone hint in date/time picker

* chore: move pen line icon to the right of timezone offset

* fix: fix the failing tests

* feat: handle switching off the timezone adaptation
2024-12-16 11:27:20 +05:30
amlannandy
8a3319cdf5 chore: address comments 2024-12-16 11:24:57 +05:30
amlannandy
d09c4d947e feat: update unit test 2024-12-16 11:24:57 +05:30
amlannandy
2508e6f9f1 feat: update unit test 2024-12-16 11:24:57 +05:30
amlannandy
1b8213653a feat: update tests 2024-12-16 11:24:57 +05:30
amlannandy
b499b10333 feat: add unit test 2024-12-16 11:24:57 +05:30
amlannandy
b35b975798 chore: address comments 2024-12-16 11:24:57 +05:30
amlannandy
715f8a2363 feat: address comments 2024-12-16 11:24:57 +05:30
amlannandy
8d1c4491b7 feat: add functionality to export dashboard as json from listing page 2024-12-16 11:24:57 +05:30
dependabot[bot]
e3caa6a8f5 chore(deps): bump golang.org/x/crypto from 0.27.0 to 0.31.0 (#6638)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.27.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.27.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-14 04:29:25 +00:00
Vibhu Pandey
a1059ed949 feat(signoz-ingestion-key): rename signoz-access-token to signoz-ingestion-key (#6633) 2024-12-13 18:18:12 +05:30
SagarRajput-7
8c46de8eac feat: added table column and row logic for the new api response structure for producer overview (#6433)
* feat: added table column and row logic for the new api response structure for prodcure overview

* feat: fixed typo in function name

* feat: consumed new 2 table merging logic for producer latency overview

* feat: added 3 digit precision to 'ingestion_rate' and 'byte_rate'in consumer overview
2024-12-13 11:25:39 +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
Nityananda Gohain
0fbfb6b22b fix: fix count aggregate attribute column name (#6613)
* fix: fix count aggregate attribute column name

* fix: add test for column as well

* fix: use new schema in threshold rule enrichment
2024-12-11 13:37:25 +05:30
Nityananda Gohain
b25df66381 fix: migration for ingestion dashboard (#6610)
* fix: migration for ingestion dashboard

* fix: remove redundant uuid

* Update pkg/query-service/app/dashboards/model.go

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-12-10 18:49:21 +05:30
Nityananda Gohain
32fa5a403c fix: update default alert ch queries for traces and logs (#6552)
* fix: update default alert ch queries for traces and logs

* fix: use service.name instead of peer.service

* fix: add link to docs

* fix: add alias for service name
2024-12-10 18:36:02 +05:30
Nityananda Gohain
f9d4cf19e9 fix: smartTraceAlgo for new schema (#6602) 2024-12-10 13:43:51 +05:30
Amlan Kumar Nandy
81775c7d55 fix: resolve unnecessary refetching of graphs on service details page 2024-12-10 12:00:55 +05:30
amlannandy
8d2666004b fix: resolve the unneccessary refetching of graphs on service details page 2024-12-10 11:39:26 +05:30
Shivanshu Raj Shrivastava
51baf7f8d3 feat: add byte rate to producer API (#6579)
* fix: fix partition check for topic throughput
2024-12-09 15:18:41 +00:00
Amlan Kumar Nandy
31a2926375 chore: replace the 'Get started' CTA with 'New source' 2024-12-09 14:34:18 +05:30
amlannandy
8c6225185d chore: replace the 'Get started' CTA with 'New source' 2024-12-09 14:00:41 +05:30
deepakdinesh1123
d4458d65ad fix(docker): otel-collector-migrator command fixed in docker-compose-core.yaml (#6564)
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2024-12-07 20:15:07 +05:30
Nityananda Gohain
02d8fdb212 fix: enable smart trace detail (#6596) 2024-12-05 09:30:39 +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
602 changed files with 6420 additions and 4773 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

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:

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

@@ -0,0 +1,16 @@
name: releaser
on:
# schedule every wednesday 9:30 AM UTC (3pm IST)
schedule:
- cron: '30 9 * * 3'
# allow manual triggering of the workflow by a maintainer with no inputs
workflow_dispatch: {}
jobs:
releaser:
uses: signoz/primus.workflows/.github/workflows/releaser-signoz.yaml@main
secrets: inherit
with:
PRIMUS_REF: main

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 "------------------"

11
conf/defaults.yaml Normal file
View File

@@ -0,0 +1,11 @@
##################### SigNoz Configuration Defaults #####################
#
# Do not modify this file
#
##################### Web #####################
web:
# The prefix to serve web on
prefix: /
# The directory containing the static build files.
directory: /etc/signoz/web

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.65.0
command: command: ["-config=/root/config/prometheus.yml", "--use-logs-new-schema=true", "--use-trace-new-schema=true"]
[
"-config=/root/config/prometheus.yml",
"--use-logs-new-schema=true"
]
# ports: # ports:
# - "6060:6060" # pprof port # - "6060:6060" # pprof port
# - "8080:8080" # query-service port # - "8080:8080" # query-service port
@@ -169,24 +149,16 @@ services:
- TELEMETRY_ENABLED=true - TELEMETRY_ENABLED=true
- DEPLOYMENT_TYPE=docker-swarm - DEPLOYMENT_TYPE=docker-swarm
healthcheck: healthcheck:
test: test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/health"]
[
"CMD",
"wget",
"--spider",
"-q",
"localhost:8080/api/v1/health"
]
interval: 30s interval: 30s
timeout: 5s timeout: 5s
retries: 3 retries: 3
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
<<: *db-depend !!merge <<: *db-depend
frontend: frontend:
image: signoz/frontend:0.61.0 image: signoz/frontend:0.65.0
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
@@ -197,15 +169,9 @@ services:
- "3301:3301" - "3301:3301"
volumes: volumes:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:0.111.14 image: signoz/signoz-otel-collector:0.111.16
command: command: ["--config=/etc/otel-collector-config.yaml", "--manager-config=/etc/manager-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
[
"--config=/etc/otel-collector-config.yaml",
"--manager-config=/etc/manager-config.yaml",
"--feature-gates=-pkg.translator.prometheus.NormalizeName"
]
user: root # required for reading docker container logs user: root # required for reading docker container logs
volumes: volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
@@ -235,22 +201,20 @@ services:
- clickhouse - clickhouse
- otel-collector-migrator - otel-collector-migrator
- query-service - query-service
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.111.14 image: signoz/signoz-schema-migrator:0.111.16
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure
delay: 5s delay: 5s
command: command:
- "sync" - "sync"
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
- "--up=" - "--up="
depends_on: depends_on:
- clickhouse - clickhouse
# - clickhouse-2 # - clickhouse-2
# - clickhouse-3 # - clickhouse-3
logspout: logspout:
image: "gliderlabs/logspout:v3.2.14" image: "gliderlabs/logspout:v3.2.14"
volumes: volumes:
@@ -263,17 +227,15 @@ services:
mode: global mode: global
restart_policy: restart_policy:
condition: on-failure condition: on-failure
hotrod: hotrod:
image: jaegertracing/example-hotrod:1.30 image: jaegertracing/example-hotrod:1.30
command: [ "all" ] command: ["all"]
environment: environment:
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces - JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
logging: logging:
options: options:
max-size: 50m max-size: 50m
max-file: "3" max-file: "3"
load-hotrod: load-hotrod:
image: "signoz/locust:1.2.3" image: "signoz/locust:1.2.3"
hostname: load-hotrod hostname: load-hotrod

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

@@ -69,10 +69,12 @@ services:
- --storage.path=/data - --storage.path=/data
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.14} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.16}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "sync"
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
- "--up="
depends_on: depends_on:
clickhouse: clickhouse:
condition: service_healthy condition: service_healthy
@@ -84,7 +86,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector: otel-collector:
container_name: signoz-otel-collector container_name: signoz-otel-collector
image: signoz/signoz-otel-collector:0.111.14 image: signoz/signoz-otel-collector:0.111.16
command: command:
[ [
"--config=/etc/otel-collector-config.yaml", "--config=/etc/otel-collector-config.yaml",

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.65.0}
container_name: signoz-query-service container_name: signoz-query-service
command: command: ["-config=/root/config/prometheus.yml", "--use-logs-new-schema=true", "--use-trace-new-schema=true"]
[
"-config=/root/config/prometheus.yml",
"--use-logs-new-schema=true"
]
# ports: # ports:
# - "6060:6060" # pprof port # - "6060:6060" # pprof port
# - "8080:8080" # query-service port # - "8080:8080" # query-service port
@@ -187,21 +166,13 @@ services:
- DEPLOYMENT_TYPE=docker-standalone-amd - DEPLOYMENT_TYPE=docker-standalone-amd
restart: on-failure restart: on-failure
healthcheck: healthcheck:
test: test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/health"]
[
"CMD",
"wget",
"--spider",
"-q",
"localhost:8080/api/v1/health"
]
interval: 30s interval: 30s
timeout: 5s timeout: 5s
retries: 3 retries: 3
<<: *db-depend !!merge <<: *db-depend
frontend: frontend:
image: signoz/frontend:${DOCKER_TAG:-0.61.0} image: signoz/frontend:${DOCKER_TAG:-0.65.0}
container_name: signoz-frontend container_name: signoz-frontend
restart: on-failure restart: on-failure
depends_on: depends_on:
@@ -211,9 +182,8 @@ services:
- "3301:3301" - "3301:3301"
volumes: volumes:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator-sync: otel-collector-migrator-sync:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.14} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.16}
container_name: otel-migrator-sync container_name: otel-migrator-sync
command: command:
- "sync" - "sync"
@@ -222,13 +192,12 @@ services:
depends_on: depends_on:
clickhouse: clickhouse:
condition: service_healthy condition: service_healthy
# clickhouse-2: # clickhouse-2:
# condition: service_healthy # condition: service_healthy
# clickhouse-3: # clickhouse-3:
# condition: service_healthy # condition: service_healthy
otel-collector-migrator-async: otel-collector-migrator-async:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.14} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.16}
container_name: otel-migrator-async container_name: otel-migrator-async
command: command:
- "async" - "async"
@@ -239,21 +208,14 @@ services:
condition: service_healthy condition: service_healthy
otel-collector-migrator-sync: otel-collector-migrator-sync:
condition: service_completed_successfully condition: service_completed_successfully
# clickhouse-2: # clickhouse-2:
# condition: service_healthy # condition: service_healthy
# clickhouse-3: # clickhouse-3:
# condition: service_healthy # condition: service_healthy
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.14} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.16}
container_name: signoz-otel-collector container_name: signoz-otel-collector
command: command: ["--config=/etc/otel-collector-config.yaml", "--manager-config=/etc/manager-config.yaml", "--copy-path=/var/tmp/collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
[
"--config=/etc/otel-collector-config.yaml",
"--manager-config=/etc/manager-config.yaml",
"--copy-path=/var/tmp/collector-config.yaml",
"--feature-gates=-pkg.translator.prometheus.NormalizeName"
]
user: root # required for reading docker container logs user: root # required for reading docker container logs
volumes: volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
@@ -283,7 +245,6 @@ services:
condition: service_completed_successfully condition: service_completed_successfully
query-service: query-service:
condition: service_healthy condition: service_healthy
logspout: logspout:
image: "gliderlabs/logspout:v3.2.14" image: "gliderlabs/logspout:v3.2.14"
container_name: signoz-logspout container_name: signoz-logspout

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.65.0}
container_name: signoz-query-service container_name: signoz-query-service
command: command: ["-config=/root/config/prometheus.yml", "-gateway-url=https://api.staging.signoz.cloud", "--use-logs-new-schema=true", "--use-trace-new-schema=true"]
[
"-config=/root/config/prometheus.yml",
"-gateway-url=https://api.staging.signoz.cloud",
"--use-logs-new-schema=true"
]
# ports: # ports:
# - "6060:6060" # pprof port # - "6060:6060" # pprof port
# - "8080:8080" # query-service port # - "8080:8080" # query-service port
@@ -194,21 +170,13 @@ services:
- KAFKA_SPAN_EVAL=${KAFKA_SPAN_EVAL:-false} - KAFKA_SPAN_EVAL=${KAFKA_SPAN_EVAL:-false}
restart: on-failure restart: on-failure
healthcheck: healthcheck:
test: test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/health"]
[
"CMD",
"wget",
"--spider",
"-q",
"localhost:8080/api/v1/health"
]
interval: 30s interval: 30s
timeout: 5s timeout: 5s
retries: 3 retries: 3
<<: *db-depend !!merge <<: *db-depend
frontend: frontend:
image: signoz/frontend:${DOCKER_TAG:-0.61.0} image: signoz/frontend:${DOCKER_TAG:-0.65.0}
container_name: signoz-frontend container_name: signoz-frontend
restart: on-failure restart: on-failure
depends_on: depends_on:
@@ -218,31 +186,22 @@ services:
- "3301:3301" - "3301:3301"
volumes: volumes:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.14} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.111.16}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
depends_on: depends_on:
clickhouse: clickhouse:
condition: service_healthy condition: service_healthy
# clickhouse-2: # clickhouse-2:
# condition: service_healthy # condition: service_healthy
# clickhouse-3: # clickhouse-3:
# condition: service_healthy # condition: service_healthy
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.14} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.111.16}
container_name: signoz-otel-collector container_name: signoz-otel-collector
command: command: ["--config=/etc/otel-collector-config.yaml", "--manager-config=/etc/manager-config.yaml", "--copy-path=/var/tmp/collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
[
"--config=/etc/otel-collector-config.yaml",
"--manager-config=/etc/manager-config.yaml",
"--copy-path=/var/tmp/collector-config.yaml",
"--feature-gates=-pkg.translator.prometheus.NormalizeName"
]
user: root # required for reading docker container logs user: root # required for reading docker container logs
volumes: volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
@@ -272,7 +231,6 @@ services:
condition: service_completed_successfully condition: service_completed_successfully
query-service: query-service:
condition: service_healthy condition: service_healthy
logspout: logspout:
image: "gliderlabs/logspout:v3.2.14" image: "gliderlabs/logspout:v3.2.14"
container_name: signoz-logspout container_name: signoz-logspout

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

@@ -41,7 +41,6 @@ type APIHandlerOptions struct {
FluxInterval time.Duration FluxInterval time.Duration
UseLogsNewSchema bool UseLogsNewSchema bool
UseTraceNewSchema bool UseTraceNewSchema bool
UseLicensesV3 bool
} }
type APIHandler struct { type APIHandler struct {
@@ -68,7 +67,6 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
FluxInterval: opts.FluxInterval, FluxInterval: opts.FluxInterval,
UseLogsNewSchema: opts.UseLogsNewSchema, UseLogsNewSchema: opts.UseLogsNewSchema,
UseTraceNewSchema: opts.UseTraceNewSchema, UseTraceNewSchema: opts.UseTraceNewSchema,
UseLicensesV3: opts.UseLicensesV3,
}) })
if err != nil { if err != nil {

View File

@@ -84,13 +84,6 @@ func (ah *APIHandler) listLicenses(w http.ResponseWriter, r *http.Request) {
} }
func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) { func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
if ah.UseLicensesV3 {
// if the licenses v3 is toggled on then do not apply license in v2 and run the validator!
// TODO: remove after migration to v3 and deprecation from zeus
zap.L().Info("early return from apply license v2 call")
render.Success(w, http.StatusOK, nil)
return
}
var l model.License var l model.License
if err := json.NewDecoder(r.Body).Decode(&l); err != nil { if err := json.NewDecoder(r.Body).Decode(&l); err != nil {
@@ -102,7 +95,7 @@ func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil) RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
return return
} }
license, apiError := ah.LM().Activate(r.Context(), l.Key) license, apiError := ah.LM().ActivateV3(r.Context(), l.Key)
if apiError != nil { if apiError != nil {
RespondError(w, apiError, nil) RespondError(w, apiError, nil)
return return
@@ -265,24 +258,12 @@ func convertLicenseV3ToLicenseV2(licenses []*model.LicenseV3) []model.License {
} }
func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) { func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
licensesV3, apierr := ah.LM().GetLicensesV3(r.Context())
var licenses []model.License if apierr != nil {
RespondError(w, apierr, nil)
if ah.UseLicensesV3 { return
licensesV3, err := ah.LM().GetLicensesV3(r.Context())
if err != nil {
RespondError(w, err, nil)
return
}
licenses = convertLicenseV3ToLicenseV2(licensesV3)
} else {
_licenses, apiError := ah.LM().GetLicenses(r.Context())
if apiError != nil {
RespondError(w, apiError, nil)
return
}
licenses = _licenses
} }
licenses := convertLicenseV3ToLicenseV2(licensesV3)
resp := model.Licenses{ resp := model.Licenses{
TrialStart: -1, TrialStart: -1,

View File

@@ -32,6 +32,7 @@ import (
baseauth "go.signoz.io/signoz/pkg/query-service/auth" baseauth "go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/migrate" "go.signoz.io/signoz/pkg/query-service/migrate"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3" v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/web"
licensepkg "go.signoz.io/signoz/ee/query-service/license" licensepkg "go.signoz.io/signoz/ee/query-service/license"
"go.signoz.io/signoz/ee/query-service/usage" "go.signoz.io/signoz/ee/query-service/usage"
@@ -78,7 +79,6 @@ type ServerOptions struct {
GatewayUrl string GatewayUrl string
UseLogsNewSchema bool UseLogsNewSchema bool
UseTraceNewSchema bool UseTraceNewSchema bool
UseLicensesV3 bool
} }
// Server runs HTTP api service // Server runs HTTP api service
@@ -108,7 +108,7 @@ func (s Server) HealthCheckStatus() chan healthcheck.Status {
} }
// NewServer creates and initializes Server // NewServer creates and initializes Server
func NewServer(serverOptions *ServerOptions) (*Server, error) { func NewServer(serverOptions *ServerOptions, web *web.Web) (*Server, error) {
modelDao, err := dao.InitDao("sqlite", baseconst.RELATIONAL_DATASOURCE_PATH) modelDao, err := dao.InitDao("sqlite", baseconst.RELATIONAL_DATASOURCE_PATH)
if err != nil { if err != nil {
@@ -135,7 +135,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
} }
// initiate license manager // initiate license manager
lm, err := licensepkg.StartManager("sqlite", localDB, serverOptions.UseLicensesV3) lm, err := licensepkg.StartManager("sqlite", localDB)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -274,7 +274,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
Gateway: gatewayProxy, Gateway: gatewayProxy,
UseLogsNewSchema: serverOptions.UseLogsNewSchema, UseLogsNewSchema: serverOptions.UseLogsNewSchema,
UseTraceNewSchema: serverOptions.UseTraceNewSchema, UseTraceNewSchema: serverOptions.UseTraceNewSchema,
UseLicensesV3: serverOptions.UseLicensesV3,
} }
apiHandler, err := api.NewAPIHandler(apiOpts) apiHandler, err := api.NewAPIHandler(apiOpts)
@@ -291,7 +290,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
usageManager: usageManager, usageManager: usageManager,
} }
httpServer, err := s.createPublicServer(apiHandler) httpServer, err := s.createPublicServer(apiHandler, web)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -340,7 +339,7 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
}, nil }, nil
} }
func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, error) { func (s *Server) createPublicServer(apiHandler *api.APIHandler, web *web.Web) (*http.Server, error) {
r := baseapp.NewRouter() r := baseapp.NewRouter()
@@ -384,6 +383,11 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
handler = handlers.CompressHandler(handler) handler = handlers.CompressHandler(handler)
err := web.AddToRouter(r)
if err != nil {
return nil, err
}
return &http.Server{ return &http.Server{
Handler: handler, Handler: handler,
}, nil }, nil

View File

@@ -2,18 +2,6 @@ package signozio
type status string type status string
type ActivationResult struct {
Status status `json:"status"`
Data *ActivationResponse `json:"data,omitempty"`
ErrorType string `json:"errorType,omitempty"`
Error string `json:"error,omitempty"`
}
type ActivationResponse struct {
ActivationId string `json:"ActivationId"`
PlanDetails string `json:"PlanDetails"`
}
type ValidateLicenseResponse struct { type ValidateLicenseResponse struct {
Status status `json:"status"` Status status `json:"status"`
Data map[string]interface{} `json:"data"` Data map[string]interface{} `json:"data"`

View File

@@ -10,7 +10,6 @@ import (
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"go.uber.org/zap"
"go.signoz.io/signoz/ee/query-service/constants" "go.signoz.io/signoz/ee/query-service/constants"
"go.signoz.io/signoz/ee/query-service/model" "go.signoz.io/signoz/ee/query-service/model"
@@ -39,86 +38,6 @@ func init() {
C = New() C = New()
} }
// ActivateLicense sends key to license.signoz.io and gets activation data
func ActivateLicense(key, siteId string) (*ActivationResponse, *model.ApiError) {
licenseReq := map[string]string{
"key": key,
"siteId": siteId,
}
reqString, _ := json.Marshal(licenseReq)
httpResponse, err := http.Post(C.Prefix+"/licenses/activate", APPLICATION_JSON, bytes.NewBuffer(reqString))
if err != nil {
zap.L().Error("failed to connect to license.signoz.io", zap.Error(err))
return nil, model.BadRequest(fmt.Errorf("unable to connect with license.signoz.io, please check your network connection"))
}
httpBody, err := io.ReadAll(httpResponse.Body)
if err != nil {
zap.L().Error("failed to read activation response from license.signoz.io", zap.Error(err))
return nil, model.BadRequest(fmt.Errorf("failed to read activation response from license.signoz.io"))
}
defer httpResponse.Body.Close()
// read api request result
result := ActivationResult{}
err = json.Unmarshal(httpBody, &result)
if err != nil {
zap.L().Error("failed to marshal activation response from license.signoz.io", zap.Error(err))
return nil, model.InternalError(errors.Wrap(err, "failed to marshal license activation response"))
}
switch httpResponse.StatusCode {
case 200, 201:
return result.Data, nil
case 400, 401:
return nil, model.BadRequest(fmt.Errorf(fmt.Sprintf("failed to activate: %s", result.Error)))
default:
return nil, model.InternalError(fmt.Errorf(fmt.Sprintf("failed to activate: %s", result.Error)))
}
}
// ValidateLicense validates the license key
func ValidateLicense(activationId string) (*ActivationResponse, *model.ApiError) {
validReq := map[string]string{
"activationId": activationId,
}
reqString, _ := json.Marshal(validReq)
response, err := http.Post(C.Prefix+"/licenses/validate", APPLICATION_JSON, bytes.NewBuffer(reqString))
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, "unable to connect with license.signoz.io, please check your network connection"))
}
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, "failed to read validation response from license.signoz.io"))
}
defer response.Body.Close()
switch response.StatusCode {
case 200, 201:
a := ActivationResult{}
err = json.Unmarshal(body, &a)
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, "failed to marshal license validation response"))
}
return a.Data, nil
case 400, 401:
return nil, model.BadRequest(errors.Wrap(fmt.Errorf(string(body)),
"bad request error received from license.signoz.io"))
default:
return nil, model.InternalError(errors.Wrap(fmt.Errorf(string(body)),
"internal error received from license.signoz.io"))
}
}
func ValidateLicenseV3(licenseKey string) (*model.LicenseV3, *model.ApiError) { func ValidateLicenseV3(licenseKey string) (*model.LicenseV3, *model.ApiError) {
// Creating an HTTP client with a timeout for better control // Creating an HTTP client with a timeout for better control

View File

@@ -18,15 +18,13 @@ import (
// Repo is license repo. stores license keys in a secured DB // Repo is license repo. stores license keys in a secured DB
type Repo struct { type Repo struct {
db *sqlx.DB db *sqlx.DB
useLicensesV3 bool
} }
// NewLicenseRepo initiates a new license repo // NewLicenseRepo initiates a new license repo
func NewLicenseRepo(db *sqlx.DB, useLicensesV3 bool) Repo { func NewLicenseRepo(db *sqlx.DB) Repo {
return Repo{ return Repo{
db: db, db: db,
useLicensesV3: useLicensesV3,
} }
} }
@@ -112,26 +110,16 @@ func (r *Repo) GetActiveLicenseV2(ctx context.Context) (*model.License, *basemod
// GetActiveLicense fetches the latest active license from DB. // GetActiveLicense fetches the latest active license from DB.
// If the license is not present, expect a nil license and a nil error in the output. // If the license is not present, expect a nil license and a nil error in the output.
func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel.ApiError) { func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel.ApiError) {
if r.useLicensesV3 { activeLicenseV3, err := r.GetActiveLicenseV3(ctx)
zap.L().Info("Using licenses v3 for GetActiveLicense")
activeLicenseV3, err := r.GetActiveLicenseV3(ctx)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
}
if activeLicenseV3 == nil {
return nil, nil
}
activeLicenseV2 := model.ConvertLicenseV3ToLicenseV2(activeLicenseV3)
return activeLicenseV2, nil
}
active, err := r.GetActiveLicenseV2(ctx)
if err != nil { if err != nil {
return nil, err return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
} }
return active, nil
if activeLicenseV3 == nil {
return nil, nil
}
activeLicenseV2 := model.ConvertLicenseV3ToLicenseV2(activeLicenseV3)
return activeLicenseV2, nil
} }
func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error) { func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error) {

View File

@@ -51,12 +51,12 @@ type Manager struct {
activeFeatures basemodel.FeatureSet activeFeatures basemodel.FeatureSet
} }
func StartManager(dbType string, db *sqlx.DB, useLicensesV3 bool, features ...basemodel.Feature) (*Manager, error) { func StartManager(dbType string, db *sqlx.DB, features ...basemodel.Feature) (*Manager, error) {
if LM != nil { if LM != nil {
return LM, nil return LM, nil
} }
repo := NewLicenseRepo(db, useLicensesV3) repo := NewLicenseRepo(db)
err := repo.InitDB(dbType) err := repo.InitDB(dbType)
if err != nil { if err != nil {
@@ -67,32 +67,7 @@ func StartManager(dbType string, db *sqlx.DB, useLicensesV3 bool, features ...ba
repo: &repo, repo: &repo,
} }
if useLicensesV3 { if err := m.start(features...); err != nil {
// get active license from the db
active, err := m.repo.GetActiveLicenseV2(context.Background())
if err != nil {
return m, err
}
// if we have an active license then need to fetch the complete details
if active != nil {
// fetch the new license structure from control plane
licenseV3, apiError := validate.ValidateLicenseV3(active.Key)
if apiError != nil {
return m, apiError
}
// insert the licenseV3 in sqlite db
apiError = m.repo.InsertLicenseV3(context.Background(), licenseV3)
// if the license already exists move ahead.
if apiError != nil && apiError.Typ != model.ErrorConflict {
return m, apiError
}
zap.L().Info("Successfully inserted license from v2 to v3 table")
}
}
if err := m.start(useLicensesV3, features...); err != nil {
return m, err return m, err
} }
LM = m LM = m
@@ -100,16 +75,8 @@ func StartManager(dbType string, db *sqlx.DB, useLicensesV3 bool, features ...ba
} }
// start loads active license in memory and initiates validator // start loads active license in memory and initiates validator
func (lm *Manager) start(useLicensesV3 bool, features ...basemodel.Feature) error { func (lm *Manager) start(features ...basemodel.Feature) error {
return lm.LoadActiveLicenseV3(features...)
var err error
if useLicensesV3 {
err = lm.LoadActiveLicenseV3(features...)
} else {
err = lm.LoadActiveLicense(features...)
}
return err
} }
func (lm *Manager) Stop() { func (lm *Manager) Stop() {
@@ -117,31 +84,6 @@ func (lm *Manager) Stop() {
<-lm.terminated <-lm.terminated
} }
func (lm *Manager) SetActive(l *model.License, features ...basemodel.Feature) {
lm.mutex.Lock()
defer lm.mutex.Unlock()
if l == nil {
return
}
lm.activeLicense = l
lm.activeFeatures = append(l.FeatureSet, features...)
// set default features
setDefaultFeatures(lm)
err := lm.InitFeatures(lm.activeFeatures)
if err != nil {
zap.L().Panic("Couldn't activate features", zap.Error(err))
}
if !lm.validatorRunning {
// we want to make sure only one validator runs,
// we already have lock() so good to go
lm.validatorRunning = true
go lm.Validator(context.Background())
}
}
func (lm *Manager) SetActiveV3(l *model.LicenseV3, features ...basemodel.Feature) { func (lm *Manager) SetActiveV3(l *model.LicenseV3, features ...basemodel.Feature) {
lm.mutex.Lock() lm.mutex.Lock()
defer lm.mutex.Unlock() defer lm.mutex.Unlock()
@@ -172,29 +114,6 @@ func setDefaultFeatures(lm *Manager) {
lm.activeFeatures = append(lm.activeFeatures, baseconstants.DEFAULT_FEATURE_SET...) lm.activeFeatures = append(lm.activeFeatures, baseconstants.DEFAULT_FEATURE_SET...)
} }
// LoadActiveLicense loads the most recent active license
func (lm *Manager) LoadActiveLicense(features ...basemodel.Feature) error {
active, err := lm.repo.GetActiveLicense(context.Background())
if err != nil {
return err
}
if active != nil {
lm.SetActive(active, features...)
} else {
zap.L().Info("No active license found, defaulting to basic plan")
// if no active license is found, we default to basic(free) plan with all default features
lm.activeFeatures = model.BasicPlan
setDefaultFeatures(lm)
err := lm.InitFeatures(lm.activeFeatures)
if err != nil {
zap.L().Error("Couldn't initialize features", zap.Error(err))
return err
}
}
return nil
}
func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error { func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error {
active, err := lm.repo.GetActiveLicenseV3(context.Background()) active, err := lm.repo.GetActiveLicenseV3(context.Background())
if err != nil { if err != nil {
@@ -265,31 +184,6 @@ func (lm *Manager) GetLicensesV3(ctx context.Context) (response []*model.License
return response, nil return response, nil
} }
// Validator validates license after an epoch of time
func (lm *Manager) Validator(ctx context.Context) {
zap.L().Info("Validator started!")
defer close(lm.terminated)
tick := time.NewTicker(validationFrequency)
defer tick.Stop()
lm.Validate(ctx)
for {
select {
case <-lm.done:
return
default:
select {
case <-lm.done:
return
case <-tick.C:
lm.Validate(ctx)
}
}
}
}
// Validator validates license after an epoch of time // Validator validates license after an epoch of time
func (lm *Manager) ValidatorV3(ctx context.Context) { func (lm *Manager) ValidatorV3(ctx context.Context) {
zap.L().Info("ValidatorV3 started!") zap.L().Info("ValidatorV3 started!")
@@ -315,73 +209,6 @@ func (lm *Manager) ValidatorV3(ctx context.Context) {
} }
} }
// Validate validates the current active license
func (lm *Manager) Validate(ctx context.Context) (reterr error) {
zap.L().Info("License validation started")
if lm.activeLicense == nil {
return nil
}
defer func() {
lm.mutex.Lock()
lm.lastValidated = time.Now().Unix()
if reterr != nil {
zap.L().Error("License validation completed with error", zap.Error(reterr))
atomic.AddUint64(&lm.failedAttempts, 1)
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED,
map[string]interface{}{"err": reterr.Error()}, "", true, false)
} else {
zap.L().Info("License validation completed with no errors")
}
lm.mutex.Unlock()
}()
response, apiError := validate.ValidateLicense(lm.activeLicense.ActivationId)
if apiError != nil {
zap.L().Error("failed to validate license", zap.Error(apiError.Err))
return apiError.Err
}
if response.PlanDetails == lm.activeLicense.PlanDetails {
// license plan hasnt changed, nothing to do
return nil
}
if response.PlanDetails != "" {
// copy and replace the active license record
l := model.License{
Key: lm.activeLicense.Key,
CreatedAt: lm.activeLicense.CreatedAt,
PlanDetails: response.PlanDetails,
ValidationMessage: lm.activeLicense.ValidationMessage,
ActivationId: lm.activeLicense.ActivationId,
}
if err := l.ParsePlan(); err != nil {
zap.L().Error("failed to parse updated license", zap.Error(err))
return err
}
// updated plan is parsable, check if plan has changed
if lm.activeLicense.PlanDetails != response.PlanDetails {
err := lm.repo.UpdatePlanDetails(ctx, lm.activeLicense.Key, response.PlanDetails)
if err != nil {
// unexpected db write issue but we can let the user continue
// and wait for update to work in next cycle.
zap.L().Error("failed to validate license", zap.Error(err))
}
}
// activate the update license plan
lm.SetActive(&l)
}
return nil
}
func (lm *Manager) RefreshLicense(ctx context.Context) *model.ApiError { func (lm *Manager) RefreshLicense(ctx context.Context) *model.ApiError {
license, apiError := validate.ValidateLicenseV3(lm.activeLicenseV3.Key) license, apiError := validate.ValidateLicenseV3(lm.activeLicenseV3.Key)
@@ -429,50 +256,6 @@ func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) {
return nil return nil
} }
// Activate activates a license key with signoz server
func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *model.License, errResponse *model.ApiError) {
defer func() {
if errResponse != nil {
userEmail, err := auth.GetEmailFromJwt(ctx)
if err == nil {
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED,
map[string]interface{}{"err": errResponse.Err.Error()}, userEmail, true, false)
}
}
}()
response, apiError := validate.ActivateLicense(key, "")
if apiError != nil {
zap.L().Error("failed to activate license", zap.Error(apiError.Err))
return nil, apiError
}
l := &model.License{
Key: key,
ActivationId: response.ActivationId,
PlanDetails: response.PlanDetails,
}
// parse validity and features from the plan details
err := l.ParsePlan()
if err != nil {
zap.L().Error("failed to activate license", zap.Error(err))
return nil, model.InternalError(err)
}
// store the license before activating it
err = lm.repo.InsertLicense(ctx, l)
if err != nil {
zap.L().Error("failed to activate license", zap.Error(err))
return nil, model.InternalError(err)
}
// license is valid, activate it
lm.SetActive(l)
return l, nil
}
func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (licenseResponse *model.LicenseV3, errResponse *model.ApiError) { func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (licenseResponse *model.LicenseV3, errResponse *model.ApiError) {
defer func() { defer func() {
if errResponse != nil { if errResponse != nil {

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"
signozweb "go.signoz.io/signoz/pkg/web"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
@@ -95,7 +99,6 @@ func main() {
var useLogsNewSchema bool var useLogsNewSchema bool
var useTraceNewSchema bool var useTraceNewSchema bool
var useLicensesV3 bool
var cacheConfigPath, fluxInterval string var cacheConfigPath, fluxInterval string
var enableQueryServiceLogOTLPExport bool var enableQueryServiceLogOTLPExport bool
var preferSpanMetrics bool var preferSpanMetrics bool
@@ -104,10 +107,10 @@ func main() {
var maxOpenConns int var maxOpenConns int
var dialTimeout time.Duration var dialTimeout time.Duration
var gatewayUrl string var gatewayUrl string
var useLicensesV3 bool
flag.BoolVar(&useLogsNewSchema, "use-logs-new-schema", false, "use logs_v2 schema for logs") flag.BoolVar(&useLogsNewSchema, "use-logs-new-schema", false, "use logs_v2 schema for logs")
flag.BoolVar(&useTraceNewSchema, "use-trace-new-schema", false, "use new schema for traces") flag.BoolVar(&useTraceNewSchema, "use-trace-new-schema", false, "use new schema for traces")
flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses")
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)") flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)") flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)")
flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)") flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
@@ -121,6 +124,7 @@ func main() {
flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)") flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)")
flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')") flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')")
flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)") flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)")
flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses")
flag.Parse() flag.Parse()
@@ -131,6 +135,23 @@ func main() {
version.PrintVersion() version.PrintVersion()
config, err := signozconfig.New(context.Background(), signozconfig.ProviderSettings{
ResolverSettings: confmap.ResolverSettings{
URIs: []string{"signozenv:"},
ProviderFactories: []confmap.ProviderFactory{
signozenvprovider.NewFactory(),
},
},
})
if err != nil {
zap.L().Fatal("Failed to create config", zap.Error(err))
}
web, err := signozweb.New(zap.L(), config.Web)
if err != nil {
zap.L().Fatal("Failed to create web", zap.Error(err))
}
serverOptions := &app.ServerOptions{ serverOptions := &app.ServerOptions{
HTTPHostPort: baseconst.HTTPHostPort, HTTPHostPort: baseconst.HTTPHostPort,
PromConfigPath: promConfigPath, PromConfigPath: promConfigPath,
@@ -148,7 +169,6 @@ func main() {
GatewayUrl: gatewayUrl, GatewayUrl: gatewayUrl,
UseLogsNewSchema: useLogsNewSchema, UseLogsNewSchema: useLogsNewSchema,
UseTraceNewSchema: useTraceNewSchema, UseTraceNewSchema: useTraceNewSchema,
UseLicensesV3: useLicensesV3,
} }
// Read the jwt secret key // Read the jwt secret key
@@ -166,7 +186,7 @@ func main() {
zap.L().Info("Migration successful") zap.L().Info("Migration successful")
} }
server, err := app.NewServer(serverOptions) server, err := app.NewServer(serverOptions, web)
if err != nil { if err != nil {
zap.L().Fatal("Failed to create server", zap.Error(err)) zap.L().Fatal("Failed to create server", zap.Error(err))
} }

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

@@ -242,6 +242,7 @@
"xml2js": "0.5.0", "xml2js": "0.5.0",
"phin": "^3.7.1", "phin": "^3.7.1",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"http-proxy-middleware": "3.0.3" "http-proxy-middleware": "3.0.3",
"cross-spawn": "7.0.5"
} }
} }

View File

@@ -3,7 +3,7 @@
"alert_channels": "Alert Channels", "alert_channels": "Alert Channels",
"organization_settings": "Organization Settings", "organization_settings": "Organization Settings",
"ingestion_settings": "Ingestion Settings", "ingestion_settings": "Ingestion Settings",
"api_keys": "Access Tokens", "api_keys": "API Keys",
"my_settings": "My Settings", "my_settings": "My Settings",
"overview_metrics": "Overview Metrics", "overview_metrics": "Overview Metrics",
"dbcall_metrics": "Database Calls", "dbcall_metrics": "Database Calls",

View File

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

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,7 +3,7 @@
"alert_channels": "Alert Channels", "alert_channels": "Alert Channels",
"organization_settings": "Organization Settings", "organization_settings": "Organization Settings",
"ingestion_settings": "Ingestion Settings", "ingestion_settings": "Ingestion Settings",
"api_keys": "Access Tokens", "api_keys": "API Keys",
"my_settings": "My Settings", "my_settings": "My Settings",
"overview_metrics": "Overview Metrics", "overview_metrics": "Overview Metrics",
"dbcall_metrics": "Database Calls", "dbcall_metrics": "Database Calls",

View File

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

View File

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

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

View File

@@ -427,24 +427,27 @@ export const LIST_LICENSES: AppRoutes = {
export const oldRoutes = [ export const oldRoutes = [
'/pipelines', '/pipelines',
'/logs/old-logs-explorer',
'/logs-explorer', '/logs-explorer',
'/logs-explorer/live', '/logs-explorer/live',
'/logs-save-views', '/logs-save-views',
'/traces-save-views', '/traces-save-views',
'/settings/api-keys', '/settings/access-tokens',
]; ];
export const oldNewRoutesMapping: Record<string, string> = { export const oldNewRoutesMapping: Record<string, string> = {
'/pipelines': '/logs/pipelines', '/pipelines': '/logs/pipelines',
'/logs/old-logs-explorer': '/logs/old-logs-explorer',
'/logs-explorer': '/logs/logs-explorer', '/logs-explorer': '/logs/logs-explorer',
'/logs-explorer/live': '/logs/logs-explorer/live', '/logs-explorer/live': '/logs/logs-explorer/live',
'/logs-save-views': '/logs/saved-views', '/logs-save-views': '/logs/saved-views',
'/traces-save-views': '/traces/saved-views', '/traces-save-views': '/traces/saved-views',
'/settings/api-keys': '/settings/access-tokens', '/settings/access-tokens': '/settings/api-keys',
}; };
export const ROUTES_NOT_TO_BE_OVERRIDEN: string[] = [
ROUTES.WORKSPACE_LOCKED,
ROUTES.WORKSPACE_SUSPENDED,
];
export interface AppRoutes { export interface AppRoutes {
component: RouteProps['component']; component: RouteProps['component'];
path: RouteProps['path']; path: RouteProps['path'];

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

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

View File

@@ -1,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

@@ -40,7 +40,7 @@
&.custom-time { &.custom-time {
input:not(:focus) { input:not(:focus) {
min-width: 240px; min-width: 280px;
} }
} }
@@ -119,3 +119,69 @@
color: var(--bg-slate-400) !important; color: var(--bg-slate-400) !important;
} }
} }
.date-time-popover__footer {
border-top: 1px solid var(--bg-ink-200);
padding: 8px 14px;
.timezone-container {
&,
.timezone {
font-family: Inter;
font-size: 12px;
line-height: 16px;
letter-spacing: -0.06px;
}
display: flex;
align-items: center;
color: var(--bg-vanilla-400);
gap: 6px;
.timezone {
display: flex;
align-items: center;
gap: 4px;
border-radius: 2px;
background: rgba(171, 189, 255, 0.04);
cursor: pointer;
padding: 0px 4px;
color: var(--bg-vanilla-100);
border: none;
}
}
}
.timezone-badge {
display: flex;
align-items: center;
justify-content: center;
padding: 0 4px;
border-radius: 2px;
background: rgba(171, 189, 255, 0.04);
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 400;
line-height: 16px;
letter-spacing: -0.06px;
cursor: pointer;
}
.lightMode {
.date-time-popover__footer {
border-color: var(--bg-vanilla-400);
}
.timezone-container {
color: var(--bg-ink-400);
&__clock-icon {
stroke: var(--bg-ink-400);
}
.timezone {
color: var(--bg-ink-100);
background: rgb(179 179 179 / 15%);
&__icon {
stroke: var(--bg-ink-100);
}
}
}
.timezone-badge {
color: var(--bg-ink-100);
background: rgb(179 179 179 / 15%);
}
}

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 {
@@ -15,11 +16,14 @@ import { isValidTimeFormat } from 'lib/getMinMax';
import { defaultTo, isFunction, noop } from 'lodash-es'; import { defaultTo, isFunction, noop } from 'lodash-es';
import debounce from 'lodash-es/debounce'; import debounce from 'lodash-es/debounce';
import { CheckCircle, ChevronDown, Clock } from 'lucide-react'; import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { import {
ChangeEvent, ChangeEvent,
Dispatch, Dispatch,
SetStateAction, SetStateAction,
useCallback,
useEffect, useEffect,
useMemo,
useState, useState,
} from 'react'; } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
@@ -28,6 +32,8 @@ import { popupContainer } from 'utils/selectPopupContainer';
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent'; import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
const maxAllowedMinTimeInMonths = 6; const maxAllowedMinTimeInMonths = 6;
type ViewType = 'datetime' | 'timezone';
const DEFAULT_VIEW: ViewType = 'datetime';
interface CustomTimePickerProps { interface CustomTimePickerProps {
onSelect: (value: string) => void; onSelect: (value: string) => void;
@@ -81,11 +87,42 @@ function CustomTimePicker({
const location = useLocation(); const location = useLocation();
const [isInputFocused, setIsInputFocused] = useState(false); const [isInputFocused, setIsInputFocused] = useState(false);
const [activeView, setActiveView] = useState<ViewType>(DEFAULT_VIEW);
const { timezone, browserTimezone } = useTimezone();
const activeTimezoneOffset = timezone.offset;
const isTimezoneOverridden = useMemo(
() => timezone.offset !== browserTimezone.offset,
[timezone, browserTimezone],
);
const handleViewChange = useCallback(
(newView: 'timezone' | 'datetime'): void => {
if (activeView !== newView) {
setActiveView(newView);
}
setOpen(true);
},
[activeView, setOpen],
);
const [isOpenedFromFooter, setIsOpenedFromFooter] = useState(false);
const getSelectedTimeRangeLabel = ( const getSelectedTimeRangeLabel = (
selectedTime: string, selectedTime: string,
selectedTimeValue: string, selectedTimeValue: string,
): string => { ): string => {
if (selectedTime === 'custom') { if (selectedTime === 'custom') {
// Convert the date range string to 12-hour format
const dates = selectedTimeValue.split(' - ');
if (dates.length === 2) {
const startDate = dayjs(dates[0], 'DD/MM/YYYY HH:mm');
const endDate = dayjs(dates[1], 'DD/MM/YYYY HH:mm');
return `${startDate.format('DD/MM/YYYY hh:mm A')} - ${endDate.format(
'DD/MM/YYYY hh:mm A',
)}`;
}
return selectedTimeValue; return selectedTimeValue;
} }
@@ -131,6 +168,7 @@ function CustomTimePicker({
setOpen(newOpen); setOpen(newOpen);
if (!newOpen) { if (!newOpen) {
setCustomDTPickerVisible?.(false); setCustomDTPickerVisible?.(false);
setActiveView('datetime');
} }
}; };
@@ -244,6 +282,7 @@ function CustomTimePicker({
const handleFocus = (): void => { const handleFocus = (): void => {
setIsInputFocused(true); setIsInputFocused(true);
setActiveView('datetime');
}; };
const handleBlur = (): void => { const handleBlur = (): void => {
@@ -259,6 +298,18 @@ function CustomTimePicker({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname]); }, [location.pathname]);
const handleTimezoneHintClick = (e: React.MouseEvent): void => {
e.stopPropagation();
handleViewChange('timezone');
setIsOpenedFromFooter(false);
logEvent(
'DateTimePicker: Timezone picker opened from time range input badge',
{
page: location.pathname,
},
);
};
return ( return (
<div className="custom-time-picker"> <div className="custom-time-picker">
<Popover <Popover
@@ -280,6 +331,10 @@ function CustomTimePicker({
handleGoLive={defaultTo(handleGoLive, noop)} handleGoLive={defaultTo(handleGoLive, noop)}
options={items} options={items}
selectedTime={selectedTime} selectedTime={selectedTime}
activeView={activeView}
setActiveView={setActiveView}
setIsOpenedFromFooter={setIsOpenedFromFooter}
isOpenedFromFooter={isOpenedFromFooter}
/> />
) : ( ) : (
content content
@@ -316,12 +371,17 @@ function CustomTimePicker({
) )
} }
suffix={ suffix={
<ChevronDown <>
size={14} {!!isTimezoneOverridden && activeTimezoneOffset && (
onClick={(): void => { <div className="timezone-badge" onClick={handleTimezoneHintClick}>
setOpen(!open); <span>{activeTimezoneOffset}</span>
}} </div>
/> )}
<ChevronDown
size={14}
onClick={(): void => handleViewChange('datetime')}
/>
</>
} }
/> />
</Popover> </Popover>

View File

@@ -1,6 +1,8 @@
import './CustomTimePicker.styles.scss'; import './CustomTimePicker.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd'; import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames'; import cx from 'classnames';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal'; import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
@@ -9,10 +11,13 @@ import {
Option, Option,
RelativeDurationSuggestionOptions, RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/config'; } from 'container/TopNav/DateTimeSelectionV2/config';
import { Clock, PenLine } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { Dispatch, SetStateAction, useMemo } from 'react'; import { Dispatch, SetStateAction, useMemo } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import RangePickerModal from './RangePickerModal'; import RangePickerModal from './RangePickerModal';
import TimezonePicker from './TimezonePicker';
interface CustomTimePickerPopoverContentProps { interface CustomTimePickerPopoverContentProps {
options: any[]; options: any[];
@@ -26,8 +31,13 @@ interface CustomTimePickerPopoverContentProps {
onSelectHandler: (label: string, value: string) => void; onSelectHandler: (label: string, value: string) => void;
handleGoLive: () => void; handleGoLive: () => void;
selectedTime: string; selectedTime: string;
activeView: 'datetime' | 'timezone';
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
isOpenedFromFooter: boolean;
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
} }
// eslint-disable-next-line sonarjs/cognitive-complexity
function CustomTimePickerPopoverContent({ function CustomTimePickerPopoverContent({
options, options,
setIsOpen, setIsOpen,
@@ -37,12 +47,18 @@ function CustomTimePickerPopoverContent({
onSelectHandler, onSelectHandler,
handleGoLive, handleGoLive,
selectedTime, selectedTime,
activeView,
setActiveView,
isOpenedFromFooter,
setIsOpenedFromFooter,
}: CustomTimePickerPopoverContentProps): JSX.Element { }: CustomTimePickerPopoverContentProps): JSX.Element {
const { pathname } = useLocation(); const { pathname } = useLocation();
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [ const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
pathname, pathname,
]); ]);
const { timezone } = useTimezone();
const activeTimezoneOffset = timezone.offset;
function getTimeChips(options: Option[]): JSX.Element { function getTimeChips(options: Option[]): JSX.Element {
return ( return (
@@ -63,55 +79,105 @@ function CustomTimePickerPopoverContent({
); );
} }
const handleTimezoneHintClick = (): void => {
setActiveView('timezone');
setIsOpenedFromFooter(true);
logEvent(
'DateTimePicker: Timezone picker opened from time range picker footer',
{
page: pathname,
},
);
};
if (activeView === 'timezone') {
return (
<div className="date-time-popover">
<TimezonePicker
setActiveView={setActiveView}
setIsOpen={setIsOpen}
isOpenedFromFooter={isOpenedFromFooter}
/>
</div>
);
}
return ( return (
<div className="date-time-popover"> <>
<div className="date-time-options"> <div className="date-time-popover">
{isLogsExplorerPage && ( <div className="date-time-options">
<Button className="data-time-live" type="text" onClick={handleGoLive}> {isLogsExplorerPage && (
Live <Button className="data-time-live" type="text" onClick={handleGoLive}>
</Button> Live
)} </Button>
{options.map((option) => ( )}
<Button {options.map((option) => (
type="text" <Button
key={option.label + option.value} type="text"
onClick={(): void => { key={option.label + option.value}
onSelectHandler(option.label, option.value); onClick={(): void => {
}} onSelectHandler(option.label, option.value);
className={cx( }}
'date-time-options-btn', className={cx(
customDateTimeVisible 'date-time-options-btn',
? option.value === 'custom' && 'active' customDateTimeVisible
: selectedTime === option.value && 'active', ? option.value === 'custom' && 'active'
)} : selectedTime === option.value && 'active',
> )}
{option.label} >
</Button> {option.label}
))} </Button>
))}
</div>
<div
className={cx(
'relative-date-time',
selectedTime === 'custom' || customDateTimeVisible
? 'date-picker'
: 'relative-times',
)}
>
{selectedTime === 'custom' || customDateTimeVisible ? (
<RangePickerModal
setCustomDTPickerVisible={setCustomDTPickerVisible}
setIsOpen={setIsOpen}
onCustomDateHandler={onCustomDateHandler}
selectedTime={selectedTime}
/>
) : (
<div className="relative-times-container">
<div className="time-heading">RELATIVE TIMES</div>
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div>
)}
</div>
</div> </div>
<div
className={cx( <div className="date-time-popover__footer">
'relative-date-time', <div className="timezone-container">
selectedTime === 'custom' || customDateTimeVisible <Clock
? 'date-picker' color={Color.BG_VANILLA_400}
: 'relative-times', className="timezone-container__clock-icon"
)} height={12}
> width={12}
{selectedTime === 'custom' || customDateTimeVisible ? (
<RangePickerModal
setCustomDTPickerVisible={setCustomDTPickerVisible}
setIsOpen={setIsOpen}
onCustomDateHandler={onCustomDateHandler}
selectedTime={selectedTime}
/> />
) : ( <span className="timezone__icon">Current timezone</span>
<div className="relative-times-container"> <div></div>
<div className="time-heading">RELATIVE TIMES</div> <button
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div> type="button"
</div> className="timezone"
)} onClick={handleTimezoneHintClick}
>
<span>{activeTimezoneOffset}</span>
<PenLine
color={Color.BG_VANILLA_100}
className="timezone__icon"
size={10}
/>
</button>
</div>
</div> </div>
</div> </>
); );
} }

View File

@@ -4,7 +4,8 @@ import { DatePicker } from 'antd';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal'; import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config'; import { LexicalContext } from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import { Dispatch, SetStateAction } from 'react'; import { useTimezone } from 'providers/Timezone';
import { Dispatch, SetStateAction, useMemo } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
@@ -31,7 +32,10 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
(state) => state.globalTime, (state) => state.globalTime,
); );
const disabledDate = (current: Dayjs): boolean => { // Using any type here because antd's DatePicker expects its own internal Dayjs type
// which conflicts with our project's Dayjs type that has additional plugins (tz, utc etc).
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
const disabledDate = (current: any): boolean => {
const currentDay = dayjs(current); const currentDay = dayjs(current);
return currentDay.isAfter(dayjs()); return currentDay.isAfter(dayjs());
}; };
@@ -49,16 +53,32 @@ function RangePickerModal(props: RangePickerModalProps): JSX.Element {
} }
onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER); onCustomDateHandler(date_time, LexicalContext.CUSTOM_DATE_PICKER);
}; };
const { timezone } = useTimezone();
const rangeValue: [Dayjs, Dayjs] = useMemo(
() => [
dayjs(minTime / 1000_000).tz(timezone.value),
dayjs(maxTime / 1000_000).tz(timezone.value),
],
[maxTime, minTime, timezone.value],
);
return ( return (
<div className="custom-date-picker"> <div className="custom-date-picker">
<RangePicker <RangePicker
disabledDate={disabledDate} disabledDate={disabledDate}
allowClear allowClear
showTime showTime={{
use12Hours: true,
format: 'hh:mm A',
}}
format={(date: Dayjs): string =>
date.tz(timezone.value).format('YYYY-MM-DD hh:mm A')
}
onOk={onModalOkHandler} onOk={onModalOkHandler}
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
{...(selectedTime === 'custom' && { {...(selectedTime === 'custom' && {
defaultValue: [dayjs(minTime / 1000000), dayjs(maxTime / 1000000)], value: rangeValue,
})} })}
/> />
</div> </div>

View File

@@ -0,0 +1,166 @@
// Variables
$font-family: 'Inter';
$item-spacing: 8px;
:root {
--border-color: var(--bg-slate-400);
}
.lightMode {
--border-color: var(--bg-vanilla-400);
}
// Mixins
@mixin text-style-base {
font-family: $font-family;
font-style: normal;
font-weight: 400;
}
@mixin flex-center {
display: flex;
align-items: center;
}
.timezone-picker {
width: 532px;
color: var(--bg-vanilla-400);
font-family: $font-family;
&__search {
@include flex-center;
justify-content: space-between;
padding: 12px 14px;
border-bottom: 1px solid var(--border-color);
}
&__input-container {
@include flex-center;
gap: 6px;
width: -webkit-fill-available;
}
&__input {
@include text-style-base;
width: 100%;
background: transparent;
border: none;
outline: none;
color: var(--bg-vanilla-100);
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
padding: 0;
&.ant-input:focus {
box-shadow: none;
}
&::placeholder {
color: var(--bg-vanilla-400);
}
}
&__esc-key {
@include text-style-base;
font-size: 8px;
color: var(--bg-vanilla-400);
letter-spacing: -0.04px;
border-radius: 2.286px;
border: 1.143px solid var(--bg-ink-200);
border-bottom-width: 2.286px;
background: var(--bg-ink-400);
padding: 0 1px;
}
&__list {
max-height: 310px;
overflow-y: auto;
}
&__item {
@include flex-center;
justify-content: space-between;
padding: 7.5px 6px 7.5px $item-spacing;
margin: 4px $item-spacing;
cursor: pointer;
background: transparent;
border: none;
width: -webkit-fill-available;
color: var(--bg-vanilla-400);
font-family: $font-family;
&:hover,
&.selected {
border-radius: 2px;
background: rgba(171, 189, 255, 0.04);
color: var(--bg-vanilla-100);
}
&.has-divider {
position: relative;
&::after {
content: '';
position: absolute;
bottom: -2px;
left: -$item-spacing;
right: -$item-spacing;
border-bottom: 1px solid var(--border-color);
}
}
}
&__name {
@include text-style-base;
font-size: 14px;
line-height: 20px;
letter-spacing: -0.07px;
}
&__offset {
color: var(--bg-vanilla-100);
font-size: 12px;
line-height: 16px;
letter-spacing: -0.06px;
}
}
.timezone-name-wrapper {
@include flex-center;
gap: 6px;
&__selected-icon {
height: 15px;
width: 15px;
}
}
.lightMode {
.timezone-picker {
&__search {
.search-icon {
stroke: var(--bg-ink-400);
}
}
&__input {
color: var(--bg-ink-100);
}
&__esc-key {
background-color: var(--bg-vanilla-100);
border-color: var(--bg-vanilla-400);
color: var(--bg-ink-400);
}
&__item {
color: var(--bg-ink-400);
}
&__offset {
color: var(--bg-ink-100);
}
}
.timezone-name-wrapper {
&__selected-icon {
.check-icon {
stroke: var(--bg-ink-100);
}
}
}
}

View File

@@ -0,0 +1,208 @@
import './TimezonePicker.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Input } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { TimezonePickerShortcuts } from 'constants/shortcuts/TimezonePickerShortcuts';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { Check, Search } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useState,
} from 'react';
import { Timezone, TIMEZONE_DATA } from './timezoneUtils';
interface SearchBarProps {
value: string;
onChange: (value: string) => void;
setIsOpen: Dispatch<SetStateAction<boolean>>;
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
isOpenedFromFooter: boolean;
}
interface TimezoneItemProps {
timezone: Timezone;
isSelected?: boolean;
onClick?: () => void;
}
const ICON_SIZE = 14;
function SearchBar({
value,
onChange,
setIsOpen,
setActiveView,
isOpenedFromFooter = false,
}: SearchBarProps): JSX.Element {
const handleKeyDown = useCallback(
(e: React.KeyboardEvent): void => {
if (e.key === 'Escape') {
if (isOpenedFromFooter) {
setActiveView('datetime');
} else {
setIsOpen(false);
}
}
},
[setActiveView, setIsOpen, isOpenedFromFooter],
);
return (
<div className="timezone-picker__search">
<div className="timezone-picker__input-container">
<Search
color={Color.BG_VANILLA_400}
className="search-icon"
height={ICON_SIZE}
width={ICON_SIZE}
/>
<Input
type="text"
className="timezone-picker__input"
placeholder="Search timezones..."
value={value}
onChange={(e): void => onChange(e.target.value)}
onKeyDown={handleKeyDown}
tabIndex={0}
autoFocus
/>
</div>
<kbd className="timezone-picker__esc-key">esc</kbd>
</div>
);
}
function TimezoneItem({
timezone,
isSelected = false,
onClick,
}: TimezoneItemProps): JSX.Element {
return (
<button
type="button"
className={cx('timezone-picker__item', {
selected: isSelected,
'has-divider': timezone.hasDivider,
})}
onClick={onClick}
>
<div className="timezone-name-wrapper">
<div className="timezone-name-wrapper__selected-icon">
{isSelected && (
<Check
className="check-icon"
color={Color.BG_VANILLA_100}
height={ICON_SIZE}
width={ICON_SIZE}
/>
)}
</div>
<div className="timezone-picker__name">{timezone.name}</div>
</div>
<div className="timezone-picker__offset">{timezone.offset}</div>
</button>
);
}
TimezoneItem.defaultProps = {
isSelected: false,
onClick: undefined,
};
interface TimezonePickerProps {
setActiveView: Dispatch<SetStateAction<'datetime' | 'timezone'>>;
setIsOpen: Dispatch<SetStateAction<boolean>>;
isOpenedFromFooter: boolean;
}
function TimezonePicker({
setActiveView,
setIsOpen,
isOpenedFromFooter,
}: TimezonePickerProps): JSX.Element {
console.log({ isOpenedFromFooter });
const [searchTerm, setSearchTerm] = useState('');
const { timezone, updateTimezone } = useTimezone();
const [selectedTimezone, setSelectedTimezone] = useState<string>(
timezone.name ?? TIMEZONE_DATA[0].name,
);
const getFilteredTimezones = useCallback((searchTerm: string): Timezone[] => {
const normalizedSearch = searchTerm.toLowerCase();
return TIMEZONE_DATA.filter(
(tz) =>
tz.name.toLowerCase().includes(normalizedSearch) ||
tz.offset.toLowerCase().includes(normalizedSearch) ||
tz.searchIndex.toLowerCase().includes(normalizedSearch),
);
}, []);
const handleCloseTimezonePicker = useCallback(() => {
if (isOpenedFromFooter) {
setActiveView('datetime');
} else {
setIsOpen(false);
}
}, [isOpenedFromFooter, setActiveView, setIsOpen]);
const handleTimezoneSelect = useCallback(
(timezone: Timezone) => {
setSelectedTimezone(timezone.name);
updateTimezone(timezone);
handleCloseTimezonePicker();
setIsOpen(false);
logEvent('DateTimePicker: New Timezone Selected', {
timezone: {
name: timezone.name,
offset: timezone.offset,
},
});
},
[handleCloseTimezonePicker, setIsOpen, updateTimezone],
);
// Register keyboard shortcuts
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
useEffect(() => {
registerShortcut(
TimezonePickerShortcuts.CloseTimezonePicker,
handleCloseTimezonePicker,
);
return (): void => {
deregisterShortcut(TimezonePickerShortcuts.CloseTimezonePicker);
};
}, [deregisterShortcut, handleCloseTimezonePicker, registerShortcut]);
return (
<div className="timezone-picker">
<SearchBar
value={searchTerm}
onChange={setSearchTerm}
setIsOpen={setIsOpen}
setActiveView={setActiveView}
isOpenedFromFooter={isOpenedFromFooter}
/>
<div className="timezone-picker__list">
{getFilteredTimezones(searchTerm).map((timezone) => (
<TimezoneItem
key={timezone.value}
timezone={timezone}
isSelected={timezone.name === selectedTimezone}
onClick={(): void => handleTimezoneSelect(timezone)}
/>
))}
</div>
</div>
);
}
export default TimezonePicker;

View File

@@ -0,0 +1,152 @@
import dayjs from 'dayjs';
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);
dayjs.extend(timezone);
export interface Timezone {
name: string;
value: string;
offset: string;
searchIndex: string;
hasDivider?: boolean;
}
const TIMEZONE_TYPES = {
BROWSER: 'BROWSER',
UTC: 'UTC',
STANDARD: 'STANDARD',
} as const;
type TimezoneType = typeof TIMEZONE_TYPES[keyof typeof TIMEZONE_TYPES];
export const UTC_TIMEZONE: Timezone = {
name: 'Coordinated Universal Time — UTC, GMT',
value: 'UTC',
offset: 'UTC',
searchIndex: 'UTC',
hasDivider: true,
};
const normalizeTimezoneName = (timezone: string): string => {
// https://github.com/tc39/proposal-temporal/issues/1076
if (timezone === 'Asia/Calcutta') {
return 'Asia/Kolkata';
}
return timezone;
};
const formatOffset = (offsetMinutes: number): string => {
if (offsetMinutes === 0) return 'UTC';
const hours = Math.floor(Math.abs(offsetMinutes) / 60);
const minutes = Math.abs(offsetMinutes) % 60;
const sign = offsetMinutes > 0 ? '+' : '-';
return `UTC ${sign} ${hours}${
minutes ? `:${minutes.toString().padStart(2, '0')}` : ':00'
}`;
};
const createTimezoneEntry = (
name: string,
offsetMinutes: number,
type: TimezoneType = TIMEZONE_TYPES.STANDARD,
hasDivider = false,
): Timezone => {
const offset = formatOffset(offsetMinutes);
let value = name;
let displayName = name;
switch (type) {
case TIMEZONE_TYPES.BROWSER:
displayName = `Browser time — ${name}`;
value = name;
break;
case TIMEZONE_TYPES.UTC:
displayName = 'Coordinated Universal Time — UTC, GMT';
value = 'UTC';
break;
case TIMEZONE_TYPES.STANDARD:
displayName = name;
value = name;
break;
default:
console.error(`Invalid timezone type: ${type}`);
}
return {
name: displayName,
value,
offset,
searchIndex: offset.replace(/ /g, ''),
...(hasDivider && { hasDivider }),
};
};
const getOffsetByTimezone = (timezone: string): number => {
const dayjsTimezone = dayjs().tz(timezone);
return dayjsTimezone.utcOffset();
};
export const getBrowserTimezone = (): Timezone => {
const browserTz = dayjs.tz.guess();
const normalizedTz = normalizeTimezoneName(browserTz);
const browserOffset = getOffsetByTimezone(normalizedTz);
return createTimezoneEntry(
normalizedTz,
browserOffset,
TIMEZONE_TYPES.BROWSER,
);
};
const filterAndSortTimezones = (
allTimezones: string[],
browserTzName?: string,
includeEtcTimezones = false,
): Timezone[] =>
allTimezones
.filter((tz) => {
const isNotBrowserTz = tz !== browserTzName;
const isNotEtcTz = includeEtcTimezones || !tz.startsWith('Etc/');
return isNotBrowserTz && isNotEtcTz;
})
.sort((a, b) => a.localeCompare(b))
.map((tz) => {
const normalizedTz = normalizeTimezoneName(tz);
const offset = getOffsetByTimezone(normalizedTz);
return createTimezoneEntry(normalizedTz, offset);
});
const generateTimezoneData = (includeEtcTimezones = false): Timezone[] => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const allTimezones = (Intl as any).supportedValuesOf('timeZone');
const timezones: Timezone[] = [];
// Add browser timezone
const browserTzObject = getBrowserTimezone();
timezones.push(browserTzObject);
// Add UTC timezone with divider
timezones.push(UTC_TIMEZONE);
timezones.push(
...filterAndSortTimezones(
allTimezones,
browserTzObject.value,
includeEtcTimezones,
),
);
return timezones;
};
export const getTimezoneObjectByTimezoneString = (
timezone: string,
): Timezone => {
const utcOffset = getOffsetByTimezone(timezone);
return createTimezoneEntry(timezone, utcOffset);
};
export const TIMEZONE_DATA = generateTimezoneData();

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

@@ -1,4 +1,5 @@
import { import {
_adapters,
BarController, BarController,
BarElement, BarElement,
CategoryScale, CategoryScale,
@@ -18,8 +19,10 @@ import {
} from 'chart.js'; } from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation'; import annotationPlugin from 'chartjs-plugin-annotation';
import { generateGridTitle } from 'container/GridPanelSwitch/utils'; import { generateGridTitle } from 'container/GridPanelSwitch/utils';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import isEqual from 'lodash-es/isEqual'; import isEqual from 'lodash-es/isEqual';
import { useTimezone } from 'providers/Timezone';
import { import {
forwardRef, forwardRef,
memo, memo,
@@ -62,6 +65,17 @@ Chart.register(
Tooltip.positioners.custom = TooltipPositionHandler; Tooltip.positioners.custom = TooltipPositionHandler;
// Map of Chart.js time formats to dayjs format strings
const formatMap = {
'HH:mm:ss': 'HH:mm:ss',
'HH:mm': 'HH:mm',
'MM/DD HH:mm': 'MM/DD HH:mm',
'MM/dd HH:mm': 'MM/DD HH:mm',
'MM/DD': 'MM/DD',
'YY-MM': 'YY-MM',
YY: 'YY',
};
const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>( const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
( (
{ {
@@ -80,11 +94,13 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
dragSelectColor, dragSelectColor,
}, },
ref, ref,
// eslint-disable-next-line sonarjs/cognitive-complexity
): JSX.Element => { ): JSX.Element => {
const nearestDatasetIndex = useRef<null | number>(null); const nearestDatasetIndex = useRef<null | number>(null);
const chartRef = useRef<HTMLCanvasElement>(null); const chartRef = useRef<HTMLCanvasElement>(null);
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
const gridTitle = useMemo(() => generateGridTitle(title), [title]); const gridTitle = useMemo(() => generateGridTitle(title), [title]);
const { timezone } = useTimezone();
const currentTheme = isDarkMode ? 'dark' : 'light'; const currentTheme = isDarkMode ? 'dark' : 'light';
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
@@ -112,6 +128,22 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
return 'rgba(231,233,237,0.8)'; return 'rgba(231,233,237,0.8)';
}, [currentTheme]); }, [currentTheme]);
// Override Chart.js date adapter to use dayjs with timezone support
useEffect(() => {
_adapters._date.override({
format(time: number | Date, fmt: string) {
const dayjsTime = dayjs(time).tz(timezone.value);
const format = formatMap[fmt as keyof typeof formatMap];
if (!format) {
console.warn(`Missing datetime format for ${fmt}`);
return dayjsTime.format('YYYY-MM-DD HH:mm:ss'); // fallback format
}
return dayjsTime.format(format);
},
});
}, [timezone]);
const buildChart = useCallback(() => { const buildChart = useCallback(() => {
if (lineChartRef.current !== undefined) { if (lineChartRef.current !== undefined) {
lineChartRef.current.destroy(); lineChartRef.current.destroy();
@@ -132,6 +164,7 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
isStacked, isStacked,
onClickHandler, onClickHandler,
data, data,
timezone,
); );
const chartHasData = hasData(data); const chartHasData = hasData(data);
@@ -166,6 +199,7 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
isStacked, isStacked,
onClickHandler, onClickHandler,
data, data,
timezone,
name, name,
type, type,
]); ]);

View File

@@ -1,5 +1,6 @@
import { Chart, ChartConfiguration, ChartData, Color } from 'chart.js'; import { Chart, ChartConfiguration, ChartData, Color } from 'chart.js';
import * as chartjsAdapter from 'chartjs-adapter-date-fns'; import * as chartjsAdapter from 'chartjs-adapter-date-fns';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { MutableRefObject } from 'react'; import { MutableRefObject } from 'react';
@@ -50,6 +51,7 @@ export const getGraphOptions = (
isStacked: boolean | undefined, isStacked: boolean | undefined,
onClickHandler: GraphOnClickHandler | undefined, onClickHandler: GraphOnClickHandler | undefined,
data: ChartData, data: ChartData,
timezone: Timezone,
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
): CustomChartOptions => ({ ): CustomChartOptions => ({
animation: { animation: {
@@ -97,7 +99,7 @@ export const getGraphOptions = (
callbacks: { callbacks: {
title(context): string | string[] { title(context): string | string[] {
const date = dayjs(context[0].parsed.x); const date = dayjs(context[0].parsed.x);
return date.format('MMM DD, YYYY, HH:mm:ss'); return date.tz(timezone.value).format('MMM DD, YYYY, HH:mm:ss');
}, },
label(context): string | string[] { label(context): string | string[] {
let label = context.dataset.label || ''; let label = context.dataset.label || '';

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

@@ -8,13 +8,13 @@ import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants'; import { VIEW_TYPES } from 'components/LogDetail/constants';
import { unescapeString } from 'container/LogDetailedView/utils'; import { unescapeString } from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types'; import { FontSize } from 'container/OptionsMenu/types';
import dayjs from 'dayjs';
import dompurify from 'dompurify'; import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog'; import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink'; import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
// utils // utils
import { FlatLogData } from 'lib/logs/flatLogData'; import { FlatLogData } from 'lib/logs/flatLogData';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
// interfaces // interfaces
import { IField } from 'types/api/logs/fields'; import { IField } from 'types/api/logs/fields';
@@ -174,12 +174,20 @@ function ListLogView({
[selectedFields], [selectedFields],
); );
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const timestampValue = useMemo( const timestampValue = useMemo(
() => () =>
typeof flattenLogData.timestamp === 'string' typeof flattenLogData.timestamp === 'string'
? dayjs(flattenLogData.timestamp).format('YYYY-MM-DD HH:mm:ss.SSS') ? formatTimezoneAdjustedTimestamp(
: dayjs(flattenLogData.timestamp / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'), flattenLogData.timestamp,
[flattenLogData.timestamp], 'YYYY-MM-DD HH:mm:ss.SSS',
)
: formatTimezoneAdjustedTimestamp(
flattenLogData.timestamp / 1e6,
'YYYY-MM-DD HH:mm:ss.SSS',
),
[flattenLogData.timestamp, formatTimezoneAdjustedTimestamp],
); );
const logType = getLogIndicatorType(logData); const logType = getLogIndicatorType(logData);

View File

@@ -6,7 +6,6 @@ import LogDetail from 'components/LogDetail';
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants'; import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
import { unescapeString } from 'container/LogDetailedView/utils'; import { unescapeString } from 'container/LogDetailedView/utils';
import LogsExplorerContext from 'container/LogsExplorerContext'; import LogsExplorerContext from 'container/LogsExplorerContext';
import dayjs from 'dayjs';
import dompurify from 'dompurify'; import dompurify from 'dompurify';
import { useActiveLog } from 'hooks/logs/useActiveLog'; import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink'; import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
@@ -14,6 +13,7 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { FlatLogData } from 'lib/logs/flatLogData'; import { FlatLogData } from 'lib/logs/flatLogData';
import { isEmpty, isNumber, isUndefined } from 'lodash-es'; import { isEmpty, isNumber, isUndefined } from 'lodash-es';
import { useTimezone } from 'providers/Timezone';
import { import {
KeyboardEvent, KeyboardEvent,
MouseEvent, MouseEvent,
@@ -89,16 +89,24 @@ function RawLogView({
attributesText += ' | '; attributesText += ' | ';
} }
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const text = useMemo(() => { const text = useMemo(() => {
const date = const date =
typeof data.timestamp === 'string' typeof data.timestamp === 'string'
? dayjs(data.timestamp) ? formatTimezoneAdjustedTimestamp(data.timestamp, 'YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(data.timestamp / 1e6); : formatTimezoneAdjustedTimestamp(
data.timestamp / 1e6,
'YYYY-MM-DD HH:mm:ss.SSS',
);
return `${date.format('YYYY-MM-DD HH:mm:ss.SSS')} | ${attributesText} ${ return `${date} | ${attributesText} ${data.body}`;
data.body }, [
}`; data.timestamp,
}, [data.timestamp, data.body, attributesText]); data.body,
attributesText,
formatTimezoneAdjustedTimestamp,
]);
const handleClickExpand = useCallback(() => { const handleClickExpand = useCallback(() => {
if (activeContextLog || isReadOnly) return; if (activeContextLog || isReadOnly) return;

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

@@ -5,10 +5,10 @@ import { Typography } from 'antd';
import { ColumnsType } from 'antd/es/table'; import { ColumnsType } from 'antd/es/table';
import cx from 'classnames'; import cx from 'classnames';
import { unescapeString } from 'container/LogDetailedView/utils'; import { unescapeString } from 'container/LogDetailedView/utils';
import dayjs from 'dayjs';
import dompurify from 'dompurify'; import dompurify from 'dompurify';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { FlatLogData } from 'lib/logs/flatLogData'; import { FlatLogData } from 'lib/logs/flatLogData';
import { useTimezone } from 'providers/Timezone';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app'; import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
@@ -44,6 +44,8 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
logs, logs,
]); ]);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => { const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
const fieldColumns: ColumnsType<Record<string, unknown>> = fields const fieldColumns: ColumnsType<Record<string, unknown>> = fields
.filter((e) => e.name !== 'id') .filter((e) => e.name !== 'id')
@@ -73,23 +75,38 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
} }
return [ return [
{
// We do not need any title and data index for the log state indicator
title: '',
dataIndex: '',
key: 'state-indicator',
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
children: (
<div className={cx('state-indicator', fontSize)}>
<LogStateIndicator
type={getLogIndicatorTypeForTable(item)}
fontSize={fontSize}
/>
</div>
),
}),
},
{ {
title: 'timestamp', title: 'timestamp',
dataIndex: 'timestamp', dataIndex: 'timestamp',
key: 'timestamp', key: 'timestamp',
// https://github.com/ant-design/ant-design/discussions/36886 // https://github.com/ant-design/ant-design/discussions/36886
render: (field, item): ColumnTypeRender<Record<string, unknown>> => { render: (field): ColumnTypeRender<Record<string, unknown>> => {
const date = const date =
typeof field === 'string' typeof field === 'string'
? dayjs(field).format('YYYY-MM-DD HH:mm:ss.SSS') ? formatTimezoneAdjustedTimestamp(field, 'YYYY-MM-DD HH:mm:ss.SSS')
: dayjs(field / 1e6).format('YYYY-MM-DD HH:mm:ss.SSS'); : formatTimezoneAdjustedTimestamp(
field / 1e6,
'YYYY-MM-DD HH:mm:ss.SSS',
);
return { return {
children: ( children: (
<div className="table-timestamp"> <div className="table-timestamp">
<LogStateIndicator
type={getLogIndicatorTypeForTable(item)}
fontSize={fontSize}
/>
<Typography.Paragraph ellipsis className={cx('text', fontSize)}> <Typography.Paragraph ellipsis className={cx('text', fontSize)}>
{date} {date}
</Typography.Paragraph> </Typography.Paragraph>
@@ -125,7 +142,15 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
}, },
...(appendTo === 'end' ? fieldColumns : []), ...(appendTo === 'end' ? fieldColumns : []),
]; ];
}, [fields, isListViewPanel, appendTo, isDarkMode, linesPerRow, fontSize]); }, [
fields,
isListViewPanel,
appendTo,
isDarkMode,
linesPerRow,
fontSize,
formatTimezoneAdjustedTimestamp,
]);
return { columns, dataSource: flattenLogData }; return { columns, dataSource: flattenLogData };
}; };

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

@@ -1,11 +1,13 @@
import { Typography } from 'antd'; import { Typography } from 'antd';
import convertDateToAmAndPm from 'lib/convertDateToAmAndPm'; import { useTimezone } from 'providers/Timezone';
import getFormattedDate from 'lib/getFormatedDate';
function Time({ CreatedOrUpdateTime }: DateProps): JSX.Element { function Time({ CreatedOrUpdateTime }: DateProps): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const time = new Date(CreatedOrUpdateTime); const time = new Date(CreatedOrUpdateTime);
const date = getFormattedDate(time); const timeString = formatTimezoneAdjustedTimestamp(
const timeString = `${date} ${convertDateToAmAndPm(time)}`; time,
'MM/DD/YYYY hh:mm:ss A (UTC Z)',
);
return <Typography>{timeString}</Typography>; return <Typography>{timeString}</Typography>;
} }

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

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

View File

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

View File

@@ -0,0 +1,3 @@
export const TimezonePickerShortcuts = {
CloseTimezonePicker: 'escape',
};

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

@@ -7,6 +7,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history'; import history from 'lib/history';
import heatmapPlugin from 'lib/uPlotLib/plugins/heatmapPlugin'; import heatmapPlugin from 'lib/uPlotLib/plugins/heatmapPlugin';
import timelinePlugin from 'lib/uPlotLib/plugins/timelinePlugin'; import timelinePlugin from 'lib/uPlotLib/plugins/timelinePlugin';
import { useTimezone } from 'providers/Timezone';
import { useMemo, useRef } from 'react'; import { useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { UpdateTimeInterval } from 'store/actions'; import { UpdateTimeInterval } from 'store/actions';
@@ -48,6 +49,7 @@ function HorizontalTimelineGraph({
const urlQuery = useUrlQuery(); const urlQuery = useUrlQuery();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { timezone } = useTimezone();
const options: uPlot.Options = useMemo( const options: uPlot.Options = useMemo(
() => ({ () => ({
@@ -116,8 +118,18 @@ function HorizontalTimelineGraph({
}), }),
] ]
: [], : [],
tzDate: (timestamp: number): Date =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
}), }),
[width, isDarkMode, transformedData.length, urlQuery, dispatch], [
width,
isDarkMode,
transformedData.length,
urlQuery,
dispatch,
timezone.value,
],
); );
return <Uplot data={transformedData} options={options} />; return <Uplot data={transformedData} options={options} />;
} }

View File

@@ -7,6 +7,7 @@ import {
useGetAlertRuleDetailsTimelineTable, useGetAlertRuleDetailsTimelineTable,
useTimelineTable, useTimelineTable,
} from 'pages/AlertDetails/hooks'; } from 'pages/AlertDetails/hooks';
import { useTimezone } from 'providers/Timezone';
import { HTMLAttributes, useMemo, useState } from 'react'; import { HTMLAttributes, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def'; import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
@@ -41,6 +42,8 @@ function TimelineTable(): JSX.Element {
const { t } = useTranslation('common'); const { t } = useTranslation('common');
const { formatTimezoneAdjustedTimestamp } = useTimezone();
if (isError || !isValidRuleId || !ruleId) { if (isError || !isValidRuleId || !ruleId) {
return <div>{t('something_went_wrong')}</div>; return <div>{t('something_went_wrong')}</div>;
} }
@@ -64,6 +67,7 @@ function TimelineTable(): JSX.Element {
filters, filters,
labels: labels ?? {}, labels: labels ?? {},
setFilters, setFilters,
formatTimezoneAdjustedTimestamp,
})} })}
onRow={handleRowClick} onRow={handleRowClick}
dataSource={timelineData} dataSource={timelineData}

View File

@@ -8,6 +8,7 @@ import ClientSideQBSearch, {
import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover'; import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover';
import { transformKeyValuesToAttributeValuesMap } from 'container/QueryBuilder/filters/utils'; import { transformKeyValuesToAttributeValuesMap } from 'container/QueryBuilder/filters/utils';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
import { Search } from 'lucide-react'; import { Search } from 'lucide-react';
import AlertLabels, { import AlertLabels, {
AlertLabelsProps, AlertLabelsProps,
@@ -16,7 +17,6 @@ import AlertState from 'pages/AlertDetails/AlertHeader/AlertState/AlertState';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def'; import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData'; import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { formatEpochTimestamp } from 'utils/timeUtils';
const transformLabelsToQbKeys = ( const transformLabelsToQbKeys = (
labels: AlertRuleTimelineTableResponse['labels'], labels: AlertRuleTimelineTableResponse['labels'],
@@ -74,10 +74,15 @@ export const timelineTableColumns = ({
filters, filters,
labels, labels,
setFilters, setFilters,
formatTimezoneAdjustedTimestamp,
}: { }: {
filters: TagFilter; filters: TagFilter;
labels: AlertLabelsProps['labels']; labels: AlertLabelsProps['labels'];
setFilters: (filters: TagFilter) => void; setFilters: (filters: TagFilter) => void;
formatTimezoneAdjustedTimestamp: (
input: TimestampInput,
format?: string,
) => string;
}): ColumnsType<AlertRuleTimelineTableResponse> => [ }): ColumnsType<AlertRuleTimelineTableResponse> => [
{ {
title: 'STATE', title: 'STATE',
@@ -106,7 +111,9 @@ export const timelineTableColumns = ({
dataIndex: 'unixMilli', dataIndex: 'unixMilli',
width: 200, width: 200,
render: (value): JSX.Element => ( render: (value): JSX.Element => (
<div className="alert-rule__created-at">{formatEpochTimestamp(value)}</div> <div className="alert-rule__created-at">
{formatTimezoneAdjustedTimestamp(value, 'MMM D, YYYY ⎯ HH:mm:ss')}
</div>
), ),
}, },
{ {

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

@@ -17,14 +17,15 @@ import getAll from 'api/errors/getAll';
import getErrorCounts from 'api/errors/getErrorCounts'; import getErrorCounts from 'api/errors/getErrorCounts';
import { ResizeTable } from 'components/ResizeTable'; import { ResizeTable } from 'components/ResizeTable';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import dayjs from 'dayjs';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import useResourceAttribute from 'hooks/useResourceAttribute'; import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams'; import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history'; import history from 'lib/history';
import { isUndefined } from 'lodash-es'; import { isUndefined } from 'lodash-es';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQueries } from 'react-query'; import { useQueries } from 'react-query';
@@ -155,8 +156,16 @@ function AllErrors(): JSX.Element {
} }
}, [data?.error, data?.payload, t, notifications]); }, [data?.error, data?.payload, t, notifications]);
const getDateValue = (value: string): JSX.Element => ( const getDateValue = (
<Typography>{dayjs(value).format('DD/MM/YYYY HH:mm:ss A')}</Typography> value: string,
formatTimezoneAdjustedTimestamp: (
input: TimestampInput,
format?: string,
) => string,
): JSX.Element => (
<Typography>
{formatTimezoneAdjustedTimestamp(value, 'DD/MM/YYYY hh:mm:ss A')}
</Typography>
); );
const filterIcon = useCallback(() => <SearchOutlined />, []); const filterIcon = useCallback(() => <SearchOutlined />, []);
@@ -283,6 +292,8 @@ function AllErrors(): JSX.Element {
[filterIcon, filterDropdownWrapper], [filterIcon, filterDropdownWrapper],
); );
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const columns: ColumnsType<Exception> = [ const columns: ColumnsType<Exception> = [
{ {
title: 'Exception Type', title: 'Exception Type',
@@ -342,7 +353,8 @@ function AllErrors(): JSX.Element {
dataIndex: 'lastSeen', dataIndex: 'lastSeen',
width: 80, width: 80,
key: 'lastSeen', key: 'lastSeen',
render: getDateValue, render: (value): JSX.Element =>
getDateValue(value, formatTimezoneAdjustedTimestamp),
sorter: true, sorter: true,
defaultSortOrder: getDefaultOrder( defaultSortOrder: getDefaultOrder(
getUpdatedParams, getUpdatedParams,
@@ -355,7 +367,8 @@ function AllErrors(): JSX.Element {
dataIndex: 'firstSeen', dataIndex: 'firstSeen',
width: 80, width: 80,
key: 'firstSeen', key: 'firstSeen',
render: getDateValue, render: (value): JSX.Element =>
getDateValue(value, formatTimezoneAdjustedTimestamp),
sorter: true, sorter: true,
defaultSortOrder: getDefaultOrder( defaultSortOrder: getDefaultOrder(
getUpdatedParams, getUpdatedParams,

View File

@@ -10,6 +10,7 @@ import getAxes from 'lib/uPlotLib/utils/getAxes';
import { getUplotChartDataForAnomalyDetection } from 'lib/uPlotLib/utils/getUplotChartData'; import { getUplotChartDataForAnomalyDetection } from 'lib/uPlotLib/utils/getUplotChartData';
import { getYAxisScaleForAnomalyDetection } from 'lib/uPlotLib/utils/getYAxisScale'; import { getYAxisScaleForAnomalyDetection } from 'lib/uPlotLib/utils/getYAxisScale';
import { LineChart } from 'lucide-react'; import { LineChart } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import uPlot from 'uplot'; import uPlot from 'uplot';
@@ -148,10 +149,12 @@ function AnomalyAlertEvaluationView({
] ]
: []; : [];
const { timezone } = useTimezone();
const options = { const options = {
width: dimensions.width, width: dimensions.width,
height: dimensions.height - 36, height: dimensions.height - 36,
plugins: [bandsPlugin, tooltipPlugin(isDarkMode)], plugins: [bandsPlugin, tooltipPlugin(isDarkMode, timezone.value)],
focus: { focus: {
alpha: 0.3, alpha: 0.3,
}, },
@@ -256,6 +259,8 @@ function AnomalyAlertEvaluationView({
show: true, show: true,
}, },
axes: getAxes(isDarkMode, yAxisUnit), axes: getAxes(isDarkMode, yAxisUnit),
tzDate: (timestamp: number): Date =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
}; };
const handleSearch = (searchText: string): void => { const handleSearch = (searchText: string): void => {

View File

@@ -1,8 +1,10 @@
import { themeColors } from 'constants/theme'; import { themeColors } from 'constants/theme';
import dayjs from 'dayjs';
import { generateColor } from 'lib/uPlotLib/utils/generateColor'; import { generateColor } from 'lib/uPlotLib/utils/generateColor';
const tooltipPlugin = ( const tooltipPlugin = (
isDarkMode: boolean, isDarkMode: boolean,
timezone: string,
): { hooks: { init: (u: any) => void } } => { ): { hooks: { init: (u: any) => void } } => {
let tooltip: HTMLDivElement; let tooltip: HTMLDivElement;
const tooltipLeftOffset = 10; const tooltipLeftOffset = 10;
@@ -17,7 +19,7 @@ const tooltipPlugin = (
return value.toFixed(3); return value.toFixed(3);
} }
if (value instanceof Date) { if (value instanceof Date) {
return value.toLocaleString(); return dayjs(value).tz(timezone).format('MM/DD/YYYY, h:mm:ss A');
} }
if (value == null) { if (value == null) {
return 'N/A'; return 'N/A';

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);
} }
}; };
@@ -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

@@ -117,7 +117,7 @@ export const logAlertDefaults: AlertDef = {
chQueries: { chQueries: {
A: { A: {
name: 'A', name: 'A',
query: `select \ntoStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 MINUTE) AS interval, \ntoFloat64(count()) as value \nFROM signoz_logs.distributed_logs_v2 \nWHERE timestamp BETWEEN {{.start_timestamp_nano}} AND {{.end_timestamp_nano}} \nGROUP BY interval;\n\n-- available variables:\n-- \t{{.start_timestamp_nano}}\n-- \t{{.end_timestamp_nano}}\n\n-- required columns (or alias):\n-- \tvalue\n-- \tinterval`, query: `select \ntoStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 30 MINUTE) AS interval, \ntoFloat64(count()) as value \nFROM signoz_logs.distributed_logs_v2 \nWHERE timestamp BETWEEN {{.start_timestamp_nano}} AND {{.end_timestamp_nano}} \nAND ts_bucket_start BETWEEN {{.start_timestamp}} - 1800 AND {{.end_timestamp}} \nGROUP BY interval;\n\n-- Please check docs here https://signoz.io/docs/userguide/logs_clickhouse_queries/\n\n-- available variables:\n-- \t{{.start_timestamp_nano}}\n-- \t{{.end_timestamp_nano}}\n-- \t{{.start_timestamp}}\n-- \t{{.end_timestamp}}\n\n-- required columns (or alias):\n-- \tvalue\n-- \tinterval`,
legend: '', legend: '',
disabled: false, disabled: false,
}, },
@@ -149,7 +149,7 @@ export const traceAlertDefaults: AlertDef = {
chQueries: { chQueries: {
A: { A: {
name: 'A', name: 'A',
query: `SELECT \n\ttoStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS interval, \n\tstringTagMap['peer.service'] AS op_name, \n\ttoFloat64(avg(durationNano)) AS value \nFROM signoz_traces.distributed_signoz_index_v2 \nWHERE stringTagMap['peer.service']!='' \nAND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}} \nGROUP BY (op_name, interval);\n\n-- available variables:\n-- \t{{.start_datetime}}\n-- \t{{.end_datetime}}\n\n-- required column alias:\n-- \tvalue\n-- \tinterval`, query: `SELECT \n\ttoStartOfInterval(timestamp, INTERVAL 1 MINUTE) AS interval, \n\tresource_string_service$$name AS \`service.name\`, \n\ttoFloat64(avg(duration_nano)) AS value \nFROM signoz_traces.distributed_signoz_index_v3 \nWHERE resource_string_service$$name !='' \nAND timestamp BETWEEN {{.start_datetime}} AND {{.end_datetime}} \nAND ts_bucket_start BETWEEN {{.start_timestamp}} - 1800 AND {{.end_timestamp}} \nGROUP BY (\`service.name\`, interval);\n\n-- Please check docs here https://signoz.io/docs/userguide/writing-clickhouse-traces-query/\n\n-- available variables:\n-- \t{{.start_datetime}}\n-- \t{{.end_datetime}}\n-- \t{{.start_timestamp}}\n-- \t{{.end_timestamp}}\n\n-- required column alias:\n-- \tvalue\n-- \tinterval`,
legend: '', legend: '',
disabled: false, disabled: false,
}, },

View File

@@ -6,12 +6,12 @@ import getNextPrevId from 'api/errors/getNextPrevId';
import Editor from 'components/Editor'; import Editor from 'components/Editor';
import { ResizeTable } from 'components/ResizeTable'; import { ResizeTable } from 'components/ResizeTable';
import { getNanoSeconds } from 'container/AllError/utils'; import { getNanoSeconds } from 'container/AllError/utils';
import dayjs from 'dayjs';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import createQueryParams from 'lib/createQueryParams'; import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history'; import history from 'lib/history';
import { isUndefined } from 'lodash-es'; import { isUndefined } from 'lodash-es';
import { urlKey } from 'pages/ErrorDetails/utils'; import { urlKey } from 'pages/ErrorDetails/utils';
import { useTimezone } from 'providers/Timezone';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
@@ -103,8 +103,6 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
} }
}; };
const timeStamp = dayjs(errorDetail.timestamp);
const data: { key: string; value: string }[] = Object.keys(errorDetail) const data: { key: string; value: string }[] = Object.keys(errorDetail)
.filter((e) => !keyToExclude.includes(e)) .filter((e) => !keyToExclude.includes(e))
.map((key) => ({ .map((key) => ({
@@ -136,6 +134,8 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]); }, [data]);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
return ( return (
<> <>
<Typography>{errorDetail.exceptionType}</Typography> <Typography>{errorDetail.exceptionType}</Typography>
@@ -145,7 +145,12 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
<EventContainer> <EventContainer>
<div> <div>
<Typography>Event {errorDetail.errorId}</Typography> <Typography>Event {errorDetail.errorId}</Typography>
<Typography>{timeStamp.format('MMM DD YYYY hh:mm:ss A')}</Typography> <Typography>
{formatTimezoneAdjustedTimestamp(
errorDetail.timestamp,
'DD/MM/YYYY hh:mm:ss A (UTC Z)',
)}
</Typography>
</div> </div>
<div> <div>
<Space align="end" direction="horizontal"> <Space align="end" direction="horizontal">

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,14 @@ import {
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions'; import { useResizeObserver } from 'hooks/useDimensions';
import useFeatureFlags from 'hooks/useFeatureFlag';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax'; import GetMinMax from 'lib/getMinMax';
import getTimeString from 'lib/getTimeString'; import getTimeString from 'lib/getTimeString';
import history from 'lib/history'; import history from 'lib/history';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useAppContext } from 'providers/App/App';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
@@ -35,6 +36,7 @@ import { AlertDef } from 'types/api/alerts/def';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import uPlot from 'uplot';
import { getGraphType } from 'utils/getGraphType'; import { getGraphType } from 'utils/getGraphType';
import { getSortedSeriesData } from 'utils/getSortedSeriesData'; import { getSortedSeriesData } from 'utils/getSortedSeriesData';
import { getTimeRange } from 'utils/getTimeRange'; import { getTimeRange } from 'utils/getTimeRange';
@@ -82,6 +84,8 @@ function ChartPreview({
GlobalReducer GlobalReducer
>((state) => state.globalTime); >((state) => state.globalTime);
const { featureFlags } = useAppContext();
const handleBackNavigation = (): void => { const handleBackNavigation = (): void => {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
const startTime = searchParams.get(QueryParams.startTime); const startTime = searchParams.get(QueryParams.startTime);
@@ -201,6 +205,8 @@ function ChartPreview({
[dispatch, location.pathname, urlQuery], [dispatch, location.pathname, urlQuery],
); );
const { timezone } = useTimezone();
const options = useMemo( const options = useMemo(
() => () =>
getUPlotChartOptions({ getUPlotChartOptions({
@@ -236,6 +242,9 @@ function ChartPreview({
softMax: null, softMax: null,
softMin: null, softMin: null,
panelType: graphType, panelType: graphType,
tzDate: (timestamp: number) =>
uPlot.tzDate(new Date(timestamp * 1e3), timezone.value),
timezone: timezone.value,
}), }),
[ [
yAxisUnit, yAxisUnit,
@@ -250,6 +259,7 @@ function ChartPreview({
optionName, optionName,
alertDef?.condition.targetUnit, alertDef?.condition.targetUnit,
graphType, graphType,
timezone.value,
], ],
); );
@@ -262,7 +272,8 @@ function ChartPreview({
chartData && !queryResponse.isError && !queryResponse.isLoading; chartData && !queryResponse.isError && !queryResponse.isLoading;
const isAnomalyDetectionEnabled = const isAnomalyDetectionEnabled =
useFeatureFlags(FeatureKeys.ANOMALY_DETECTION)?.active || false; featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
?.active || false;
return ( return (
<div className="alert-chart-container" ref={graphRef}> <div className="alert-chart-container" ref={graphRef}>

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

@@ -4,6 +4,7 @@ import { Popover, Typography } from 'antd';
import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils'; import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useTimezone } from 'providers/Timezone';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { toFixed } from 'utils/toFixed'; import { toFixed } from 'utils/toFixed';
@@ -32,13 +33,17 @@ function Span(props: SpanLengthProps): JSX.Element {
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount); const { time, timeUnitName } = convertTimeToRelevantUnit(inMsCount);
const { timezone } = useTimezone();
useEffect(() => { useEffect(() => {
document.documentElement.scrollTop = document.documentElement.clientHeight; document.documentElement.scrollTop = document.documentElement.clientHeight;
document.documentElement.scrollLeft = document.documentElement.clientWidth; document.documentElement.scrollLeft = document.documentElement.clientWidth;
}, []); }, []);
const getContent = (): JSX.Element => { const getContent = (): JSX.Element => {
const timeStamp = dayjs(startTime).format('h:mm:ss:SSS A'); const timeStamp = dayjs(startTime)
.tz(timezone.value)
.format('h:mm:ss:SSS A (UTC Z)');
const startTimeInMs = startTime - globalStart; const startTimeInMs = startTime - globalStart;
return ( return (
<div> <div>

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,

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