Compare commits

...

49 Commits

Author SHA1 Message Date
ahmadshaheer
e8605488e0 chore: remove dev env check 2025-06-16 11:15:16 +05:30
Shivanshu Raj Shrivastava
67f02c6609 fix: add new api endpoints for analytics
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-16 11:13:32 +05:30
Shivanshu Raj Shrivastava
cadc599643 Merge branch 'main' into feat/issues/query/7210 2025-06-16 08:50:46 +05:30
Vibhu Pandey
59ff7ed1e1 feat(licensing): add analytics (#8252) 2025-06-16 01:09:41 +05:30
Vibhu Pandey
d236b6ce1e feat(statsreporter): add stats for telemetry.*.last_observed.time (#8251)
## 📄 Summary

- add stats for telemetry.*.last_observed.time
2025-06-16 00:02:17 +05:30
Dimitris Mavrommatis
44b118a212 fix: span links tab to span details drawer (#7888)
* fix: span links tab to span details drawer

* Update LinkedSpans.styles.scss

---------

Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2025-06-15 16:22:36 +04:30
Dimitris Mavrommatis
3fc6f7ee63 feat(trace): add visuals for events on span waterfall and flamegraph (#7889)
* fix: add visuals on waterfall and flamegraph for span events

* fix: correct offsets for events

* fix: addressed comments

* chore: update the name of the import

* fix: interface change

* chore: formatting

---------

Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
Co-authored-by: Vikrant Gupta <vikrant@signoz.io>
2025-06-15 10:39:39 +00:00
Sahil Khan
f1016baf03 chore: updated http-proxy-middleware to 3.0.5 from 3.0.3 (#8245) 2025-06-13 21:35:48 +05:30
Amlan Kumar Nandy
e5c0d9e44a chore: allow url as label value in alerts (#8244) 2025-06-13 17:47:01 +07:00
Yunus M
e51056c804 fix: use preference.name rather than preference.key (#8234) 2025-06-13 11:44:42 +05:30
Nityananda Gohain
7d8dad4550 Revert "fix: remove whitespace from sso cert (#8141)" (#8233)
This reverts commit 44ea237039.
2025-06-12 22:39:24 +05:30
Yunus M
c477e0ef16 feat: sidebar revamp (#8087)
* feat: sidebar revamp - initial commit

* feat: move billinga and other isolated routes to settings

* feat: handle channel related routes

* feat: update account settings page

* feat: show dropdown for secondary items

* feat: handle reordering of pinned nav items

* feat: improve font load performance

* feat: update font reference

* feat: update page content styles

* feat: handle external links in sidebar

* feat: handle secondary nav item clicks

* feat: handle pinned nav items reordering

* feat: handle sidenav pinned state using preference, handle light mode

* feat: show sidenav items conditionally

* feat: show version diff indicator only to self hosted users

* feat: show billing to admins only and integrations to cloud and enterprise users

* feat: update fallback link

* feat: handle settings menu items

* fix: settings page reload on nav chnage

* feat: intercom to pylon

* feat: show invite user to admin only

* feat: handle review comments

* chore: remove react query dev tools

* feat: minor ui updates

* feat: update changes based on preference store changes

* feat: handle sidenav shortcut state

* feat: handle scroll for more

* feat: maintain shortcuts order

* feat: manage license ui updates

* feat: manage settings options based on license and roles

* feat: update types

* chore: add logEvents

* feat: update types

* chore: fix type errors

* chore: remove unused variable

* feat: update my settings page test cases

---------

Co-authored-by: makeavish <makeavish786@gmail.com>
2025-06-12 19:55:32 +05:30
Shaheer Kochai
fff7f8fc76 feat: add span scope filter to trace details page (#8005)
* feat: add span scope filter to trace details page

* chore: add tests for the span scope selector flows when onchange and query are provided

* refactor: remove the unnecessary queryName prop and infer it from query

* fix: fix the failing span scope selector tests
2025-06-12 11:03:28 +00:00
Shaheer Kochai
8cfeef4521 fix: fix sentries (#8003)
* fix: handle potential undefined values in groupBy calculation in TracesExplorer

* fix: add optional chaining for aggregateAttribute key check in Query component

* fix: add optional chaining for filters in SpanScopeSelector to handle potential undefined values

* fix: fix the warning in logs chart by adding the missing date-time format option

* fix: improve trace graph allDataPoints null check

* chore: remove the keys.length from null check
2025-06-12 15:28:06 +04:30
Sahil Khan
d85a1a21ac feat: generalised preferences framework (#7903)
* feat: preferences framework generalised scaffolded

* feat: preferences framework integrated logs & saved logs views

* feat: fixed bugs in saved views for traces

* fix: removed unused file

* fix: wrapped metric explorer inside preferences context as it uses useoptions hook

* feat: added tests for preferences framework alongside some minor bugs improvements

* chore: added tests for traces loader and updater

* chore: fixed failing tests due to new context for preferences

* fix: minor saved views handling bug

* fix: minor pref fix

* fix: breaking tests

* fix: undo removal of pref context from live logs

* feat: split the logic of columns and formatting load in logs

* fix: breaking tests

* fix: pr comments

* fix: minor bug and pr comments regarding better resync

* fix: bugs in internal flows

* fix: url pref sync

* fix: minor bug fix

* fix: fixed failing tests

* fix: fixed failing tests
2025-06-11 10:06:01 +00:00
primus-bot[bot]
17f48d656d chore(release): bump to v0.87.0 (#8222)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-06-11 12:06:17 +05:30
Srikanth Chekuri
2d6774da68 fix: add missing denominator for reset case (#8180) 2025-06-11 11:32:50 +05:30
Vibhu Pandey
62a9d7e602 docs(contributing): add endpoint docs (#8215)
* docs(contributing): add endpoint docs

* docs(contributing): add endpoint docs

* docs(contributing): add endpoint docs

* docs(contributing): add endpoint docs

* docs(contributing): add endpoint docs
2025-06-10 17:25:07 +00:00
Vikrant Gupta
3a2c7a7a68 fix(dashboard): create dashboard panic for id (#8214) 2025-06-10 21:31:56 +05:30
Sahil Khan
33e70d1f37 fix: traces back button issue (#8041)
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2025-06-10 13:25:59 +00:00
Srikanth Chekuri
85f04e4bae chore: add querier HTTP API endpoint and bucket cache implementation (#8178)
* chore: update types
1. add partial bool to indicate if the value covers the partial interval
2. add optional unit if present (ex: duration_nano, metrics with units)
3. use pointers wherever necessary
4. add format options for request and remove redundant name in query envelope

* chore: fix some gaps
1. make the range as [start, end)
2. provide the logs statement builder with the body column
3. skip the body filter on resource filter statement builder
4. remove unnecessary agg expr rewriter in metrics
5. add ability to skip full text in where clause visitor

* chore: add API endpoint for new query range

* chore: add bucket cache implementation

* chore: add fingerprinting impl and add bucket cache to querier

* chore: add provider factory
2025-06-10 12:56:28 +00:00
Shaheer Kochai
53f9e7d811 chore: trace funnels bugfixes/improvements (#8114)
* fix: refetch funnel steps overview on clicking refresh

* chore: temporarily hide latency pointer from funnel steps

* chore: remove the existing filters of a step on clicking replace button

* fix(useLocalStorage): stabilize initialValue handling to prevent unnecessary re-renders

* chore: remove p99_latency references from funnel metrics

* fix(useFunnelMetrics): ensure latency type defaults to P99 when undefined
2025-06-10 12:45:25 +00:00
Vibhu Pandey
ad46e22561 docs(contributing): add provider docs (#8193) 2025-06-10 05:39:14 +00:00
Yunus M
e79195ccf1 fix: handle alert list updates (#8109) 2025-06-10 10:56:45 +05:30
Amlan Kumar Nandy
f77bb888a8 chore: add analytics for metrics explorer (#8108) 2025-06-10 04:42:49 +00:00
Amlan Kumar Nandy
baa15baea9 chore: metrics explorer summary view fixes (#8126) 2025-06-10 04:18:12 +00:00
Vibhu Pandey
316e6821f1 feat(statsreporter): report stats on stop (#8187) 2025-06-10 07:55:32 +05:30
Vibhu Pandey
a1fa2769e4 feat(statsreporter): build a statsreporter service (#8177)
- build a new statsreporter service
2025-06-09 16:43:29 +05:30
Vikrant Gupta
decb660992 chore(sqlmigration): drop the rule history and data migrations table (#8181) 2025-06-09 15:46:22 +05:30
Amlan Kumar Nandy
0acbcf8322 chore: remove critters-webpack-plugin (#8172) 2025-06-07 16:21:06 +07:00
dependabot[bot]
11eabdc2ac chore(deps): bump webpack-dev-server from 4.15.2 to 5.2.1 in /frontend (#8160)
* chore(deps): bump webpack-dev-server from 4.15.2 to 5.2.1 in /frontend

Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.15.2 to 5.2.1.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.15.2...v5.2.1)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-version: 5.2.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* feat: upgraded webpack-cli for compatibility fix for webpack-dev-server upgrade

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com>
Co-authored-by: SagarRajput-7 <sagar@signoz.io>
2025-06-07 07:24:18 +00:00
Vibhu Pandey
eb94554f5a feat(preference): add support for objects and arrays (#8142)
* refactor(preference): better readability

* refactor: better readability

* refactor: better readability

* fix: change frontend contract

* refactor: change frontend

* refactor: change frontend

* refactor: change frontend

* refactor: change frontend

* chore: fix tsc

* chore: fix tsc

* chore: fix tsc

* chore: fix tsc
2025-06-06 22:38:28 +05:30
Piyush Singariya
e8280dbea4 feat: Adding ContainerInsights in ECS Integrations (AWS) (#8122)
* fix: adding ECS ContainerInsights

* chore: dashboard added

* chore: format integrations.json

* feat(7294): added _dot metrics for aws ecs

---------

Co-authored-by: aniket <aniket@signoz.io>
2025-06-06 09:27:35 +00:00
Nityananda Gohain
44ea237039 fix: remove whitespace from sso cert (#8141)
* fix: remove whitespace from sso cert

* fix: use trimspace instead

* fix: use replaceall
2025-06-06 09:03:46 +00:00
Srikanth Chekuri
72b0214d1d chore: add range query impl for promql (#8130) 2025-06-05 19:18:44 +00:00
Srikanth Chekuri
386a215324 chore: metric statement builder (#8104) 2025-06-06 00:38:48 +05:30
Vibhu Pandey
ba0ba4bbc9 build(go): upgrade purego to v0.8.4 (#8159) 2025-06-05 12:31:49 +00:00
SagarRajput-7
d60c9ab36b feat: handle unkown metric in panel query (#8083)
* feat: handle unkown metric in panel query

* feat: added handling with type empty and key present or not

* feat: added test cases

* feat: added comment to better explain the logic

* feat: fixed operator list for unkown metric

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-06-05 11:23:25 +00:00
Piyush Singariya
90770b90bd feat: Introducing EKS integration (AWS) (#8021)
* feat: introducing EKS integration (AWS)

* fix: update metrics and enable logs collection

* feat: eks Overview dashboard ready

* feat: containerinsights incoming

* chore: dashboard name update

* feat(7294): added _dot metrics for aws ecs

* feat(7274): added dot metrics for overview metrics in eks

* Update pkg/query-service/app/cloudintegrations/services/definitions/aws/eks/assets/dashboards/overview_dot.json

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update pkg/query-service/app/cloudintegrations/services/definitions/aws/eks/assets/dashboards/containerinsights_dot.json

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: aniket <aniket@signoz.io>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-06-05 15:35:39 +05:30
Vikrant Gupta
a19874c1dd fix(dashboard): dashboards/alerts info telemetry fix (#8161) 2025-06-05 13:47:25 +05:30
Piyush Singariya
65ff460d63 fix: Enhance filter support for Pipeline Simulation (#8134)
* feat: enhance filter support for JSON log body

* test: added tests for exists and not exists

* test: remove the value
2025-06-05 05:05:39 +00:00
Shivanshu Raj Shrivastava
2f74cef1cb Merge branch 'main' into feat/issues/query/7210 2025-06-04 11:59:04 +05:30
Shivanshu Raj Shrivastava
bfffe424c2 Merge branch 'main' into feat/issues/query/7210 2025-06-03 12:41:21 +05:30
Shivanshu Raj Shrivastava
3b46c72374 fix: update step overview queries
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-03 08:13:40 +05:30
Shivanshu Raj Shrivastava
9e531ac52d fix: update clauses
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-03 08:13:40 +05:30
Shivanshu Raj Shrivastava
78ef1e7896 fix: minor fix in handler
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-03 08:13:40 +05:30
Shivanshu Raj Shrivastava
2b647ddcb4 fix: fix queries
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-03 08:13:40 +05:30
Shivanshu Raj Shrivastava
5a548d14e0 fix: update access
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-03 08:13:40 +05:30
Shivanshu Raj Shrivastava
43ca30e23e feat: trace funnel queries
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-03 08:13:40 +05:30
358 changed files with 33280 additions and 3422 deletions

View File

@@ -74,7 +74,8 @@ jobs:
-X github.com/SigNoz/signoz/pkg/version.variant=community
-X github.com/SigNoz/signoz/pkg/version.hash=${{ needs.prepare.outputs.hash }}
-X github.com/SigNoz/signoz/pkg/version.time=${{ needs.prepare.outputs.time }}
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}'
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
GO_CGO_ENABLED: 1
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
DOCKER_DOCKERFILE_PATH: ./pkg/query-service/Dockerfile.multi-arch

View File

@@ -108,7 +108,8 @@ jobs:
-X github.com/SigNoz/signoz/ee/zeus.url=https://api.signoz.cloud
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.signoz.io
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.signoz.cloud
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1'
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
GO_CGO_ENABLED: 1
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
DOCKER_DOCKERFILE_PATH: ./ee/query-service/Dockerfile.multi-arch

View File

@@ -107,7 +107,8 @@ jobs:
-X github.com/SigNoz/signoz/ee/zeus.url=https://api.staging.signoz.cloud
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.staging.signoz.cloud
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.staging.signoz.cloud
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.staging.signoz.cloud/api/v1'
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.staging.signoz.cloud/api/v1
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
GO_CGO_ENABLED: 1
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
DOCKER_DOCKERFILE_PATH: ./ee/query-service/Dockerfile.multi-arch

View File

@@ -165,12 +165,6 @@ alertmanager:
# Retention of the notification logs.
retention: 120h
##################### Analytics #####################
analytics:
# Whether to enable analytics.
enabled: false
##################### Emailing #####################
emailing:
# Whether to enable emailing.
@@ -215,3 +209,18 @@ sharder:
single:
# The org id to which this instance belongs to.
org_id: org_id
##################### Analytics #####################
analytics:
# Whether to enable analytics.
enabled: false
segment:
# The key to use for segment.
key: ""
##################### StatsReporter #####################
statsreporter:
# Whether to enable stats reporter. This is used to provide valuable insights to the SigNoz team. It does not collect any sensitive/PII data.
enabled: true
# The interval at which the stats are collected.
interval: 6h

View File

@@ -174,7 +174,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.86.2
image: signoz/signoz:v0.87.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -110,7 +110,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.86.2
image: signoz/signoz:v0.87.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -177,7 +177,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.86.2}
image: signoz/signoz:${VERSION:-v0.87.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -110,7 +110,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.86.2}
image: signoz/signoz:${VERSION:-v0.87.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -0,0 +1,51 @@
# Endpoint
This guide outlines the recommended approach for designing endpoints, with a focus on entity relationships, RESTful structure, and examples from the codebase.
## How do we design an endpoint?
### Understand the core entities and their relationships
Start with understanding the core entities and their relationships. For example:
- **Organization**: an organization can have multiple users
### Structure Endpoints RESTfully
Endpoints should reflect the resource hierarchy and follow RESTful conventions. Use clear, **pluralized resource names** and versioning. For example:
- `POST /v1/organizations` — Create an organization
- `GET /v1/organizations/:id` — Get an organization by id
- `DELETE /v1/organizations/:id` — Delete an organization by id
- `PUT /v1/organizations/:id` — Update an organization by id
- `GET /v1/organizations/:id/users` — Get all users in an organization
- `GET /v1/organizations/me/users` — Get all users in my organization
Think in terms of resource navigation in a file system. For example, to find your organization, you would navigate to the root of the file system and then to the `organizations` directory. To find a user in an organization, you would navigate to the `organizations` directory and then to the `id` directory.
```bash
v1/
├── organizations/
│ └── 123/
│ └── users/
```
`me` endpoints are special. They are used to determine the actual id via some auth/external mechanism. For `me` endpoints, think of the `me` directory being symlinked to your organization directory. For example, if you are a part of the organization `123`, the `me` directory will be symlinked to `/v1/organizations/123`:
```bash
v1/
├── organizations/
│ └── me/ -> symlink to /v1/organizations/123
│ └── users/
│ └── 123/
│ └── users/
```
> 💡 **Note**: There are various ways to structure endpoints. Some prefer to use singular resource names instead of `me`. Others prefer to use singular resource names for all endpoints. We have, however, chosen to standardize our endpoints in the manner described above.
## What should I remember?
- Use clear, **plural resource names**
- Use `me` endpoints for determining the actual id via some auth mechanism
> 💡 **Note**: When in doubt, diagram the relationships and walk through the user flows as if navigating a file system. This will help you design endpoints that are both logical and user-friendly.

View File

@@ -0,0 +1,106 @@
# Provider
SigNoz is built on the provider pattern, a design approach where code is organized into providers that handle specific application responsibilities. Providers act as adapter components that integrate with external services and deliver required functionality to the application.
> 💡 **Note**: Coming from a DDD background? Providers are similar (not exactly the same) to adapter/infrastructure services.
## How to create a new provider?
To create a new provider, create a directory in the `pkg/` directory named after your provider. The provider package consists of four key components:
- **Interface** (`pkg/<name>/<name>.go`): Defines the provider's interface. Other packages should import this interface to use the provider.
- **Config** (`pkg/<name>/config.go`): Contains provider configuration, implementing the `factory.Config` interface from [factory/config.go](/pkg/factory/config.go).
- **Implementation** (`pkg/<name>/<implname><name>/provider.go`): Contains the provider implementation, including a `NewProvider` function that returns a `factory.Provider` interface from [factory/provider.go](/pkg/factory/provider.go).
- **Mock** (`pkg/<name>/<name>test.go`): Provides mocks for the provider, typically used by dependent packages for unit testing.
For example, the [prometheus](/pkg/prometheus) provider delivers a prometheus engine to the application:
- `pkg/prometheus/prometheus.go` - Interface definition
- `pkg/prometheus/config.go` - Configuration
- `pkg/prometheus/clickhouseprometheus/provider.go` - Clickhouse-powered implementation
- `pkg/prometheus/prometheustest/provider.go` - Mock implementation
## How to wire it up?
The `pkg/signoz` package contains the inversion of control container responsible for wiring providers. It handles instantiation, configuration, and assembly of providers based on configuration metadata.
> 💡 **Note**: Coming from a Java background? Providers are similar to Spring beans.
Wiring up a provider involves three steps:
1. Wiring up the configuration
Add your config from `pkg/<name>/config.go` to the `pkg/signoz/config.Config` struct and in new factories:
```go
type Config struct {
...
MyProvider myprovider.Config `mapstructure:"myprovider"`
...
}
func NewConfig(ctx context.Context, resolverConfig config.ResolverConfig, ....) (Config, error) {
...
configFactories := []factory.ConfigFactory{
myprovider.NewConfigFactory(),
}
...
}
```
2. Wiring up the provider
Add available provider implementations in `pkg/signoz/provider.go`:
```go
func NewMyProviderFactories() factory.NamedMap[factory.ProviderFactory[myprovider.MyProvider, myprovider.Config]] {
return factory.MustNewNamedMap(
myproviderone.NewFactory(),
myprovidertwo.NewFactory(),
)
}
```
3. Instantiate the provider by adding it to the `SigNoz` struct in `pkg/signoz/signoz.go`:
```go
type SigNoz struct {
...
MyProvider myprovider.MyProvider
...
}
func New(...) (*SigNoz, error) {
...
myprovider, err := myproviderone.New(ctx, settings, config.MyProvider, "one/two")
if err != nil {
return nil, err
}
...
}
```
## How to use it?
To use a provider, import its interface. For example, to use the prometheus provider, import `pkg/prometheus/prometheus.go`:
```go
import "github.com/SigNoz/signoz/pkg/prometheus/prometheus"
func CreateSomething(ctx context.Context, prometheus prometheus.Prometheus) {
...
prometheus.DoSomething()
...
}
```
## Why do we need this?
Like any dependency injection framework, providers decouple the codebase from implementation details. This is especially valuable in SigNoz's large codebase, where we need to swap implementations without changing dependent code. The provider pattern offers several benefits apart from the obvious one of decoupling:
- Configuration is **defined with each provider and centralized in one place**, making it easier to understand and manage through various methods (environment variables, config files, etc.)
- Provider mocking is **straightforward for unit testing**, with a consistent pattern for locating mocks
- **Multiple implementations** of the same provider are **supported**, as demonstrated by our sqlstore provider
## What should I remember?
- Use the provider pattern wherever applicable.
- Always create a provider **irrespective of the number of implementations**. This makes it easier to add new implementations in the future.

View File

@@ -6,11 +6,13 @@ import (
"time"
"github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/analyticstypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/zeus"
@@ -23,16 +25,17 @@ type provider struct {
config licensing.Config
settings factory.ScopedProviderSettings
orgGetter organization.Getter
analytics analytics.Analytics
stopChan chan struct{}
}
func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
return factory.NewProviderFactory(factory.MustNewName("http"), func(ctx context.Context, providerSettings factory.ProviderSettings, config licensing.Config) (licensing.Licensing, error) {
return New(ctx, providerSettings, config, store, zeus, orgGetter)
return New(ctx, providerSettings, config, store, zeus, orgGetter, analytics)
})
}
func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter) (licensing.Licensing, error) {
func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) (licensing.Licensing, error) {
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/ee/licensing/httplicensing")
licensestore := sqllicensingstore.New(sqlstore)
return &provider{
@@ -42,6 +45,7 @@ func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Conf
settings: settings,
orgGetter: orgGetter,
stopChan: make(chan struct{}),
analytics: analytics,
}, nil
}
@@ -159,6 +163,25 @@ func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUI
return err
}
stats := licensetypes.NewStatsFromLicense(activeLicense)
provider.analytics.Send(ctx,
analyticstypes.Track{
UserId: "stats_" + organizationID.String(),
Event: "License Updated",
Properties: analyticstypes.NewPropertiesFromMap(stats),
Context: &analyticstypes.Context{
Extra: map[string]interface{}{
analyticstypes.KeyGroupID: organizationID.String(),
},
},
},
analyticstypes.Group{
UserId: "stats_" + organizationID.String(),
GroupId: organizationID.String(),
Traits: analyticstypes.NewTraitsFromMap(stats),
},
)
return nil
}
@@ -211,3 +234,16 @@ func (provider *provider) GetFeatureFlags(ctx context.Context, organizationID va
return license.Features, nil
}
func (provider *provider) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
activeLicense, err := provider.GetActive(ctx, orgID)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return map[string]any{}, nil
}
return nil, err
}
return licensetypes.NewStatsFromLicense(activeLicense), nil
}

View File

@@ -39,6 +39,7 @@ builds:
- -X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.signoz.io
- -X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.signoz.cloud
- -X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1
- -X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr
- >-
{{- if eq .Os "linux" }}-linkmode external -extldflags '-static'{{- end }}
mod_timestamp: "{{ .CommitTimestamp }}"

View File

@@ -12,6 +12,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apis/fields"
"github.com/SigNoz/signoz/pkg/http/middleware"
querierAPI "github.com/SigNoz/signoz/pkg/querier"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
@@ -58,8 +59,9 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler,
FluxInterval: opts.FluxInterval,
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
FieldsAPI: fields.NewAPI(signoz.TelemetryStore, signoz.Instrumentation.Logger()),
FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore),
Signoz: signoz,
QuerierAPI: querierAPI.NewAPI(signoz.Querier),
})
if err != nil {

View File

@@ -294,6 +294,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterInfraMetricsRoutes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am)
apiHandler.RegisterQueryRangeV5Routes(r, am)
apiHandler.RegisterWebSocketPaths(r, am)
apiHandler.RegisterMessagingQueuesRoutes(r, am)
apiHandler.RegisterThirdPartyApiRoutes(r, am)

View File

@@ -12,6 +12,7 @@ import (
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
"github.com/SigNoz/signoz/ee/zeus"
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/config"
"github.com/SigNoz/signoz/pkg/config/envprovider"
"github.com/SigNoz/signoz/pkg/config/fileprovider"
@@ -134,8 +135,8 @@ func main() {
zeus.Config(),
httpzeus.NewProviderFactory(),
licensing.Config(24*time.Hour, 3),
func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus, orgGetter organization.Getter) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] {
return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter)
func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] {
return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter, analytics)
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),

View File

@@ -78,7 +78,7 @@
"fontfaceobserver": "2.3.0",
"history": "4.10.1",
"html-webpack-plugin": "5.5.0",
"http-proxy-middleware": "3.0.3",
"http-proxy-middleware": "3.0.5",
"http-status-codes": "2.3.0",
"i18next": "^21.6.12",
"i18next-browser-languagedetector": "^6.1.3",
@@ -134,7 +134,7 @@
"uuid": "^8.3.2",
"web-vitals": "^0.2.4",
"webpack": "5.94.0",
"webpack-dev-server": "^4.15.2",
"webpack-dev-server": "^5.2.1",
"webpack-retry-chunk-load-plugin": "3.1.1",
"xstate": "^4.31.0"
},
@@ -197,7 +197,6 @@
"babel-plugin-styled-components": "^1.12.0",
"compression-webpack-plugin": "9.0.0",
"copy-webpack-plugin": "^11.0.0",
"critters-webpack-plugin": "^3.0.1",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.4",
@@ -235,7 +234,7 @@
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "5.0.1",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.2"
"webpack-cli": "^5.1.4"
},
"lint-staged": {
"*.(js|jsx|ts|tsx)": [
@@ -251,7 +250,7 @@
"xml2js": "0.5.0",
"phin": "^3.7.1",
"body-parser": "1.20.3",
"http-proxy-middleware": "3.0.3",
"http-proxy-middleware": "3.0.5",
"cross-spawn": "7.0.5",
"cookie": "^0.7.1",
"serialize-javascript": "6.0.2",

View File

@@ -9,8 +9,8 @@
"tooltip_notification_channels": "More details on how to setting notification channels",
"sending_channels_note": "The alerts will be sent to all the configured channels.",
"loading_channels_message": "Loading Channels..",
"page_title_create": "New Notification Channels",
"page_title_edit": "Edit Notification Channels",
"page_title_create": "New Notification Channel",
"page_title_edit": "Edit Notification Channel",
"button_save_channel": "Save",
"button_test_channel": "Test",
"button_return": "Back",

View File

@@ -9,8 +9,8 @@
"tooltip_notification_channels": "More details on how to setting notification channels",
"sending_channels_note": "The alerts will be sent to all the configured channels.",
"loading_channels_message": "Loading Channels..",
"page_title_create": "New Notification Channels",
"page_title_edit": "Edit Notification Channels",
"page_title_create": "New Notification Channel",
"page_title_edit": "Edit Notification Channel",
"button_save_channel": "Save",
"button_test_channel": "Test",
"button_return": "Back",

View File

@@ -3,6 +3,7 @@ import setLocalStorageApi from 'api/browser/localstorage/set';
import getAll from 'api/v1/user/get';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import history from 'lib/history';
@@ -14,6 +15,7 @@ import { matchPath, useLocation } from 'react-router-dom';
import { SuccessResponseV2 } from 'types/api';
import APIError from 'types/api/error';
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
import { OrgPreference } from 'types/api/preferences/preference';
import { Organization } from 'types/api/user/getOrganization';
import { UserResponse } from 'types/api/user/getUser';
import { USER_ROLES } from 'types/roles';
@@ -95,7 +97,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
usersData.data
) {
const isOnboardingComplete = orgPreferences?.find(
(preference: Record<string, any>) => preference.key === 'ORG_ONBOARDING',
(preference: OrgPreference) =>
preference.name === ORG_PREFERENCES.ORG_ONBOARDING,
)?.value;
const isFirstUser = checkFirstTimeUser();

View File

@@ -193,11 +193,12 @@ function App(): JSX.Element {
updatedRoutes = updatedRoutes.filter(
(route) => route?.path !== ROUTES.BILLING,
);
if (isEnterpriseSelfHostedUser) {
updatedRoutes.push(LIST_LICENSES);
}
}
if (isEnterpriseSelfHostedUser) {
updatedRoutes.push(LIST_LICENSES);
}
// always add support route for cloud users
updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE];
} else {

View File

@@ -128,12 +128,11 @@ export const AlertOverview = Loadable(
);
export const CreateAlertChannelAlerts = Loadable(
() =>
import(/* webpackChunkName: "Create Channels" */ 'pages/AlertChannelCreate'),
() => import(/* webpackChunkName: "Create Channels" */ 'pages/Settings'),
);
export const EditAlertChannelsAlerts = Loadable(
() => import(/* webpackChunkName: "Edit Channels" */ 'pages/ChannelsEdit'),
() => import(/* webpackChunkName: "Edit Channels" */ 'pages/Settings'),
);
export const AllAlertChannels = Loadable(
@@ -165,7 +164,7 @@ export const APIKeys = Loadable(
);
export const MySettings = Loadable(
() => import(/* webpackChunkName: "All MySettings" */ 'pages/MySettings'),
() => import(/* webpackChunkName: "All MySettings" */ 'pages/Settings'),
);
export const CustomDomainSettings = Loadable(
@@ -222,7 +221,7 @@ export const LogsIndexToFields = Loadable(
);
export const BillingPage = Loadable(
() => import(/* webpackChunkName: "BillingPage" */ 'pages/Billing'),
() => import(/* webpackChunkName: "BillingPage" */ 'pages/Settings'),
);
export const SupportPage = Loadable(
@@ -249,7 +248,7 @@ export const WorkspaceAccessRestricted = Loadable(
);
export const ShortcutsPage = Loadable(
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'),
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Settings'),
);
export const InstalledIntegrations = Loadable(

View File

@@ -7,12 +7,9 @@ import {
AlertOverview,
AllAlertChannels,
AllErrors,
APIKeys,
ApiMonitoring,
BillingPage,
CreateAlertChannelAlerts,
CreateNewAlerts,
CustomDomainSettings,
DashboardPage,
DashboardWidget,
EditAlertChannelsAlerts,
@@ -20,7 +17,6 @@ import {
ErrorDetails,
Home,
InfrastructureMonitoring,
IngestionSettings,
InstalledIntegrations,
LicensePage,
ListAllALertsPage,
@@ -31,12 +27,10 @@ import {
LogsIndexToFields,
LogsSaveViews,
MetricsExplorer,
MySettings,
NewDashboardPage,
OldLogsExplorer,
Onboarding,
OnboardingV2,
OrganizationSettings,
OrgOnboarding,
PasswordReset,
PipelinePage,
@@ -45,7 +39,6 @@ import {
ServicesTablePage,
ServiceTopLevelOperationsPage,
SettingsPage,
ShortcutsPage,
SignupPage,
SomethingWentWrong,
StatusPage,
@@ -150,7 +143,7 @@ const routes: AppRoutes[] = [
},
{
path: ROUTES.SETTINGS,
exact: true,
exact: false,
component: SettingsPage,
isPrivate: true,
key: 'SETTINGS',
@@ -295,41 +288,6 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'VERSION',
},
{
path: ROUTES.ORG_SETTINGS,
exact: true,
component: OrganizationSettings,
isPrivate: true,
key: 'ORG_SETTINGS',
},
{
path: ROUTES.INGESTION_SETTINGS,
exact: true,
component: IngestionSettings,
isPrivate: true,
key: 'INGESTION_SETTINGS',
},
{
path: ROUTES.API_KEYS,
exact: true,
component: APIKeys,
isPrivate: true,
key: 'API_KEYS',
},
{
path: ROUTES.MY_SETTINGS,
exact: true,
component: MySettings,
isPrivate: true,
key: 'MY_SETTINGS',
},
{
path: ROUTES.CUSTOM_DOMAIN_SETTINGS,
exact: true,
component: CustomDomainSettings,
isPrivate: true,
key: 'CUSTOM_DOMAIN_SETTINGS',
},
{
path: ROUTES.LOGS,
exact: true,
@@ -393,13 +351,6 @@ const routes: AppRoutes[] = [
key: 'SOMETHING_WENT_WRONG',
isPrivate: false,
},
{
path: ROUTES.BILLING,
exact: true,
component: BillingPage,
key: 'BILLING',
isPrivate: true,
},
{
path: ROUTES.WORKSPACE_LOCKED,
exact: true,
@@ -421,13 +372,6 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'WORKSPACE_ACCESS_RESTRICTED',
},
{
path: ROUTES.SHORTCUTS,
exact: true,
component: ShortcutsPage,
isPrivate: true,
key: 'SHORTCUTS',
},
{
path: ROUTES.INTEGRATIONS,
exact: true,

View File

@@ -1,18 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetAllOrgPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
const getAllOrgPreferences = async (): Promise<
SuccessResponse<GetAllOrgPreferencesResponseProps> | ErrorResponse
> => {
const response = await axios.get(`/org/preferences`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getAllOrgPreferences;

View File

@@ -1,18 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetAllUserPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
const getAllUserPreferences = async (): Promise<
SuccessResponse<GetAllUserPreferencesResponseProps> | ErrorResponse
> => {
const response = await axios.get(`/user/preferences`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getAllUserPreferences;

View File

@@ -1,20 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetOrgPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
const getOrgPreference = async ({
preferenceID,
}: {
preferenceID: string;
}): Promise<SuccessResponse<GetOrgPreferenceResponseProps> | ErrorResponse> => {
const response = await axios.get(`/org/preferences/${preferenceID}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getOrgPreference;

View File

@@ -1,22 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetUserPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
const getUserPreference = async ({
preferenceID,
}: {
preferenceID: string;
}): Promise<
SuccessResponse<GetUserPreferenceResponseProps> | ErrorResponse
> => {
const response = await axios.get(`/user/preferences/${preferenceID}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getUserPreference;

View File

@@ -1,28 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
UpdateOrgPreferenceProps,
UpdateOrgPreferenceResponseProps,
} from 'types/api/preferences/userOrgPreferences';
const updateOrgPreference = async (
preferencePayload: UpdateOrgPreferenceProps,
): Promise<
SuccessResponse<UpdateOrgPreferenceResponseProps> | ErrorResponse
> => {
const response = await axios.put(
`/org/preferences/${preferencePayload.preferenceID}`,
{
preference_value: preferencePayload.value,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateOrgPreference;

View File

@@ -1,28 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
UpdateUserPreferenceProps,
UpdateUserPreferenceResponseProps,
} from 'types/api/preferences/userOrgPreferences';
const updateUserPreference = async (
preferencePayload: UpdateUserPreferenceProps,
): Promise<
SuccessResponse<UpdateUserPreferenceResponseProps> | ErrorResponse
> => {
const response = await axios.put(
`/user/preferences/${preferencePayload.preferenceID}`,
{
preference_value: preferencePayload.value,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateUserPreference;

View File

@@ -196,8 +196,6 @@ export interface FunnelOverviewResponse {
avg_rate: number;
conversion_rate: number | null;
errors: number;
// TODO(shaheer): remove p99_latency once we have support for latency
p99_latency: number;
latency: number;
};
}>;

View File

@@ -0,0 +1,23 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps } from 'types/api/preferences/list';
import { OrgPreference } from 'types/api/preferences/preference';
const listPreference = async (): Promise<
SuccessResponseV2<OrgPreference[]>
> => {
try {
const response = await axios.get<PayloadProps>(`/org/preferences`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default listPreference;

View File

@@ -0,0 +1,25 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/preferences/get';
import { OrgPreference } from 'types/api/preferences/preference';
const getPreference = async (
props: Props,
): Promise<SuccessResponseV2<OrgPreference>> => {
try {
const response = await axios.get<PayloadProps>(
`/org/preferences/${props.name}`,
);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getPreference;

View File

@@ -0,0 +1,22 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Props } from 'types/api/preferences/update';
const update = async (props: Props): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.put(`/org/preferences/${props.name}`, {
value: props.value,
});
return {
httpStatusCode: response.status,
data: null,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default update;

View File

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

View File

@@ -0,0 +1,21 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps } from 'types/api/preferences/list';
import { UserPreference } from 'types/api/preferences/preference';
const list = async (): Promise<SuccessResponseV2<UserPreference[]>> => {
try {
const response = await axios.get<PayloadProps>(`/user/preferences`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default list;

View File

@@ -0,0 +1,25 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/preferences/get';
import { UserPreference } from 'types/api/preferences/preference';
const get = async (
props: Props,
): Promise<SuccessResponseV2<UserPreference>> => {
try {
const response = await axios.get<PayloadProps>(
`/user/preferences/${props.name}`,
);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default get;

View File

@@ -0,0 +1,22 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Props } from 'types/api/preferences/update';
const update = async (props: Props): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.put(`/user/preferences/${props.name}`, {
value: props.value,
});
return {
httpStatusCode: response.status,
data: null,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default update;

View File

@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react';
import ROUTES from 'constants/routes';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { DataSource } from 'types/common/queryBuilder';
@@ -52,11 +53,32 @@ jest.mock('hooks/saveViews/useDeleteView', () => ({
})),
}));
// Mock usePreferenceSync
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
usePreferenceSync: (): any => ({
preferences: {
columns: [],
formatting: {
maxLines: 2,
format: 'table',
fontSize: 'small',
version: 1,
},
},
loading: false,
error: null,
updateColumns: jest.fn(),
updateFormatting: jest.fn(),
}),
}));
describe('ExplorerCard', () => {
it('renders a card with a title and a description', () => {
render(
<MockQueryClientProvider>
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
<PreferenceContextProvider>
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
</PreferenceContextProvider>
</MockQueryClientProvider>,
);
expect(screen.queryByText('Query Builder')).not.toBeInTheDocument();
@@ -65,7 +87,9 @@ describe('ExplorerCard', () => {
it('renders a save view button', () => {
render(
<MockQueryClientProvider>
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
<PreferenceContextProvider>
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
</PreferenceContextProvider>
</MockQueryClientProvider>,
);
expect(screen.queryByText('Save view')).not.toBeInTheDocument();

View File

@@ -6,6 +6,7 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import isEqual from 'lodash-es/isEqual';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import {
DeleteViewHandlerProps,
@@ -106,7 +107,11 @@ export const isQueryUpdatedInView = ({
!isEqual(
options?.selectColumns,
extraData && JSON.parse(extraData)?.selectColumns,
)
) ||
(stagedQuery?.builder?.queryData?.[0]?.dataSource === DataSource.LOGS &&
(!isEqual(options?.format, extraData && JSON.parse(extraData)?.format) ||
!isEqual(options?.maxLines, extraData && JSON.parse(extraData)?.maxLines) ||
!isEqual(options?.fontSize, extraData && JSON.parse(extraData)?.fontSize)))
);
};

View File

@@ -74,6 +74,7 @@ const formatMap = {
'MM/dd HH:mm': DATE_TIME_FORMATS.SLASH_SHORT,
'MM/DD': DATE_TIME_FORMATS.DATE_SHORT,
'YY-MM': DATE_TIME_FORMATS.YEAR_MONTH,
'MMM d, yyyy, h:mm:ss aaaa': DATE_TIME_FORMATS.DASH_DATETIME,
YY: DATE_TIME_FORMATS.YEAR_SHORT,
};

View File

@@ -1,7 +1,12 @@
import { Tabs, TabsProps } from 'antd';
import { useLocation, useParams } from 'react-router-dom';
import { RouteTabProps } from './types';
interface Params {
[key: string]: string;
}
function RouteTab({
routes,
activeKey,
@@ -9,19 +14,38 @@ function RouteTab({
history,
...rest
}: RouteTabProps & TabsProps): JSX.Element {
const params = useParams<Params>();
const location = useLocation();
// Replace dynamic parameters in routes
const routesWithParams = routes.map((route) => ({
...route,
route: route.route.replace(
/:(\w+)/g,
(match, param) => params[param] || match,
),
}));
// Find the matching route for the current pathname
const currentRoute = routesWithParams.find((route) => {
const routePattern = route.route.replace(/:(\w+)/g, '([^/]+)');
const regex = new RegExp(`^${routePattern}$`);
return regex.test(location.pathname);
});
const onChange = (activeRoute: string): void => {
if (onChangeHandler) {
onChangeHandler(activeRoute);
}
const selectedRoute = routes.find((e) => e.key === activeRoute);
const selectedRoute = routesWithParams.find((e) => e.key === activeRoute);
if (selectedRoute) {
history.push(selectedRoute.route);
}
};
const items = routes.map(({ Component, name, route, key }) => ({
const items = routesWithParams.map(({ Component, name, route, key }) => ({
label: name,
key,
tabKey: route,
@@ -32,8 +56,8 @@ function RouteTab({
<Tabs
onChange={onChange}
destroyInactiveTabPane
activeKey={activeKey}
defaultActiveKey={activeKey}
activeKey={currentRoute?.key || activeKey}
defaultActiveKey={currentRoute?.key || activeKey}
animated
items={items}
// eslint-disable-next-line react/jsx-props-no-spreading

View File

@@ -0,0 +1,18 @@
export const ORG_PREFERENCES = {
ORG_ONBOARDING: 'org_onboarding',
WELCOME_CHECKLIST_DO_LATER: 'welcome_checklist_do_later',
WELCOME_CHECKLIST_SEND_LOGS_SKIPPED: 'welcome_checklist_send_logs_skipped',
WELCOME_CHECKLIST_SEND_TRACES_SKIPPED: 'welcome_checklist_send_traces_skipped',
WELCOME_CHECKLIST_SETUP_ALERTS_SKIPPED:
'welcome_checklist_setup_alerts_skipped',
WELCOME_CHECKLIST_SETUP_SAVED_VIEW_SKIPPED:
'welcome_checklist_setup_saved_view_skipped',
WELCOME_CHECKLIST_SEND_INFRA_METRICS_SKIPPED:
'welcome_checklist_send_infra_metrics_skipped',
WELCOME_CHECKLIST_SETUP_DASHBOARDS_SKIPPED:
'welcome_checklist_setup_dashboards_skipped',
WELCOME_CHECKLIST_SETUP_WORKSPACE_SKIPPED:
'welcome_checklist_setup_workspace_skipped',
WELCOME_CHECKLIST_ADD_DATA_SOURCE_SKIPPED:
'welcome_checklist_add_data_source_skipped',
};

View File

@@ -425,3 +425,79 @@ export const metricsEmptyTimeAggregateOperatorOptions: SelectOption<
string,
string
>[] = [];
export const metricsUnknownTimeAggregateOperatorOptions: SelectOption<
string,
string
>[] = [
{
value: MetricAggregateOperator.MAX,
label: 'Max',
},
{
value: MetricAggregateOperator.MIN,
label: 'Min',
},
{
value: MetricAggregateOperator.SUM,
label: 'Sum',
},
{
value: MetricAggregateOperator.AVG,
label: 'Avg',
},
{
value: MetricAggregateOperator.COUNT,
label: 'Count',
},
{
value: MetricAggregateOperator.RATE,
label: 'Rate',
},
{
value: MetricAggregateOperator.INCREASE,
label: 'Increase',
},
];
export const metricsUnknownSpaceAggregateOperatorOptions: SelectOption<
string,
string
>[] = [
{
value: MetricAggregateOperator.SUM,
label: 'Sum',
},
{
value: MetricAggregateOperator.AVG,
label: 'Avg',
},
{
value: MetricAggregateOperator.MIN,
label: 'Min',
},
{
value: MetricAggregateOperator.MAX,
label: 'Max',
},
{
value: MetricAggregateOperator.P50,
label: 'P50',
},
{
value: MetricAggregateOperator.P75,
label: 'P75',
},
{
value: MetricAggregateOperator.P90,
label: 'P90',
},
{
value: MetricAggregateOperator.P95,
label: 'P95',
},
{
value: MetricAggregateOperator.P99,
label: 'P99',
},
];

View File

@@ -29,12 +29,12 @@ const ROUTES = {
ALERT_OVERVIEW: '/alerts/overview',
ALL_CHANNELS: '/settings/channels',
CHANNELS_NEW: '/settings/channels/new',
CHANNELS_EDIT: '/settings/channels/:id',
CHANNELS_EDIT: '/settings/channels/edit/:id',
ALL_ERROR: '/exceptions',
ERROR_DETAIL: '/error-detail',
VERSION: '/status',
MY_SETTINGS: '/my-settings',
SETTINGS: '/settings',
MY_SETTINGS: '/settings/my-settings',
ORG_SETTINGS: '/settings/org-settings',
CUSTOM_DOMAIN_SETTINGS: '/settings/custom-domain-settings',
API_KEYS: '/settings/api-keys',
@@ -52,7 +52,7 @@ const ROUTES = {
LIST_LICENSES: '/licenses',
LOGS_INDEX_FIELDS: '/logs-explorer/index-fields',
TRACE_EXPLORER: '/trace-explorer',
BILLING: '/billing',
BILLING: '/settings/billing',
SUPPORT: '/support',
LOGS_SAVE_VIEWS: '/logs/saved-views',
TRACES_SAVE_VIEWS: '/traces/saved-views',
@@ -60,7 +60,7 @@ const ROUTES = {
TRACES_FUNNELS_DETAIL: '/traces/funnels/:funnelId',
WORKSPACE_LOCKED: '/workspace-locked',
WORKSPACE_SUSPENDED: '/workspace-suspended',
SHORTCUTS: '/shortcuts',
SHORTCUTS: '/settings/shortcuts',
INTEGRATIONS: '/integrations',
MESSAGING_QUEUES_KAFKA: '/messaging-queues/kafka',
MESSAGING_QUEUES_KAFKA_DETAIL: '/messaging-queues/kafka/detail',

View File

@@ -0,0 +1,4 @@
export const USER_PREFERENCES = {
SIDENAV_PINNED: 'sidenav_pinned',
NAV_SHORTCUTS: 'nav_shortcuts',
};

View File

@@ -7,7 +7,7 @@ import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { useCallback, useState } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { Channels } from 'types/api/channels/getAll';
@@ -17,12 +17,11 @@ import Delete from './Delete';
function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
const { t } = useTranslation(['channels']);
const { notifications } = useNotifications();
const [channels, setChannels] = useState<Channels[]>(allChannels);
const { user } = useAppContext();
const [action] = useComponentPermission(['new_alert_action'], user.role);
const onClickEditHandler = useCallback((id: string) => {
history.replace(
history.push(
generatePath(ROUTES.CHANNELS_EDIT, {
id,
}),
@@ -56,14 +55,19 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
<Button onClick={(): void => onClickEditHandler(id)} type="link">
{t('column_channel_edit')}
</Button>
<Delete id={id} setChannels={setChannels} notifications={notifications} />
<Delete id={id} notifications={notifications} />
</>
),
});
}
return (
<ResizeTable columns={columns} dataSource={channels} rowKey="id" bordered />
<ResizeTable
columns={columns}
dataSource={allChannels}
rowKey="id"
bordered
/>
);
}

View File

@@ -0,0 +1,4 @@
.alert-channels-container {
width: 90%;
margin: 12px auto;
}

View File

@@ -1,14 +1,15 @@
import { Button } from 'antd';
import { NotificationInstance } from 'antd/es/notification/interface';
import deleteChannel from 'api/channels/delete';
import { Dispatch, SetStateAction, useState } from 'react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Channels } from 'types/api/channels/getAll';
import { useQueryClient } from 'react-query';
import APIError from 'types/api/error';
function Delete({ notifications, setChannels, id }: DeleteProps): JSX.Element {
function Delete({ notifications, id }: DeleteProps): JSX.Element {
const { t } = useTranslation(['channels']);
const [loading, setLoading] = useState(false);
const queryClient = useQueryClient();
const onClickHandler = async (): Promise<void> => {
try {
@@ -21,7 +22,8 @@ function Delete({ notifications, setChannels, id }: DeleteProps): JSX.Element {
message: 'Success',
description: t('channel_delete_success'),
});
setChannels((preChannels) => preChannels.filter((e) => e.id !== id));
// Invalidate and refetch
queryClient.invalidateQueries(['getChannels']);
setLoading(false);
} catch (error) {
notifications.error({
@@ -46,7 +48,6 @@ function Delete({ notifications, setChannels, id }: DeleteProps): JSX.Element {
interface DeleteProps {
notifications: NotificationInstance;
setChannels: Dispatch<SetStateAction<Channels[]>>;
id: string;
}

View File

@@ -1,3 +1,5 @@
import './AllAlertChannels.styles.scss';
import { PlusOutlined } from '@ant-design/icons';
import { Tooltip, Typography } from 'antd';
import getAll from 'api/channels/getAll';
@@ -56,7 +58,7 @@ function AlertChannels(): JSX.Element {
}
return (
<>
<div className="alert-channels-container">
<ButtonContainer>
<Paragraph ellipsis type="secondary">
{t('sending_channels_note')}
@@ -87,7 +89,7 @@ function AlertChannels(): JSX.Element {
</ButtonContainer>
<AlertChannelsComponent allChannels={data?.data || []} />
</>
</div>
);
}

View File

@@ -22,6 +22,12 @@
width: 100%;
}
}
&.side-nav-pinned {
.app-content {
width: calc(100% - 240px);
}
}
}
.chat-support-gateway {

View File

@@ -18,6 +18,7 @@ import { Events } from 'constants/events';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import ROUTES from 'constants/routes';
import { USER_PREFERENCES } from 'constants/userPreferences';
import SideNav from 'container/SideNav';
import TopNav from 'container/TopNav';
import dayjs from 'dayjs';
@@ -27,7 +28,6 @@ import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { isNull } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { INTEGRATION_TYPES } from 'pages/Integrations/utils';
import { useAppContext } from 'providers/App/App';
import {
ReactNode,
@@ -41,7 +41,7 @@ import { Helmet } from 'react-helmet-async';
import { useTranslation } from 'react-i18next';
import { useMutation, useQueries } from 'react-query';
import { useDispatch } from 'react-redux';
import { matchPath, useLocation } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import {
@@ -80,6 +80,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
userPreferences,
} = useAppContext();
const { notifications } = useNotifications();
@@ -330,53 +331,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
});
}, [manageCreditCard]);
const isHome = (): boolean => routeKey === 'HOME';
const isLogsView = (): boolean =>
routeKey === 'LOGS' ||
routeKey === 'LOGS_EXPLORER' ||
routeKey === 'LOGS_PIPELINES' ||
routeKey === 'LOGS_SAVE_VIEWS';
const isApiMonitoringView = (): boolean => routeKey === 'API_MONITORING';
const isExceptionsView = (): boolean => routeKey === 'ALL_ERROR';
const isTracesView = (): boolean =>
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
const isMessagingQueues = (): boolean =>
routeKey === 'MESSAGING_QUEUES_KAFKA' ||
routeKey === 'MESSAGING_QUEUES_KAFKA_DETAIL' ||
routeKey === 'MESSAGING_QUEUES_CELERY_TASK' ||
routeKey === 'MESSAGING_QUEUES_OVERVIEW';
const isCloudIntegrationPage = (): boolean =>
routeKey === 'INTEGRATIONS' &&
new URLSearchParams(window.location.search).get('integration') ===
INTEGRATION_TYPES.AWS_INTEGRATION;
const isDashboardListView = (): boolean => routeKey === 'ALL_DASHBOARD';
const isAlertHistory = (): boolean => routeKey === 'ALERT_HISTORY';
const isAlertOverview = (): boolean => routeKey === 'ALERT_OVERVIEW';
const isInfraMonitoring = (): boolean =>
routeKey === 'INFRASTRUCTURE_MONITORING_HOSTS' ||
routeKey === 'INFRASTRUCTURE_MONITORING_KUBERNETES';
const isTracesFunnels = (): boolean => routeKey === 'TRACES_FUNNELS';
const isTracesFunnelDetails = (): boolean =>
!!matchPath(pathname, ROUTES.TRACES_FUNNELS_DETAIL);
const isPathMatch = (regex: RegExp): boolean => regex.test(pathname);
const isDashboardView = (): boolean =>
isPathMatch(/^\/dashboard\/[a-zA-Z0-9_-]+$/);
const isDashboardWidgetView = (): boolean =>
isPathMatch(/^\/dashboard\/[a-zA-Z0-9_-]+\/new$/);
const isTraceDetailsView = (): boolean =>
isPathMatch(/^\/trace\/[a-zA-Z0-9]+(\?.*)?$/);
useEffect(() => {
if (isDarkMode) {
document.body.classList.remove('lightMode');
@@ -593,6 +547,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</div>
);
const sideNavPinned = userPreferences?.find(
(preference) => preference.name === USER_PREFERENCES.SIDENAV_PINNED,
)?.value as boolean;
return (
<Layout className={cx(isDarkMode ? 'darkMode dark' : 'lightMode')}>
<Helmet>
@@ -645,9 +603,15 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
)}
<Flex
className={cx('app-layout', isDarkMode ? 'darkMode dark' : 'lightMode')}
className={cx(
'app-layout',
isDarkMode ? 'darkMode dark' : 'lightMode',
sideNavPinned ? 'side-nav-pinned' : '',
)}
>
{isToDisplayLayout && !renderFullScreen && <SideNav />}
{isToDisplayLayout && !renderFullScreen && (
<SideNav isPinned={sideNavPinned} />
)}
<div
className={cx('app-content', {
'full-screen-content': renderFullScreen,
@@ -657,32 +621,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<LayoutContent data-overlayscrollbars-initialize>
<OverlayScrollbar>
<ChildrenContainer
style={{
margin:
isHome() ||
isLogsView() ||
isTracesView() ||
isDashboardView() ||
isDashboardWidgetView() ||
isDashboardListView() ||
isAlertHistory() ||
isAlertOverview() ||
isMessagingQueues() ||
isCloudIntegrationPage() ||
isInfraMonitoring() ||
isApiMonitoringView() ||
isExceptionsView()
? 0
: '0 1rem',
...(isTraceDetailsView() ||
isTracesFunnels() ||
isTracesFunnelDetails()
? { margin: 0 }
: {}),
}}
>
<ChildrenContainer>
{isToDisplayLayout && !renderFullScreen && <TopNav />}
{children}
</ChildrenContainer>

View File

@@ -1,7 +1,8 @@
.billing-container {
margin-bottom: 40px;
padding-top: 36px;
width: 65%;
width: 90%;
margin: 0 auto;
.billing-summary {
margin: 24px 8px;

View File

@@ -0,0 +1,15 @@
.create-alert-channels-container {
width: 90%;
margin: 12px auto;
border: 1px solid var(--Slate-500, #161922);
background: var(--Ink-400, #121317);
border-radius: 3px;
padding: 16px;
.form-alert-channels-title {
margin-top: 0px;
margin-bottom: 16px;
}
}

View File

@@ -1,3 +1,5 @@
import './CreateAlertChannels.styles.scss';
import { Form } from 'antd';
import createEmail from 'api/channels/createEmail';
import createMsTeamsApi from 'api/channels/createMsTeams';
@@ -477,26 +479,28 @@ function CreateAlertChannels({
);
return (
<FormAlertChannels
{...{
formInstance,
onTypeChangeHandler,
setSelectedConfig,
type,
onTestHandler,
onSaveHandler,
savingState,
testingState,
title: t('page_title_create'),
initialValue: {
<div className="create-alert-channels-container">
<FormAlertChannels
{...{
formInstance,
onTypeChangeHandler,
setSelectedConfig,
type,
...selectedConfig,
...PagerInitialConfig,
...OpsgenieInitialConfig,
...EmailInitialConfig,
},
}}
/>
onTestHandler,
onSaveHandler,
savingState,
testingState,
title: t('page_title_create'),
initialValue: {
type,
...selectedConfig,
...PagerInitialConfig,
...OpsgenieInitialConfig,
...EmailInitialConfig,
},
}}
/>
</div>
);
}

View File

@@ -24,6 +24,10 @@ import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import ExportPanelContainer from 'container/ExportPanel/ExportPanelContainer';
import {
MetricsExplorerEventKeys,
MetricsExplorerEvents,
} from 'container/MetricsExplorer/events';
import { useOptionsMenu } from 'container/OptionsMenu';
import {
defaultLogsSelectedColumns,
@@ -50,6 +54,7 @@ import {
X,
} from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { FormattingOptions } from 'providers/preferences/types';
import {
CSSProperties,
Dispatch,
@@ -140,7 +145,9 @@ function ExplorerOptions({
panelType,
});
} else if (isMetricsExplorer) {
logEvent('Metrics Explorer: Save view clicked', {
logEvent(MetricsExplorerEvents.SaveViewClicked, {
[MetricsExplorerEventKeys.Tab]: 'explorer',
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
panelType,
});
}
@@ -184,8 +191,10 @@ function ExplorerOptions({
panelType,
});
} else if (isMetricsExplorer) {
logEvent('Metrics Explorer: Create alert', {
logEvent(MetricsExplorerEvents.AddToAlertClicked, {
panelType,
[MetricsExplorerEventKeys.Tab]: 'explorer',
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
});
}
@@ -218,11 +227,14 @@ function ExplorerOptions({
panelType,
});
} else if (isMetricsExplorer) {
logEvent('Metrics Explorer: Add to dashboard clicked', {
logEvent(MetricsExplorerEvents.AddToDashboardClicked, {
panelType,
[MetricsExplorerEventKeys.Tab]: 'explorer',
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
});
}
setIsExport(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLogsExplorer, isMetricsExplorer, panelType, setIsExport, sourcepage]);
const {
@@ -259,17 +271,26 @@ function ExplorerOptions({
const getUpdatedExtraData = (
extraData: string | undefined,
newSelectedColumns: BaseAutocompleteData[],
formattingOptions?: FormattingOptions,
): string => {
let updatedExtraData;
if (extraData) {
const parsedExtraData = JSON.parse(extraData);
parsedExtraData.selectColumns = newSelectedColumns;
if (formattingOptions) {
parsedExtraData.format = formattingOptions.format;
parsedExtraData.maxLines = formattingOptions.maxLines;
parsedExtraData.fontSize = formattingOptions.fontSize;
}
updatedExtraData = JSON.stringify(parsedExtraData);
} else {
updatedExtraData = JSON.stringify({
color: Color.BG_SIENNA_500,
selectColumns: newSelectedColumns,
format: formattingOptions?.format,
maxLines: formattingOptions?.maxLines,
fontSize: formattingOptions?.fontSize,
});
}
return updatedExtraData;
@@ -278,6 +299,14 @@ function ExplorerOptions({
const updatedExtraData = getUpdatedExtraData(
extraData,
options?.selectColumns,
// pass this only for logs
sourcepage === DataSource.LOGS
? {
format: options?.format,
maxLines: options?.maxLines,
fontSize: options?.fontSize,
}
: undefined,
);
const {
@@ -506,6 +535,14 @@ function ExplorerOptions({
color,
selectColumns: options.selectColumns,
version: 1,
...// pass this only for logs
(sourcepage === DataSource.LOGS
? {
format: options?.format,
maxLines: options?.maxLines,
fontSize: options?.fontSize,
}
: {}),
}),
notifications,
panelType: panelType || PANEL_TYPES.LIST,

View File

@@ -57,7 +57,9 @@ function FormAlertChannels({
return (
<>
<Typography.Title level={3}>{title}</Typography.Title>
<Typography.Title level={4} className="form-alert-channels-title">
{title}
</Typography.Title>
<Form initialValues={initialValue} layout="vertical" form={formInstance}>
<Form.Item label={t('field_channel_name')} labelAlign="left" name="name">

View File

@@ -85,7 +85,13 @@ function LabelSelect({
}, [handleBlur]);
const handleLabelChange = (event: ChangeEvent<HTMLInputElement>): void => {
setCurrentVal(event.target?.value.replace(':', ''));
// Remove the colon if it's the last character.
// As the colon is used to separate the key and value in the query.
setCurrentVal(
event.target?.value.endsWith(':')
? event.target?.value.slice(0, -1)
: event.target?.value,
);
};
const handleClose = (key: string): void => {

View File

@@ -6,12 +6,13 @@ import { Alert, Button, Popover } from 'antd';
import logEvent from 'api/common/logEvent';
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
import { K8sPodsListPayload } from 'api/infraMonitoring/getK8sPodsList';
import getAllUserPreferences from 'api/preferences/getAllUserPreference';
import updateUserPreferenceAPI from 'api/preferences/updateUserPreference';
import listUserPreferences from 'api/v1/user/preferences/list';
import updateUserPreferenceAPI from 'api/v1/user/preferences/name/update';
import Header from 'components/Header/Header';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { FeatureKeys } from 'constants/features';
import { LOCALSTORAGE } from 'constants/localStorage';
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import ROUTES from 'constants/routes';
@@ -29,8 +30,8 @@ import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useMutation, useQuery } from 'react-query';
import { UserPreference } from 'types/api/preferences/preference';
import { DataSource } from 'types/common/queryBuilder';
import { UserPreference } from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -184,18 +185,25 @@ export default function Home(): JSX.Element {
);
const processUserPreferences = (userPreferences: UserPreference[]): void => {
const checklistSkipped = userPreferences?.find(
(preference) => preference.key === 'WELCOME_CHECKLIST_DO_LATER',
)?.value;
const checklistSkipped = Boolean(
userPreferences?.find(
(preference) =>
preference.name === ORG_PREFERENCES.WELCOME_CHECKLIST_DO_LATER,
)?.value,
);
const updatedChecklistItems = cloneDeep(checklistItems);
const newChecklistItems = updatedChecklistItems.map((item) => {
const newItem = { ...item };
newItem.isSkipped =
const isSkipped = Boolean(
userPreferences?.find(
(preference) => preference.key === item.skippedPreferenceKey,
)?.value || false;
(preference) => preference.name === item.skippedPreferenceKey,
)?.value,
);
newItem.isSkipped = isSkipped || false;
return newItem;
});
@@ -206,13 +214,13 @@ export default function Home(): JSX.Element {
// Fetch User Preferences
const { refetch: refetchUserPreferences } = useQuery({
queryFn: () => getAllUserPreferences(),
queryFn: () => listUserPreferences(),
queryKey: ['getUserPreferences'],
enabled: true,
refetchOnWindowFocus: false,
onSuccess: (response) => {
if (response.payload && response.payload.data) {
processUserPreferences(response.payload.data);
if (response.data) {
processUserPreferences(response.data);
}
setLoadingUserPreferences(false);
@@ -239,7 +247,7 @@ export default function Home(): JSX.Element {
setUpdatingUserPreferences(true);
updateUserPreference({
preferenceID: 'WELCOME_CHECKLIST_DO_LATER',
name: ORG_PREFERENCES.WELCOME_CHECKLIST_DO_LATER,
value: true,
});
};
@@ -249,7 +257,7 @@ export default function Home(): JSX.Element {
setUpdatingUserPreferences(true);
updateUserPreference({
preferenceID: item.skippedPreferenceKey,
name: item.skippedPreferenceKey,
value: true,
});
}

View File

@@ -1,17 +1,19 @@
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes';
import { ChecklistItem } from './HomeChecklist/HomeChecklist';
export const checkListStepToPreferenceKeyMap = {
WILL_DO_LATER: 'WELCOME_CHECKLIST_DO_LATER',
SEND_LOGS: 'WELCOME_CHECKLIST_SEND_LOGS_SKIPPED',
SEND_TRACES: 'WELCOME_CHECKLIST_SEND_TRACES_SKIPPED',
SEND_INFRA_METRICS: 'WELCOME_CHECKLIST_SEND_INFRA_METRICS_SKIPPED',
SETUP_DASHBOARDS: 'WELCOME_CHECKLIST_SETUP_DASHBOARDS_SKIPPED',
SETUP_ALERTS: 'WELCOME_CHECKLIST_SETUP_ALERTS_SKIPPED',
SETUP_SAVED_VIEWS: 'WELCOME_CHECKLIST_SETUP_SAVED_VIEW_SKIPPED',
SETUP_WORKSPACE: 'WELCOME_CHECKLIST_SETUP_WORKSPACE_SKIPPED',
ADD_DATA_SOURCE: 'WELCOME_CHECKLIST_ADD_DATA_SOURCE_SKIPPED',
WILL_DO_LATER: ORG_PREFERENCES.WELCOME_CHECKLIST_DO_LATER,
SEND_LOGS: ORG_PREFERENCES.WELCOME_CHECKLIST_SEND_LOGS_SKIPPED,
SEND_TRACES: ORG_PREFERENCES.WELCOME_CHECKLIST_SEND_TRACES_SKIPPED,
SEND_INFRA_METRICS:
ORG_PREFERENCES.WELCOME_CHECKLIST_SEND_INFRA_METRICS_SKIPPED,
SETUP_DASHBOARDS: ORG_PREFERENCES.WELCOME_CHECKLIST_SETUP_DASHBOARDS_SKIPPED,
SETUP_ALERTS: ORG_PREFERENCES.WELCOME_CHECKLIST_SETUP_ALERTS_SKIPPED,
SETUP_SAVED_VIEWS: ORG_PREFERENCES.WELCOME_CHECKLIST_SETUP_SAVED_VIEW_SKIPPED,
SETUP_WORKSPACE: ORG_PREFERENCES.WELCOME_CHECKLIST_SETUP_WORKSPACE_SKIPPED,
ADD_DATA_SOURCE: ORG_PREFERENCES.WELCOME_CHECKLIST_ADD_DATA_SOURCE_SKIPPED,
};
export const DOCS_LINKS = {

View File

@@ -0,0 +1,91 @@
.licenses-page {
max-height: 100vh;
overflow: hidden;
.licenses-page-header {
border-bottom: 1px solid var(--Slate-500, #161922);
background: rgba(11, 12, 14, 0.7);
backdrop-filter: blur(20px);
.licenses-page-header-title {
color: var(--Vanilla-100, #fff);
text-align: center;
font-family: Inter;
font-size: 13px;
font-style: normal;
line-height: 14px;
letter-spacing: 0.4px;
display: flex;
align-items: center;
gap: 8px;
padding: 16px;
}
}
.licenses-page-content-container {
display: flex;
flex-direction: row;
align-items: flex-start;
.licenses-page-content {
flex: 1;
height: calc(100vh - 48px);
background: var(--Ink-500, #0b0c0e);
padding: 10px 8px;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.3rem;
height: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
}
}
}
.lightMode {
.licenses-page {
.licenses-page-header {
border-bottom: 1px solid var(--bg-vanilla-300);
background: #fff;
backdrop-filter: blur(20px);
.licenses-page-header-title {
color: var(--bg-ink-400);
background: var(--bg-vanilla-100);
border-right: 1px solid var(--bg-vanilla-300);
}
}
.licenses-page-content-container {
.licenses-page-content {
background: var(--bg-vanilla-100);
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
}
}
}
}

View File

@@ -1,5 +1,7 @@
import { Tabs } from 'antd';
import './Licenses.styles.scss';
import Spinner from 'components/Spinner';
import { Wrench } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useTranslation } from 'react-i18next';
@@ -13,16 +15,19 @@ function Licenses(): JSX.Element {
return <Spinner tip={t('loading_licenses')} height="90vh" />;
}
const tabs = [
{
label: t('tab_current_license'),
key: 'licenses',
children: <ApplyLicenseForm licenseRefetch={activeLicenseRefetch} />,
},
];
return (
<Tabs destroyInactiveTabPane defaultActiveKey="licenses" items={tabs} />
<div className="licenses-page">
<header className="licenses-page-header">
<div className="licenses-page-header-title">
<Wrench size={16} />
License
</div>
</header>
<div className="licenses-page-content-container">
<ApplyLicenseForm licenseRefetch={activeLicenseRefetch} />
</div>
</div>
);
}

View File

@@ -3,8 +3,7 @@ import styled from 'styled-components';
export const ApplyFormContainer = styled.div`
&&& {
padding-top: 1em;
padding-bottom: 1em;
padding: 16px;
}
`;

View File

@@ -114,7 +114,6 @@ function LogsExplorerViews({
// Context
const {
initialDataSource,
currentQuery,
stagedQuery,
panelType,
@@ -144,7 +143,7 @@ function LogsExplorerViews({
const { options, config } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
dataSource: initialDataSource || DataSource.LOGS,
dataSource: DataSource.LOGS,
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
});

View File

@@ -5,6 +5,7 @@ import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_qu
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import { QueryBuilderContext } from 'providers/QueryBuilder';
import { VirtuosoMockContext } from 'react-virtuoso';
import { fireEvent, render, RenderResult } from 'tests/test-utils';
@@ -87,6 +88,25 @@ jest.mock('hooks/useSafeNavigate', () => ({
}),
}));
// Mock usePreferenceSync
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
usePreferenceSync: (): any => ({
preferences: {
columns: [],
formatting: {
maxLines: 2,
format: 'table',
fontSize: 'small',
version: 1,
},
},
loading: false,
error: null,
updateColumns: jest.fn(),
updateFormatting: jest.fn(),
}),
}));
jest.mock('hooks/logs/useCopyLogLink', () => ({
useCopyLogLink: jest.fn().mockReturnValue({
activeLogId: ACTIVE_LOG_ID,
@@ -105,13 +125,15 @@ const renderer = (): RenderResult =>
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
<LogsExplorerViews
selectedView={SELECTED_VIEWS.SEARCH}
showFrequencyChart
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
/>
<PreferenceContextProvider>
<LogsExplorerViews
selectedView={SELECTED_VIEWS.SEARCH}
showFrequencyChart
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
/>
</PreferenceContextProvider>
</VirtuosoMockContext.Provider>,
);
@@ -184,13 +206,15 @@ describe('LogsExplorerViews -', () => {
lodsQueryServerRequest();
render(
<QueryBuilderContext.Provider value={mockQueryBuilderContextValue}>
<LogsExplorerViews
selectedView={SELECTED_VIEWS.SEARCH}
showFrequencyChart
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
/>
<PreferenceContextProvider>
<LogsExplorerViews
selectedView={SELECTED_VIEWS.SEARCH}
showFrequencyChart
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
/>
</PreferenceContextProvider>
</QueryBuilderContext.Provider>,
);

View File

@@ -5,6 +5,7 @@ import { logsPaginationQueryRangeSuccessResponse } from 'mocks-server/__mockdata
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import { I18nextProvider } from 'react-i18next';
import i18n from 'ReactI18';
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
@@ -108,11 +109,13 @@ describe('LogsPanelComponent', () => {
render(
<I18nextProvider i18n={i18n}>
<DashboardProvider>
<NewWidget
selectedGraph={PANEL_TYPES.LIST}
fillSpans={undefined}
yAxisUnit={undefined}
/>
<PreferenceContextProvider>
<NewWidget
selectedGraph={PANEL_TYPES.LIST}
fillSpans={undefined}
yAxisUnit={undefined}
/>
</PreferenceContextProvider>
</DashboardProvider>
</I18nextProvider>,
);

View File

@@ -2,6 +2,7 @@ import './Explorer.styles.scss';
import * as Sentry from '@sentry/react';
import { Switch } from 'antd';
import logEvent from 'api/common/logEvent';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
@@ -10,7 +11,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -18,6 +19,7 @@ import { DataSource } from 'types/common/queryBuilder';
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
import { v4 as uuid } from 'uuid';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import QuerySection from './QuerySection';
import TimeSeries from './TimeSeries';
import { ExplorerTabs } from './types';
@@ -93,6 +95,12 @@ function Explorer(): JSX.Element {
[stagedQuery],
);
useEffect(() => {
logEvent(MetricsExplorerEvents.TabChanged, {
[MetricsExplorerEventKeys.Tab]: 'explorer',
});
}, []);
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="metrics-explorer-explore-container">

View File

@@ -1,4 +1,5 @@
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QueryBuilder } from 'container/QueryBuilder';
import { ButtonWrapper } from 'container/TracesExplorer/QuerySection/styles';
@@ -6,6 +7,8 @@ import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQ
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { DataSource } from 'types/common/queryBuilder';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
function QuerySection(): JSX.Element {
const { handleRunQuery } = useQueryBuilder();
@@ -19,7 +22,15 @@ function QuerySection(): JSX.Element {
version="v4"
actions={
<ButtonWrapper>
<Button onClick={(): void => handleRunQuery()} type="primary">
<Button
onClick={(): void => {
handleRunQuery();
logEvent(MetricsExplorerEvents.QueryBuilderQueryChanged, {
[MetricsExplorerEventKeys.Tab]: 'explorer',
});
}}
type="primary"
>
Run Query
</Button>
</ButtonWrapper>

View File

@@ -4,6 +4,7 @@
import { Color } from '@signozhq/design-tokens';
import { Card, Tooltip, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import logEvent from 'api/common/logEvent';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import classNames from 'classnames';
import ResizeTable from 'components/ResizeTable/ResizeTable';
@@ -11,6 +12,7 @@ import { DataType } from 'container/LogDetailedView/TableView';
import { ArrowDownCircle, ArrowRightCircle, Focus } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import {
SPACE_AGGREGATION_OPTIONS_FOR_EXPANDED_VIEW,
TIME_AGGREGATION_OPTIONS,
@@ -39,6 +41,21 @@ function ExpandedView({
setSelectedTimeSeries,
] = useState<InspectMetricsSeries | null>(null);
useEffect(() => {
logEvent(MetricsExplorerEvents.InspectPointClicked, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
[MetricsExplorerEventKeys.Filters]: metricInspectionOptions.filters,
[MetricsExplorerEventKeys.TimeAggregationInterval]:
metricInspectionOptions.timeAggregationInterval,
[MetricsExplorerEventKeys.TimeAggregationOption]:
metricInspectionOptions.timeAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationOption]:
metricInspectionOptions.spaceAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationLabels]:
metricInspectionOptions.spaceAggregationLabels,
});
}, [metricInspectionOptions]);
useEffect(() => {
if (step !== InspectionStep.COMPLETED) {
setSelectedTimeSeries(options?.timeSeries ?? null);

View File

@@ -1,5 +1,6 @@
import { Color } from '@signozhq/design-tokens';
import { Button, Skeleton, Switch, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import Uplot from 'components/Uplot';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
@@ -8,6 +9,7 @@ import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
import { METRIC_TYPE_TO_COLOR_MAP, METRIC_TYPE_TO_ICON_MAP } from './constants';
import GraphPopover from './GraphPopover';
@@ -203,7 +205,14 @@ function GraphView({
<div className="view-toggle-button">
<Switch
checked={viewType === 'graph'}
onChange={(checked): void => setViewType(checked ? 'graph' : 'table')}
onChange={(checked): void => {
const newViewType = checked ? 'graph' : 'table';
setViewType(newViewType);
logEvent(MetricsExplorerEvents.InspectViewChanged, {
[MetricsExplorerEventKeys.Tab]: 'inspect',
[MetricsExplorerEventKeys.InspectView]: newViewType,
});
}}
/>
<Typography.Text>
{viewType === 'graph' ? 'Graph View' : 'Table View'}

View File

@@ -3,6 +3,7 @@ import './Inspect.styles.scss';
import * as Sentry from '@sentry/react';
import { Color } from '@signozhq/design-tokens';
import { Button, Drawer, Empty, Skeleton, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetMetricDetails } from 'hooks/metricsExplorer/useGetMetricDetails';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
@@ -11,11 +12,16 @@ import { Compass } from 'lucide-react';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import ExpandedView from './ExpandedView';
import GraphView from './GraphView';
import QueryBuilder from './QueryBuilder';
import Stepper from './Stepper';
import { GraphPopoverOptions, InspectProps } from './types';
import {
GraphPopoverOptions,
InspectProps,
MetricInspectionAction,
} from './types';
import { useInspectMetrics } from './useInspectMetrics';
function Inspect({
@@ -92,6 +98,25 @@ function Inspect({
reset,
} = useInspectMetrics(metricName);
const handleDispatchMetricInspectionOptions = useCallback(
(action: MetricInspectionAction): void => {
dispatchMetricInspectionOptions(action);
logEvent(MetricsExplorerEvents.InspectQueryChanged, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
[MetricsExplorerEventKeys.Filters]: metricInspectionOptions.filters,
[MetricsExplorerEventKeys.TimeAggregationInterval]:
metricInspectionOptions.timeAggregationInterval,
[MetricsExplorerEventKeys.TimeAggregationOption]:
metricInspectionOptions.timeAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationOption]:
metricInspectionOptions.spaceAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationLabels]:
metricInspectionOptions.spaceAggregationLabels,
});
},
[dispatchMetricInspectionOptions, metricInspectionOptions],
);
const selectedMetricType = useMemo(
() => metricDetailsData?.payload?.data?.metadata?.metric_type,
[metricDetailsData],
@@ -186,7 +211,7 @@ function Inspect({
setMetricName={setMetricName}
spaceAggregationLabels={spaceAggregationLabels}
metricInspectionOptions={metricInspectionOptions}
dispatchMetricInspectionOptions={dispatchMetricInspectionOptions}
dispatchMetricInspectionOptions={handleDispatchMetricInspectionOptions}
inspectionStep={inspectionStep}
inspectMetricsTimeSeries={inspectMetricsTimeSeries}
searchQuery={searchQuery}
@@ -227,12 +252,18 @@ function Inspect({
popoverOptions,
metricInspectionOptions,
spaceAggregationLabels,
dispatchMetricInspectionOptions,
handleDispatchMetricInspectionOptions,
searchQuery,
expandedViewOptions,
timeAggregatedSeriesMap,
]);
useEffect(() => {
logEvent(MetricsExplorerEvents.ModalOpened, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
});
}, []);
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<Drawer

View File

@@ -1,5 +1,6 @@
/* eslint-disable no-nested-ternary */
import { Card, Input, Select, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import classNames from 'classnames';
@@ -16,6 +17,7 @@ import {
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import {
SPACE_AGGREGATION_OPTIONS,
TIME_AGGREGATION_OPTIONS,
@@ -135,6 +137,9 @@ export function MetricFilters({
}}
onChange={(value): void => {
handleChangeQueryData('filters', value);
logEvent(MetricsExplorerEvents.FilterApplied, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
});
dispatchMetricInspectionOptions({
type: 'SET_FILTERS',
payload: value,

View File

@@ -1,5 +1,6 @@
import { Button, Collapse, Input, Menu, Popover, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import logEvent from 'api/common/logEvent';
import { ResizeTable } from 'components/ResizeTable';
import { DataType } from 'container/LogDetailedView/TableView';
import { useNotifications } from 'hooks/useNotifications';
@@ -10,6 +11,7 @@ import { useCopyToClipboard } from 'react-use';
import { PANEL_TYPES } from '../../../constants/queryBuilder';
import ROUTES from '../../../constants/routes';
import { useHandleExplorerTabChange } from '../../../hooks/useHandleExplorerTabChange';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import { AllAttributesProps, AllAttributesValueProps } from './types';
import { getMetricDetailsQuery } from './utils';
@@ -135,9 +137,16 @@ function AllAttributes({
},
ROUTES.METRICS_EXPLORER_EXPLORER,
);
logEvent(MetricsExplorerEvents.OpenInExplorerClicked, {
[MetricsExplorerEventKeys.MetricName]: metricName,
[MetricsExplorerEventKeys.Tab]: 'summary',
[MetricsExplorerEventKeys.Modal]: 'metric-details',
[MetricsExplorerEventKeys.AttributeKey]: groupBy,
});
},
[metricName, metricType, handleExplorerTabChange],
);
const goToMetricsExploreWithAppliedAttribute = useCallback(
(key: string, value: string) => {
const compositeQuery = getMetricDetailsQuery(metricName, metricType, {
@@ -153,6 +162,13 @@ function AllAttributes({
},
ROUTES.METRICS_EXPLORER_EXPLORER,
);
logEvent(MetricsExplorerEvents.OpenInExplorerClicked, {
[MetricsExplorerEventKeys.MetricName]: metricName,
[MetricsExplorerEventKeys.Tab]: 'summary',
[MetricsExplorerEventKeys.Modal]: 'metric-details',
[MetricsExplorerEventKeys.AttributeKey]: key,
[MetricsExplorerEventKeys.AttributeValue]: value,
});
},
[metricName, metricType, handleExplorerTabChange],
);

View File

@@ -1,5 +1,6 @@
import { Button, Collapse, Input, Select, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import logEvent from 'api/common/logEvent';
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { UpdateMetricMetadataProps } from 'api/metricsExplorer/updateMetricMetadata';
@@ -11,6 +12,7 @@ import { useNotifications } from 'hooks/useNotifications';
import { Edit2, Save, X } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import {
METRIC_TYPE_LABEL_MAP,
METRIC_TYPE_VALUES_MAP,
@@ -170,6 +172,11 @@ function Metadata({
{
onSuccess: (response): void => {
if (response?.statusCode === 200) {
logEvent(MetricsExplorerEvents.MetricMetadataUpdated, {
[MetricsExplorerEventKeys.MetricName]: metricName,
[MetricsExplorerEventKeys.Tab]: 'summary',
[MetricsExplorerEventKeys.Modal]: 'metric-details',
});
notifications.success({
message: 'Metadata updated successfully',
});

View File

@@ -11,14 +11,16 @@ import {
Tooltip,
Typography,
} from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetMetricDetails } from 'hooks/metricsExplorer/useGetMetricDetails';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Compass, Crosshair, X } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { PANEL_TYPES } from '../../../constants/queryBuilder';
import ROUTES from '../../../constants/routes';
import { useHandleExplorerTabChange } from '../../../hooks/useHandleExplorerTabChange';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import { isInspectEnabled } from '../Inspect/utils';
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
import AllAttributes from './AllAttributes';
@@ -95,11 +97,22 @@ function MetricDetails({
},
ROUTES.METRICS_EXPLORER_EXPLORER,
);
logEvent(MetricsExplorerEvents.OpenInExplorerClicked, {
[MetricsExplorerEventKeys.MetricName]: metricName,
[MetricsExplorerEventKeys.Tab]: 'summary',
[MetricsExplorerEventKeys.Modal]: 'metric-details',
});
}
}, [metricName, handleExplorerTabChange, metric?.metadata?.metric_type]);
const isMetricDetailsError = metricDetailsError || !metric;
useEffect(() => {
logEvent(MetricsExplorerEvents.ModalOpened, {
[MetricsExplorerEventKeys.Modal]: 'metric-details',
});
}, []);
return (
<Drawer
width="60%"

View File

@@ -10,24 +10,21 @@ import {
} from 'antd';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetMetricsListFilterValues } from 'hooks/metricsExplorer/useGetMetricsListFilterValues';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Search } from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { COMPOSITE_QUERY_KEY } from './constants';
import { SUMMARY_FILTERS_KEY } from './constants';
function MetricNameSearch(): JSX.Element {
const { currentQuery } = useQueryBuilder();
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
entityVersion: '',
});
const [, setSearchParams] = useSearchParams();
function MetricNameSearch({
queryFilters,
}: {
queryFilters: TagFilter;
}): JSX.Element {
const [searchParams, setSearchParams] = useSearchParams();
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
const [searchString, setSearchString] = useState<string>('');
@@ -70,9 +67,9 @@ function MetricNameSearch(): JSX.Element {
const handleSelect = useCallback(
(selectedMetricName: string): void => {
const newFilter = {
const newFilters = {
items: [
...currentQuery.builder.queryData[0].filters.items,
...queryFilters.items,
{
id: 'metric_name',
op: 'CONTAINS',
@@ -84,27 +81,15 @@ function MetricNameSearch(): JSX.Element {
value: selectedMetricName,
},
],
op: 'AND',
op: 'and',
};
const compositeQuery = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
filters: newFilter,
},
],
},
};
handleChangeQueryData('filters', newFilter);
setSearchParams({
[COMPOSITE_QUERY_KEY]: JSON.stringify(compositeQuery),
...Object.fromEntries(searchParams.entries()),
[SUMMARY_FILTERS_KEY]: JSON.stringify(newFilters),
});
setIsPopoverOpen(false);
},
[currentQuery, handleChangeQueryData, setSearchParams],
[queryFilters.items, setSearchParams, searchParams],
);
const metricNameFilterValues = useMemo(
@@ -198,7 +183,7 @@ function MetricNameSearch(): JSX.Element {
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>): void => {
const value = e.target.value.trim().toLowerCase();
const value = e.target.value.trim();
setSearchString(value);
debouncedUpdate(value);
},

View File

@@ -1,26 +1,23 @@
import { Button, Menu, Popover, Tooltip } from 'antd';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { Search } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import {
COMPOSITE_QUERY_KEY,
METRIC_TYPE_LABEL_MAP,
METRIC_TYPE_VALUES_MAP,
SUMMARY_FILTERS_KEY,
} from './constants';
function MetricTypeSearch(): JSX.Element {
const { currentQuery } = useQueryBuilder();
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
entityVersion: '',
});
function MetricTypeSearch({
queryFilters,
}: {
queryFilters: TagFilter;
}): JSX.Element {
const [searchParams, setSearchParams] = useSearchParams();
const [, setSearchParams] = useSearchParams();
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
const menuItems = useMemo(
@@ -40,9 +37,9 @@ function MetricTypeSearch(): JSX.Element {
const handleSelect = useCallback(
(selectedMetricType: string): void => {
if (selectedMetricType !== 'all') {
const newFilter = {
const newFilters = {
items: [
...currentQuery.builder.queryData[0].filters.items,
...queryFilters.items,
{
id: 'metric_type',
op: '=',
@@ -56,49 +53,23 @@ function MetricTypeSearch(): JSX.Element {
],
op: 'AND',
};
const compositeQuery = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
filters: newFilter,
},
],
},
};
handleChangeQueryData('filters', newFilter);
setSearchParams({
[COMPOSITE_QUERY_KEY]: JSON.stringify(compositeQuery),
...Object.fromEntries(searchParams.entries()),
[SUMMARY_FILTERS_KEY]: JSON.stringify(newFilters),
});
} else {
const newFilter = {
items: currentQuery.builder.queryData[0].filters.items.filter(
(item) => item.id !== 'metric_type',
),
const newFilters = {
items: queryFilters.items.filter((item) => item.id !== 'metric_type'),
op: 'AND',
};
const compositeQuery = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
filters: newFilter,
},
],
},
};
handleChangeQueryData('filters', newFilter);
setSearchParams({
[COMPOSITE_QUERY_KEY]: JSON.stringify(compositeQuery),
...Object.fromEntries(searchParams.entries()),
[SUMMARY_FILTERS_KEY]: JSON.stringify(newFilters),
});
}
setIsPopoverOpen(false);
},
[currentQuery, handleChangeQueryData, setSearchParams],
[queryFilters.items, setSearchParams, searchParams],
);
const menu = (

View File

@@ -12,7 +12,7 @@ import { Info } from 'lucide-react';
import { useCallback } from 'react';
import { MetricsListItemRowData, MetricsTableProps } from './types';
import { metricsTableColumns } from './utils';
import { getMetricsTableColumns } from './utils';
function MetricsTable({
isLoading,
@@ -24,6 +24,7 @@ function MetricsTable({
setOrderBy,
totalCount,
openMetricDetails,
queryFilters,
}: MetricsTableProps): JSX.Element {
const handleTableChange: TableProps<MetricsListItemRowData>['onChange'] = useCallback(
(
@@ -74,7 +75,7 @@ function MetricsTable({
),
}}
dataSource={data}
columns={metricsTableColumns}
columns={getMetricsTableColumns(queryFilters)}
locale={{
emptyText: isLoading ? null : (
<div
@@ -107,7 +108,7 @@ function MetricsTable({
total: totalCount,
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => openMetricDetails(record.key),
onClick: (): void => openMetricDetails(record.key, 'list'),
className: 'clickable-row',
})}
/>

View File

@@ -154,7 +154,7 @@ function MetricsTreemap({
<foreignObject
width={nodeWidth}
height={nodeHeight}
onClick={(): void => openMetricDetails(node.data.id)}
onClick={(): void => openMetricDetails(node.data.id, 'treemap')}
>
<div
style={{

View File

@@ -1,29 +1,27 @@
import './Summary.styles.scss';
import * as Sentry from '@sentry/react';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import logEvent from 'api/common/logEvent';
import { initialQueriesMap } from 'constants/queryBuilder';
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
import { useGetMetricsTreeMap } from 'hooks/metricsExplorer/useGetMetricsTreeMap';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import InspectModal from '../Inspect';
import MetricDetails from '../MetricDetails';
import {
COMPOSITE_QUERY_KEY,
IS_INSPECT_MODAL_OPEN_KEY,
IS_METRIC_DETAILS_OPEN_KEY,
SELECTED_METRIC_NAME_KEY,
SUMMARY_FILTERS_KEY,
} from './constants';
import MetricsSearch from './MetricsSearch';
import MetricsTable from './MetricsTable';
@@ -63,57 +61,37 @@ function Summary(): JSX.Element {
(state) => state.globalTime,
);
const { currentQuery, updateAllQueriesOperators } = useQueryBuilder();
const defaultQuery = useMemo(() => {
const query = updateAllQueriesOperators(
initialQueriesMap.metrics,
PANEL_TYPES.LIST,
DataSource.METRICS,
);
const queryFilters: TagFilter = useMemo(() => {
const encodedFilters = searchParams.get(SUMMARY_FILTERS_KEY);
if (encodedFilters) {
return JSON.parse(encodedFilters);
}
return {
...query,
builder: {
...query.builder,
queryData: [
{
...query.builder.queryData[0],
orderBy: [DEFAULT_ORDER_BY],
},
],
},
items: [],
op: 'AND',
};
}, [updateAllQueriesOperators]);
}, [searchParams]);
useShareBuilderUrl(defaultQuery);
useEffect(() => {
logEvent(MetricsExplorerEvents.TabChanged, {
[MetricsExplorerEventKeys.Tab]: 'summary',
});
}, []);
useEffect(() => {
logEvent(MetricsExplorerEvents.TimeUpdated, {
[MetricsExplorerEventKeys.Tab]: 'summary',
});
}, [maxTime, minTime]);
// This is used to avoid the filters from being serialized with the id
const currentQueryFiltersString = useMemo(() => {
const filters = currentQuery?.builder?.queryData[0]?.filters;
if (!filters) return '';
const queryFiltersWithoutId = useMemo(() => {
const filtersWithoutId = {
...filters,
items: filters.items.map(({ id, ...rest }) => rest),
...queryFilters,
items: queryFilters.items.map(({ id, ...rest }) => rest),
};
return JSON.stringify(filtersWithoutId);
}, [currentQuery]);
const queryFilters = useMemo(
() =>
currentQuery?.builder?.queryData[0]?.filters || {
items: [],
op: 'and',
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[currentQueryFiltersString],
);
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
entityVersion: '',
});
}, [queryFilters]);
const metricsListQuery = useMemo(() => {
const baseQuery = getMetricsListQuery();
@@ -146,6 +124,15 @@ function Summary(): JSX.Element {
isError: isMetricsError,
} = useGetMetricsList(metricsListQuery, {
enabled: !!metricsListQuery && !isInspectModalOpen,
queryKey: [
'metricsList',
queryFiltersWithoutId,
orderBy,
pageSize,
currentPage,
minTime,
maxTime,
],
});
const isListViewError = useMemo(
@@ -160,6 +147,13 @@ function Summary(): JSX.Element {
isError: isTreeMapError,
} = useGetMetricsTreeMap(metricsTreemapQuery, {
enabled: !!metricsTreemapQuery && !isInspectModalOpen,
queryKey: [
'metricsTreemap',
queryFiltersWithoutId,
heatmapView,
minTime,
maxTime,
],
});
const isProportionViewError = useMemo(
@@ -169,51 +163,37 @@ function Summary(): JSX.Element {
const handleFilterChange = useCallback(
(value: TagFilter) => {
handleChangeQueryData('filters', value);
const compositeQuery = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
filters: value,
},
],
},
};
setSearchParams({
[COMPOSITE_QUERY_KEY]: JSON.stringify(compositeQuery),
...Object.fromEntries(searchParams.entries()),
[SUMMARY_FILTERS_KEY]: JSON.stringify(value),
});
setCurrentPage(1);
logEvent(MetricsExplorerEvents.FilterApplied, {
[MetricsExplorerEventKeys.Tab]: 'summary',
});
},
[handleChangeQueryData, currentQuery, setSearchParams],
[setSearchParams, searchParams],
);
const updatedCurrentQuery = useMemo(
const searchQuery = useMemo(
() => ({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
aggregateOperator: 'noop',
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
},
],
},
...initialQueriesMap.metrics.builder.queryData[0],
filters: queryFilters,
}),
[currentQuery],
[queryFilters],
);
const searchQuery = updatedCurrentQuery?.builder?.queryData[0] || null;
const onPaginationChange = (page: number, pageSize: number): void => {
setCurrentPage(page);
setPageSize(pageSize);
logEvent(MetricsExplorerEvents.PageNumberChanged, {
[MetricsExplorerEventKeys.Tab]: 'summary',
[MetricsExplorerEventKeys.PageNumber]: page,
});
logEvent(MetricsExplorerEvents.PageSizeChanged, {
[MetricsExplorerEventKeys.Tab]: 'summary',
[MetricsExplorerEventKeys.PageSize]: pageSize,
});
};
const formattedMetricsData = useMemo(
@@ -221,19 +201,28 @@ function Summary(): JSX.Element {
[metricsData],
);
const openMetricDetails = (metricName: string): void => {
const openMetricDetails = (
metricName: string,
view: 'list' | 'treemap',
): void => {
setSelectedMetricName(metricName);
setIsMetricDetailsOpen(true);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[IS_METRIC_DETAILS_OPEN_KEY]: 'true',
[SELECTED_METRIC_NAME_KEY]: metricName,
});
logEvent(MetricsExplorerEvents.MetricClicked, {
[MetricsExplorerEventKeys.MetricName]: metricName,
[MetricsExplorerEventKeys.View]: view,
});
};
const closeMetricDetails = (): void => {
setSelectedMetricName(null);
setIsMetricDetailsOpen(false);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[IS_METRIC_DETAILS_OPEN_KEY]: 'false',
[SELECTED_METRIC_NAME_KEY]: '',
});
@@ -244,24 +233,39 @@ function Summary(): JSX.Element {
setIsInspectModalOpen(true);
setIsMetricDetailsOpen(false);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[IS_INSPECT_MODAL_OPEN_KEY]: 'true',
[SELECTED_METRIC_NAME_KEY]: metricName,
});
};
const closeInspectModal = (): void => {
handleChangeQueryData('filters', {
items: [],
op: 'AND',
});
setIsInspectModalOpen(false);
setSelectedMetricName(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[IS_INSPECT_MODAL_OPEN_KEY]: 'false',
[SELECTED_METRIC_NAME_KEY]: '',
});
};
const handleSetHeatmapView = (view: TreemapViewType): void => {
setHeatmapView(view);
logEvent(MetricsExplorerEvents.TreemapViewChanged, {
[MetricsExplorerEventKeys.Tab]: 'summary',
[MetricsExplorerEventKeys.ViewType]: view,
});
};
const handleSetOrderBy = (orderBy: OrderByPayload): void => {
setOrderBy(orderBy);
logEvent(MetricsExplorerEvents.OrderByApplied, {
[MetricsExplorerEventKeys.Tab]: 'summary',
[MetricsExplorerEventKeys.ColumnName]: orderBy.columnName,
[MetricsExplorerEventKeys.Order]: orderBy.order,
});
};
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="metrics-explorer-summary-tab">
@@ -272,7 +276,7 @@ function Summary(): JSX.Element {
isError={isProportionViewError}
viewType={heatmapView}
openMetricDetails={openMetricDetails}
setHeatmapView={setHeatmapView}
setHeatmapView={handleSetHeatmapView}
/>
<MetricsTable
isLoading={isMetricsLoading || isMetricsFetching}
@@ -281,9 +285,10 @@ function Summary(): JSX.Element {
pageSize={pageSize}
currentPage={currentPage}
onPaginationChange={onPaginationChange}
setOrderBy={setOrderBy}
setOrderBy={handleSetOrderBy}
totalCount={metricsData?.payload?.data?.total || 0}
openMetricDetails={openMetricDetails}
queryFilters={queryFilters}
/>
</div>
{isMetricDetailsOpen && (

View File

@@ -4,6 +4,7 @@ import * as useQueryBuilderOperationsHooks from 'hooks/queryBuilder/useQueryBuil
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import store from 'store';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import MetricsTable from '../MetricsTable';
import { MetricsListItemRowData } from '../types';
@@ -29,6 +30,11 @@ const mockData: MetricsListItemRowData[] = [
},
];
const mockQueryFilters: TagFilter = {
items: [],
op: 'AND',
};
jest.mock('react-router-dom-v5-compat', () => {
const actual = jest.requireActual('react-router-dom-v5-compat');
return {
@@ -76,6 +82,7 @@ describe('MetricsTable', () => {
setOrderBy={jest.fn()}
totalCount={2}
openMetricDetails={jest.fn()}
queryFilters={mockQueryFilters}
/>
</Provider>
</MemoryRouter>,
@@ -99,6 +106,7 @@ describe('MetricsTable', () => {
setOrderBy={jest.fn()}
totalCount={2}
openMetricDetails={jest.fn()}
queryFilters={mockQueryFilters}
isLoading
/>
</Provider>
@@ -122,6 +130,7 @@ describe('MetricsTable', () => {
setOrderBy={jest.fn()}
totalCount={2}
openMetricDetails={jest.fn()}
queryFilters={mockQueryFilters}
/>
</Provider>
</MemoryRouter>,
@@ -149,6 +158,7 @@ describe('MetricsTable', () => {
setOrderBy={jest.fn()}
totalCount={2}
openMetricDetails={jest.fn()}
queryFilters={mockQueryFilters}
/>
</Provider>
</MemoryRouter>,
@@ -177,13 +187,14 @@ describe('MetricsTable', () => {
setOrderBy={jest.fn()}
totalCount={2}
openMetricDetails={mockOpenMetricDetails}
queryFilters={mockQueryFilters}
/>
</Provider>
</MemoryRouter>,
);
fireEvent.click(screen.getByText('Metric 1'));
expect(mockOpenMetricDetails).toHaveBeenCalledWith('metric1');
expect(mockOpenMetricDetails).toHaveBeenCalledWith('metric1', 'list');
});
it('calls setOrderBy when column header is clicked', () => {
@@ -201,6 +212,7 @@ describe('MetricsTable', () => {
setOrderBy={mockSetOrderBy}
totalCount={2}
openMetricDetails={jest.fn()}
queryFilters={mockQueryFilters}
/>
</Provider>
</MemoryRouter>,

View File

@@ -1,45 +1,61 @@
import { Color } from '@signozhq/design-tokens';
import { render } from '@testing-library/react';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { TreemapViewType } from '../types';
import {
formatDataForMetricsTable,
metricsTableColumns,
getMetricsTableColumns,
MetricTypeRenderer,
} from '../utils';
describe('metricsTableColumns', () => {
const mockQueryFilters: TagFilter = {
items: [],
op: 'AND',
};
it('should have correct column definitions', () => {
expect(metricsTableColumns).toHaveLength(6);
expect(getMetricsTableColumns(mockQueryFilters)).toHaveLength(6);
// Metric Name column
expect(metricsTableColumns[0].dataIndex).toBe('metric_name');
expect(metricsTableColumns[0].width).toBe(400);
expect(metricsTableColumns[0].sorter).toBe(false);
expect(getMetricsTableColumns(mockQueryFilters)[0].dataIndex).toBe(
'metric_name',
);
expect(getMetricsTableColumns(mockQueryFilters)[0].width).toBe(400);
expect(getMetricsTableColumns(mockQueryFilters)[0].sorter).toBe(false);
// Description column
expect(metricsTableColumns[1].dataIndex).toBe('description');
expect(metricsTableColumns[1].width).toBe(400);
expect(getMetricsTableColumns(mockQueryFilters)[1].dataIndex).toBe(
'description',
);
expect(getMetricsTableColumns(mockQueryFilters)[1].width).toBe(400);
// Type column
expect(metricsTableColumns[2].dataIndex).toBe('metric_type');
expect(metricsTableColumns[2].width).toBe(150);
expect(metricsTableColumns[2].sorter).toBe(false);
expect(getMetricsTableColumns(mockQueryFilters)[2].dataIndex).toBe(
'metric_type',
);
expect(getMetricsTableColumns(mockQueryFilters)[2].width).toBe(150);
expect(getMetricsTableColumns(mockQueryFilters)[2].sorter).toBe(false);
// Unit column
expect(metricsTableColumns[3].dataIndex).toBe('unit');
expect(metricsTableColumns[3].width).toBe(150);
expect(getMetricsTableColumns(mockQueryFilters)[3].dataIndex).toBe('unit');
expect(getMetricsTableColumns(mockQueryFilters)[3].width).toBe(150);
// Samples column
expect(metricsTableColumns[4].dataIndex).toBe(TreemapViewType.SAMPLES);
expect(metricsTableColumns[4].width).toBe(150);
expect(metricsTableColumns[4].sorter).toBe(true);
expect(getMetricsTableColumns(mockQueryFilters)[4].dataIndex).toBe(
TreemapViewType.SAMPLES,
);
expect(getMetricsTableColumns(mockQueryFilters)[4].width).toBe(150);
expect(getMetricsTableColumns(mockQueryFilters)[4].sorter).toBe(true);
// Time Series column
expect(metricsTableColumns[5].dataIndex).toBe(TreemapViewType.TIMESERIES);
expect(metricsTableColumns[5].width).toBe(150);
expect(metricsTableColumns[5].sorter).toBe(true);
expect(getMetricsTableColumns(mockQueryFilters)[5].dataIndex).toBe(
TreemapViewType.TIMESERIES,
);
expect(getMetricsTableColumns(mockQueryFilters)[5].width).toBe(150);
expect(getMetricsTableColumns(mockQueryFilters)[5].sorter).toBe(true);
});
describe('MetricTypeRenderer', () => {

View File

@@ -36,4 +36,4 @@ export const METRIC_TYPE_VALUES_MAP = {
export const IS_METRIC_DETAILS_OPEN_KEY = 'isMetricDetailsOpen';
export const IS_INSPECT_MODAL_OPEN_KEY = 'isInspectModalOpen';
export const SELECTED_METRIC_NAME_KEY = 'selectedMetricName';
export const COMPOSITE_QUERY_KEY = 'compositeQuery';
export const SUMMARY_FILTERS_KEY = 'summaryFilters';

View File

@@ -1,5 +1,5 @@
import { MetricsTreeMapResponse } from 'api/metricsExplorer/getMetricsTreeMap';
import React, { Dispatch, SetStateAction } from 'react';
import React from 'react';
import {
IBuilderQuery,
TagFilter,
@@ -12,9 +12,10 @@ export interface MetricsTableProps {
pageSize: number;
currentPage: number;
onPaginationChange: (page: number, pageSize: number) => void;
setOrderBy: Dispatch<SetStateAction<OrderByPayload>>;
setOrderBy: (orderBy: OrderByPayload) => void;
totalCount: number;
openMetricDetails: (metricName: string) => void;
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
queryFilters: TagFilter;
}
export interface MetricsSearchProps {
@@ -27,7 +28,7 @@ export interface MetricsTreemapProps {
isLoading: boolean;
isError: boolean;
viewType: TreemapViewType;
openMetricDetails: (metricName: string) => void;
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
setHeatmapView: (value: TreemapViewType) => void;
}

View File

@@ -18,18 +18,21 @@ import {
Gauge,
} from 'lucide-react';
import { useMemo } from 'react';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { METRIC_TYPE_LABEL_MAP } from './constants';
import MetricNameSearch from './MetricNameSearch';
import MetricTypeSearch from './MetricTypeSearch';
import { MetricsListItemRowData, TreemapTile, TreemapViewType } from './types';
export const metricsTableColumns: ColumnType<MetricsListItemRowData>[] = [
export const getMetricsTableColumns = (
queryFilters: TagFilter,
): ColumnType<MetricsListItemRowData>[] => [
{
title: (
<div className="metric-name-column-header">
<span className="metric-name-column-header-text">METRIC</span>
<MetricNameSearch />
<MetricNameSearch queryFilters={queryFilters} />
</div>
),
dataIndex: 'metric_name',
@@ -51,7 +54,7 @@ export const metricsTableColumns: ColumnType<MetricsListItemRowData>[] = [
title: (
<div className="metric-type-column-header">
<span className="metric-type-column-header-text">TYPE</span>
<MetricTypeSearch />
<MetricTypeSearch queryFilters={queryFilters} />
</div>
),
dataIndex: 'metric_type',

View File

@@ -0,0 +1,51 @@
/**
* This file contains all analytics events for the Metrics Explorer.
*/
export enum MetricsExplorerEvents {
TabChanged = 'Metrics Explorer: Tab visited',
ModalOpened = 'Metrics Explorer: Modal opened',
MetricClicked = 'Metrics Explorer: Metric clicked',
FilterApplied = 'Metrics Explorer: Filter applied',
TimeUpdated = 'Metrics Explorer: Time updated',
TreemapViewChanged = 'Metrics Explorer: Treemap view changed',
PageNumberChanged = 'Metrics Explorer: Page number changed',
PageSizeChanged = 'Metrics Explorer: Page size changed',
OrderByApplied = 'Metrics Explorer: Order by applied',
MetricMetadataUpdated = 'Metrics Explorer: Metric metadata updated',
OpenInExplorerClicked = 'Metrics Explorer: Open in explorer clicked',
InspectViewChanged = 'Metrics Explorer: Inspect view changed',
InspectQueryChanged = 'Metrics Explorer: Inspect query changed',
InspectPointClicked = 'Metrics Explorer: Inspect point clicked',
QueryBuilderQueryChanged = 'Metrics Explorer: QueryBuilder query changed',
YAxisUnitApplied = 'Metrics Explorer: Y axis unit applied',
AddToAlertClicked = 'Metrics Explorer: Add to alert clicked',
AddToDashboardClicked = 'Metrics Explorer: Add to dashboard clicked',
SaveViewClicked = 'Metrics Explorer: Save view clicked',
SearchApplied = 'Metrics Explorer: Search applied',
ViewEdited = 'Metrics Explorer: View edited',
ViewDeleted = 'Metrics Explorer: View deleted',
}
export enum MetricsExplorerEventKeys {
Tab = 'tab',
Modal = 'modal',
View = 'view',
Interval = 'interval',
ViewType = 'viewType',
PageNumber = 'pageNumber',
PageSize = 'pageSize',
ColumnName = 'columnName',
Order = 'order',
AttributeKey = 'attributeKey',
AttributeValue = 'attributeValue',
MetricName = 'metricName',
InspectView = 'inspectView',
TimeAggregationOption = 'timeAggregationOption',
TimeAggregationInterval = 'timeAggregationInterval',
SpaceAggregationOption = 'spaceAggregationOption',
SpaceAggregationLabels = 'spaceAggregationLabels',
OneChartPerQueryEnabled = 'oneChartPerQueryEnabled',
YAxisUnit = 'yAxisUnit',
ViewName = 'viewName',
Filters = 'filters',
}

View File

@@ -1,3 +1,12 @@
.my-settings-container {
display: flex;
flex-direction: column;
gap: 48px;
width: 80%;
margin: 12px auto;
}
.flexBtn {
display: flex;
align-items: center;
@@ -8,4 +17,163 @@
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
}
.logout-button {
display: inline-flex;
}
.user-info-section {
display: flex;
flex-direction: column;
gap: 16px;
.user-info-section-header {
display: flex;
flex-direction: column;
gap: 4px;
.user-info-section-title {
color: #fff;
font-family: Inter;
font-size: 16px;
font-style: normal;
line-height: 24px; /* 155.556% */
letter-spacing: -0.08px;
}
.user-info-section-subtitle {
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 12px;
font-style: normal;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
.user-preference-section {
display: flex;
flex-direction: column;
gap: 16px;
.user-preference-section-header {
display: flex;
flex-direction: column;
gap: 4px;
.user-preference-section-title {
color: #fff;
font-family: Inter;
font-size: 16px;
font-style: normal;
line-height: 24px; /* 155.556% */
letter-spacing: -0.08px;
}
.user-preference-section-subtitle {
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 12px;
font-style: normal;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
.user-preference-section-content {
display: flex;
flex-direction: column;
gap: 16px;
.user-preference-section-content-item {
padding: 16px;
border-radius: 4px 4px 0px 0px;
border: 1px solid var(--Slate-500, #161922);
background: var(--Ink-400, #121317);
border-radius: 3px;
.user-preference-section-content-item-title-action {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
color: var(--Vanilla-300, #eee);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: normal;
letter-spacing: -0.07px;
margin-bottom: 8px;
}
.user-preference-section-content-item-description {
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 12px;
font-style: normal;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
}
.reset-password-card {
border-radius: 0px 0px 4px 4px;
border: 1px solid var(--Slate-500, #161922);
background: var(--Ink-400, #121317);
border-radius: 3px;
}
.lightMode {
.user-info-section {
.user-info-section-header {
.user-info-section-title {
color: var(--bg-ink-400);
}
.user-info-section-subtitle {
color: var(--bg-ink-300);
}
}
}
.user-preference-section {
.user-preference-section-header {
.user-preference-section-title {
color: var(--bg-ink-400);
}
.user-preference-section-subtitle {
color: var(--bg-ink-300);
}
}
.user-preference-section-content {
.user-preference-section-content-item {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.user-preference-section-content-item-title-action {
color: var(--bg-ink-400);
}
.user-preference-section-content-item-description {
color: var(--bg-ink-300);
}
}
}
}
.reset-password-card {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}

View File

@@ -73,7 +73,7 @@ function PasswordContainer(): JSX.Element {
currentPassword === updatePassword;
return (
<Card>
<Card className="reset-password-card">
<Space direction="vertical" size="small">
<Typography.Title
level={4}

View File

@@ -1,8 +1,11 @@
.timezone-adaption {
padding: 16px;
background: var(--bg-ink-400);
border: 1px solid var(--bg-ink-500);
border-radius: 4px;
border-radius: 4px 4px 0px 0px;
border: 1px solid var(--Slate-500, #161922);
background: var(--Ink-400, #121317);
border-radius: 3px;
&__header {
display: flex;
@@ -20,7 +23,7 @@
&__description {
color: var(--bg-vanilla-400);
font-size: 14px;
font-size: 12px;
line-height: 20px;
margin: 0 0 12px 0;
}
@@ -52,7 +55,7 @@
align-items: center;
gap: 4px;
color: var(--bg-robin-400);
font-size: 14px;
font-size: 12px;
line-height: 20px;
}
&__note-text-overridden {

View File

@@ -28,14 +28,16 @@ function TimezoneAdaptation(): JSX.Element {
const handleOverrideClear = (): void => {
updateTimezone(browserTimezone);
logEvent('Settings: Timezone override cleared', {});
logEvent('Account Settings: Timezone override cleared', {});
};
const handleSwitchChange = (): void => {
setIsAdaptationEnabled((prev) => {
const isEnabled = !prev;
logEvent(
`Settings: Timezone adaptation ${isEnabled ? 'enabled' : 'disabled'}`,
`Account Settings: Timezone adaptation ${
isEnabled ? 'enabled' : 'disabled'
}`,
{},
);
return isEnabled;

View File

@@ -5,3 +5,231 @@
.userInfo-value {
min-width: 20rem;
}
.user-info-container {
border: 1px solid var(--Slate-500, #161922);
background: var(--Ink-400, #121317);
border-radius: 3px;
padding: 16px;
.user-info-card {
display: flex;
flex-direction: row;
gap: 16px;
}
.user-info-header {
font-size: 13px;
font-weight: 600;
}
.user-info {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
.user-name {
color: var(--Vanilla-100, #fff);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.user-info-subsection {
display: flex;
flex-direction: row;
gap: 20px;
.user-email {
display: flex;
align-items: center;
gap: 8px;
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.user-role {
display: flex;
align-items: center;
gap: 8px;
text-transform: capitalize;
color: var(--Vanilla-400, #c0c1c3);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
}
}
.user-info-update-section {
display: flex;
flex-direction: row;
gap: 8px;
justify-content: flex-end;
flex-wrap: wrap;
flex: 1;
}
}
.update-name-modal,
.reset-password-modal {
width: 384px !important;
.ant-modal-content {
padding: 0;
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-header {
padding: 16px;
background: var(--bg-ink-400);
border-bottom: 1px solid var(--bg-slate-500);
}
.ant-modal-body {
padding: 12px 16px 0px 16px;
.ant-typography {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 142.857% */
}
.update-name-input {
margin-top: 8px;
display: flex;
gap: 8px;
}
.reset-password-container {
display: flex;
flex-direction: column;
gap: 8px;
padding-bottom: 16px;
}
.ant-color-picker-trigger {
padding: 6px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
width: 32px;
height: 32px;
.ant-color-picker-color-block {
border-radius: 50px;
width: 16px;
height: 16px;
flex-shrink: 0;
.ant-color-picker-color-block-inner {
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
.ant-modal-footer {
display: flex;
justify-content: flex-end;
padding: 16px 16px;
margin: 0;
> button {
display: flex;
align-items: center;
border-radius: 2px;
background-color: var(--bg-robin-500) !important;
color: var(--bg-vanilla-100) !important;
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 24px;
}
}
}
.title {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
}
.lightMode {
.user-info-container {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.user-info {
.user-name {
color: var(--bg-ink-400);
}
.user-info-subsection {
.user-email {
color: var(--bg-ink-400);
}
.user-role {
color: var(--bg-ink-300);
}
}
}
}
.update-name-modal,
.reset-password-modal {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.ant-modal-header {
background: var(--bg-vanilla-100);
border-bottom: 1px solid var(--bg-vanilla-300);
}
.ant-modal-body {
.ant-typography {
color: var(--bg-ink-400);
}
.ant-color-picker-trigger {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}
}
.title {
color: var(--bg-ink-400);
}
}
}

View File

@@ -1,35 +1,115 @@
import '../MySettings.styles.scss';
import './UserInfo.styles.scss';
import { Button, Card, Flex, Input, Space, Typography } from 'antd';
import { Button, Input, Modal, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import changeMyPassword from 'api/v1/factor_password/changeMyPassword';
import editUser from 'api/v1/user/id/update';
import { useNotifications } from 'hooks/useNotifications';
import { PencilIcon } from 'lucide-react';
import { Check, FileTerminal, MailIcon, UserIcon } from 'lucide-react';
import { isPasswordValid } from 'pages/SignUp/utils';
import { useAppContext } from 'providers/App/App';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import APIError from 'types/api/error';
import { NameInput } from '../styles';
function UserInfo(): JSX.Element {
const { user, org, updateUser } = useAppContext();
const { t } = useTranslation();
const { t } = useTranslation(['routes', 'settings', 'common']);
const { notifications } = useNotifications();
const [currentPassword, setCurrentPassword] = useState<string>('');
const [updatePassword, setUpdatePassword] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isPasswordPolicyError, setIsPasswordPolicyError] = useState<boolean>(
false,
);
const [changedName, setChangedName] = useState<string>(
user?.displayName || '',
);
const [loading, setLoading] = useState<boolean>(false);
const { notifications } = useNotifications();
const [isUpdateNameModalOpen, setIsUpdateNameModalOpen] = useState<boolean>(
false,
);
const [
isResetPasswordModalOpen,
setIsResetPasswordModalOpen,
] = useState<boolean>(false);
if (!user || !org) {
const defaultPlaceHolder = '*************';
useEffect(() => {
if (currentPassword && !isPasswordValid(currentPassword)) {
setIsPasswordPolicyError(true);
} else {
setIsPasswordPolicyError(false);
}
}, [currentPassword]);
if (!user) {
return <div />;
}
const onClickUpdateHandler = async (): Promise<void> => {
const hideUpdateNameModal = (): void => {
setIsUpdateNameModalOpen(false);
};
const hideResetPasswordModal = (): void => {
setIsResetPasswordModalOpen(false);
};
const onChangePasswordClickHandler = async (): Promise<void> => {
try {
setLoading(true);
setIsLoading(true);
if (!isPasswordValid(currentPassword)) {
setIsPasswordPolicyError(true);
setIsLoading(false);
return;
}
await changeMyPassword({
newPassword: updatePassword,
oldPassword: currentPassword,
userId: user.id,
});
notifications.success({
message: t('success', {
ns: 'common',
}),
});
hideResetPasswordModal();
setIsLoading(false);
} catch (error) {
setIsLoading(false);
notifications.error({
message: (error as APIError).error.error.code,
description: (error as APIError).error.error.message,
});
}
};
const isResetPasswordDisabled =
isLoading ||
currentPassword.length === 0 ||
updatePassword.length === 0 ||
isPasswordPolicyError ||
currentPassword === updatePassword;
const onSaveHandler = async (): Promise<void> => {
logEvent('Account Settings: Name Updated', {
name: changedName,
});
logEvent(
'Account Settings: Name Updated',
{
name: changedName,
},
'identify',
);
try {
setIsLoading(true);
await editUser({
displayName: changedName,
userId: user.id,
@@ -44,80 +124,143 @@ function UserInfo(): JSX.Element {
...user,
displayName: changedName,
});
setLoading(false);
setIsLoading(false);
hideUpdateNameModal();
} catch (error) {
notifications.error({
message: (error as APIError).getErrorCode(),
description: (error as APIError).getErrorMessage(),
});
}
setLoading(false);
setIsLoading(false);
};
if (!user || !org) {
return <div />;
}
return (
<Card>
<Space direction="vertical" size="middle">
<Flex gap={8}>
<Typography.Title level={4} style={{ marginTop: 0 }}>
User Details
</Typography.Title>
</Flex>
<div className="user-info-card">
<div className="user-info">
<div className="user-name">{user.displayName}</div>
<Flex gap={16}>
<Space>
<Typography className="userInfo-label" data-testid="name-label">
Name
</Typography>
<NameInput
data-testid="name-textbox"
placeholder="Your Name"
onChange={(event): void => {
setChangedName(event.target.value);
}}
value={changedName}
disabled={loading}
/>
</Space>
<div className="user-info-subsection">
<div className="user-email">
<MailIcon size={16} /> {user.email}
</div>
<div className="user-role">
<UserIcon size={16} /> {user.role.toLowerCase()}
</div>
</div>
</div>
<div className="user-info-update-section">
<Button
type="default"
className="periscope-btn secondary"
icon={<FileTerminal size={16} />}
onClick={(): void => setIsUpdateNameModalOpen(true)}
>
Update name
</Button>
<Button
type="default"
className="periscope-btn secondary"
icon={<FileTerminal size={16} />}
onClick={(): void => setIsResetPasswordModalOpen(true)}
>
Reset password
</Button>
</div>
<Modal
className="update-name-modal"
title={<span className="title">Update name</span>}
open={isUpdateNameModalOpen}
closable
onCancel={hideUpdateNameModal}
footer={[
<Button
className="flexBtn"
loading={loading}
disabled={loading}
onClick={onClickUpdateHandler}
data-testid="update-name-button"
key="submit"
type="primary"
icon={<Check size={16} />}
onClick={onSaveHandler}
disabled={isLoading}
data-testid="update-name-btn"
>
<PencilIcon size={12} /> Update
</Button>
</Flex>
<Space>
<Typography className="userInfo-label" data-testid="email-label">
{' '}
Email{' '}
</Typography>
Update name
</Button>,
]}
>
<Typography.Text>Name</Typography.Text>
<div className="update-name-input">
<Input
className="userInfo-value"
data-testid="email-textbox"
value={user.email}
disabled
placeholder="e.g. John Doe"
value={changedName}
onChange={(e): void => setChangedName(e.target.value)}
/>
</Space>
</div>
</Modal>
<Space>
<Typography className="userInfo-label" data-testid="role-label">
{' '}
Role{' '}
</Typography>
<Input
className="userInfo-value"
value={user.role || ''}
disabled
data-testid="role-textbox"
/>
</Space>
</Space>
</Card>
<Modal
className="reset-password-modal"
title={<span className="title">Reset password</span>}
open={isResetPasswordModalOpen}
closable
onCancel={hideResetPasswordModal}
footer={[
<Button
key="submit"
className={`periscope-btn ${
isResetPasswordDisabled ? 'secondary' : 'primary'
}`}
icon={<Check size={16} />}
onClick={onChangePasswordClickHandler}
disabled={isLoading || isResetPasswordDisabled}
data-testid="reset-password-btn"
>
Reset password
</Button>,
]}
>
<div className="reset-password-container">
<div className="current-password-input">
<Typography.Text>Current password</Typography.Text>
<Input.Password
data-testid="current-password-textbox"
disabled={isLoading}
placeholder={defaultPlaceHolder}
onChange={(event): void => {
setCurrentPassword(event.target.value);
}}
value={currentPassword}
type="password"
autoComplete="off"
visibilityToggle
/>
</div>
<div className="new-password-input">
<Typography.Text>New password</Typography.Text>
<Input.Password
data-testid="new-password-textbox"
disabled={isLoading}
placeholder={defaultPlaceHolder}
onChange={(event): void => {
const updatedValue = event.target.value;
setUpdatePassword(updatedValue);
}}
value={updatePassword}
type="password"
autoComplete="off"
visibilityToggle={false}
/>
</div>
</div>
</Modal>
</div>
);
}

View File

@@ -2,17 +2,21 @@ import MySettingsContainer from 'container/MySettings';
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
const toggleThemeFunction = jest.fn();
const logEventFunction = jest.fn();
jest.mock('hooks/useDarkMode', () => ({
__esModule: true,
useIsDarkMode: jest.fn(() => ({
toggleTheme: toggleThemeFunction,
})),
useIsDarkMode: jest.fn(() => true),
default: jest.fn(() => ({
toggleTheme: toggleThemeFunction,
})),
}));
jest.mock('api/common/logEvent', () => ({
__esModule: true,
default: jest.fn((eventName, data) => logEventFunction(eventName, data)),
}));
const errorNotification = jest.fn();
const successNotification = jest.fn();
jest.mock('hooks/useNotifications', () => ({
@@ -25,90 +29,97 @@ jest.mock('hooks/useNotifications', () => ({
})),
}));
enum ThemeOptions {
Dark = 'Dark',
Light = 'Light Beta',
}
const THEME_SELECTOR_TEST_ID = 'theme-selector';
const RESET_PASSWORD_BUTTON_TEXT = 'Reset password';
const CURRENT_PASSWORD_TEST_ID = 'current-password-textbox';
const NEW_PASSWORD_TEST_ID = 'new-password-textbox';
const UPDATE_NAME_BUTTON_TEST_ID = 'update-name-btn';
const RESET_PASSWORD_BUTTON_TEST_ID = 'reset-password-btn';
const UPDATE_NAME_BUTTON_TEXT = 'Update name';
const PASSWORD_VALIDATION_MESSAGE_TEST_ID = 'password-validation-message';
describe('MySettings Flows', () => {
beforeEach(() => {
jest.clearAllMocks();
render(<MySettingsContainer />);
});
describe('Dark/Light Theme Switch', () => {
it('Should display Dark and Light theme buttons properly', async () => {
it('Should display Dark and Light theme options properly', async () => {
// Check Dark theme option
expect(screen.getByText('Dark')).toBeInTheDocument();
const darkThemeIcon = screen.getByTestId('dark-theme-icon');
expect(darkThemeIcon).toBeInTheDocument();
expect(darkThemeIcon.tagName).toBe('svg');
// Check Light theme option
expect(screen.getByText('Light')).toBeInTheDocument();
const lightThemeIcon = screen.getByTestId('light-theme-icon');
expect(lightThemeIcon).toBeInTheDocument();
expect(lightThemeIcon.tagName).toBe('svg');
expect(screen.getByText('Beta')).toBeInTheDocument();
});
it('Should activate Dark and Light buttons on click', async () => {
const initialSelectedOption = screen.getByRole('radio', {
name: ThemeOptions.Dark,
});
expect(initialSelectedOption).toBeChecked();
const newThemeOption = screen.getByRole('radio', {
name: ThemeOptions.Light,
});
fireEvent.click(newThemeOption);
expect(newThemeOption).toBeChecked();
it('Should have Dark theme selected by default', async () => {
const themeSelector = screen.getByTestId(THEME_SELECTOR_TEST_ID);
const darkOption = themeSelector.querySelector(
'input[value="dark"]',
) as HTMLInputElement;
expect(darkOption).toBeChecked();
});
it('Should switch the them on clicking Light theme', async () => {
const lightThemeOption = screen.getByRole('radio', {
name: /light/i,
});
fireEvent.click(lightThemeOption);
it('Should switch theme and log event when Light theme is selected', async () => {
const themeSelector = screen.getByTestId(THEME_SELECTOR_TEST_ID);
const lightOption = themeSelector.querySelector(
'input[value="light"]',
) as HTMLInputElement;
fireEvent.click(lightOption);
await waitFor(() => {
expect(toggleThemeFunction).toBeCalled();
expect(toggleThemeFunction).toHaveBeenCalled();
expect(logEventFunction).toHaveBeenCalledWith(
'Account Settings: Theme Changed',
{
theme: 'light',
},
);
});
});
});
describe('User Details Form', () => {
it('Should properly display the User Details Form', () => {
const userDetailsHeader = screen.getByRole('heading', {
name: /user details/i,
});
const nameLabel = screen.getByTestId('name-label');
const nameTextbox = screen.getByTestId('name-textbox');
const updateNameButton = screen.getByTestId('update-name-button');
const emailLabel = screen.getByTestId('email-label');
const emailTextbox = screen.getByTestId('email-textbox');
const roleLabel = screen.getByTestId('role-label');
const roleTextbox = screen.getByTestId('role-textbox');
// Open the Update name modal first
const updateNameButton = screen.getByText(UPDATE_NAME_BUTTON_TEXT);
fireEvent.click(updateNameButton);
// Find the label with class 'ant-typography' and text 'Name'
const nameLabels = screen.getAllByText('Name');
const nameLabel = nameLabels.find((el) =>
el.className.includes('ant-typography'),
);
const nameTextbox = screen.getByPlaceholderText('e.g. John Doe');
const modalUpdateNameButton = screen.getByTestId(UPDATE_NAME_BUTTON_TEST_ID);
expect(userDetailsHeader).toBeInTheDocument();
expect(nameLabel).toBeInTheDocument();
expect(nameTextbox).toBeInTheDocument();
expect(updateNameButton).toBeInTheDocument();
expect(emailLabel).toBeInTheDocument();
expect(emailTextbox).toBeInTheDocument();
expect(roleLabel).toBeInTheDocument();
expect(roleTextbox).toBeInTheDocument();
expect(modalUpdateNameButton).toBeInTheDocument();
});
it('Should update the name on clicking Update button', async () => {
const nameTextbox = screen.getByTestId('name-textbox');
const updateNameButton = screen.getByTestId('update-name-button');
// Open the Update name modal first
const updateNameButton = screen.getByText(UPDATE_NAME_BUTTON_TEXT);
fireEvent.click(updateNameButton);
const nameTextbox = screen.getByPlaceholderText('e.g. John Doe');
const modalUpdateNameButton = screen.getByTestId(UPDATE_NAME_BUTTON_TEST_ID);
act(() => {
fireEvent.change(nameTextbox, { target: { value: 'New Name' } });
});
fireEvent.click(updateNameButton);
fireEvent.click(modalUpdateNameButton);
await waitFor(() =>
expect(successNotification).toHaveBeenCalledWith({
@@ -119,92 +130,53 @@ describe('MySettings Flows', () => {
});
describe('Reset password', () => {
let currentPasswordTextbox: Node | Window;
let newPasswordTextbox: Node | Window;
let submitButtonElement: HTMLElement;
it('Should open password reset modal when clicking Reset password button', async () => {
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
// The first button is the one in the user info section
fireEvent.click(resetPasswordButtons[0]);
beforeEach(() => {
currentPasswordTextbox = screen.getByTestId('current-password-textbox');
newPasswordTextbox = screen.getByTestId('new-password-textbox');
submitButtonElement = screen.getByTestId('update-password-button');
});
it('Should properly display the Password Reset Form', () => {
const passwordResetHeader = screen.getByTestId('change-password-header');
expect(passwordResetHeader).toBeInTheDocument();
const currentPasswordLabel = screen.getByTestId('current-password-label');
expect(currentPasswordLabel).toBeInTheDocument();
expect(currentPasswordTextbox).toBeInTheDocument();
const newPasswordLabel = screen.getByTestId('new-password-label');
expect(newPasswordLabel).toBeInTheDocument();
expect(newPasswordTextbox).toBeInTheDocument();
expect(submitButtonElement).toBeInTheDocument();
const savePasswordIcon = screen.getByTestId('update-password-icon');
expect(savePasswordIcon).toBeInTheDocument();
expect(savePasswordIcon.tagName).toBe('svg');
// Check if modal is opened (look for modal title)
expect(
screen.getByText((content, element) =>
Boolean(
element &&
'className' in element &&
typeof element.className === 'string' &&
element.className.includes('title') &&
content === RESET_PASSWORD_BUTTON_TEXT,
),
),
).toBeInTheDocument();
expect(screen.getByTestId(CURRENT_PASSWORD_TEST_ID)).toBeInTheDocument();
expect(screen.getByTestId(NEW_PASSWORD_TEST_ID)).toBeInTheDocument();
});
it('Should display validation error if password is less than 8 characters', async () => {
const currentPasswordTextbox = screen.getByTestId(
'current-password-textbox',
);
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
const currentPasswordTextbox = screen.getByTestId(CURRENT_PASSWORD_TEST_ID);
act(() => {
fireEvent.change(currentPasswordTextbox, { target: { value: '123' } });
});
const validationMessage = await screen.findByTestId('validation-message');
await waitFor(() => {
expect(validationMessage).toHaveTextContent(
'Password must a have minimum of 8 characters',
);
// Use getByTestId for the validation message (if present in your modal/component)
if (screen.queryByTestId(PASSWORD_VALIDATION_MESSAGE_TEST_ID)) {
expect(
screen.getByTestId(PASSWORD_VALIDATION_MESSAGE_TEST_ID),
).toBeInTheDocument();
}
});
});
test("Should display 'inavlid credentials' error if different current and new passwords are provided", async () => {
act(() => {
fireEvent.change(currentPasswordTextbox, {
target: { value: '123456879' },
});
it('Should disable reset button when current and new passwords are the same', async () => {
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
fireEvent.change(newPasswordTextbox, { target: { value: '123456789' } });
});
fireEvent.click(submitButtonElement);
await waitFor(() => expect(errorNotification).toHaveBeenCalled());
});
it('Should check if the "Change Password" button is disabled in case current / new password is less than 8 characters', () => {
act(() => {
fireEvent.change(currentPasswordTextbox, {
target: { value: '123' },
});
fireEvent.change(newPasswordTextbox, { target: { value: '123' } });
});
expect(submitButtonElement).toBeDisabled();
});
test("Should check if 'Change Password' button is enabled when password is at least 8 characters ", async () => {
expect(submitButtonElement).toBeDisabled();
act(() => {
fireEvent.change(currentPasswordTextbox, {
target: { value: '123456789' },
});
fireEvent.change(newPasswordTextbox, { target: { value: '1234567890' } });
});
expect(submitButtonElement).toBeEnabled();
});
test("Should check if 'Change Password' button is disabled when current and new passwords are the same ", async () => {
expect(submitButtonElement).toBeDisabled();
const currentPasswordTextbox = screen.getByTestId(CURRENT_PASSWORD_TEST_ID);
const newPasswordTextbox = screen.getByTestId(NEW_PASSWORD_TEST_ID);
const submitButton = screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID);
act(() => {
fireEvent.change(currentPasswordTextbox, {
@@ -213,7 +185,25 @@ describe('MySettings Flows', () => {
fireEvent.change(newPasswordTextbox, { target: { value: '123456789' } });
});
expect(submitButtonElement).toBeDisabled();
expect(submitButton).toBeDisabled();
});
it('Should enable reset button when passwords are valid and different', async () => {
const resetPasswordButtons = screen.getAllByText(RESET_PASSWORD_BUTTON_TEXT);
fireEvent.click(resetPasswordButtons[0]);
const currentPasswordTextbox = screen.getByTestId(CURRENT_PASSWORD_TEST_ID);
const newPasswordTextbox = screen.getByTestId(NEW_PASSWORD_TEST_ID);
const submitButton = screen.getByTestId(RESET_PASSWORD_BUTTON_TEST_ID);
act(() => {
fireEvent.change(currentPasswordTextbox, {
target: { value: '123456789' },
});
fireEvent.change(newPasswordTextbox, { target: { value: '987654321' } });
});
expect(submitButton).not.toBeDisabled();
});
});
});

View File

@@ -1,18 +1,52 @@
import './MySettings.styles.scss';
import { Button, Radio, RadioChangeEvent, Space, Tag, Typography } from 'antd';
import { Logout } from 'api/utils';
import { Radio, RadioChangeEvent, Switch, Tag } from 'antd';
import logEvent from 'api/common/logEvent';
import updateUserPreference from 'api/v1/user/preferences/name/update';
import { AxiosError } from 'axios';
import { USER_PREFERENCES } from 'constants/userPreferences';
import useThemeMode, { useIsDarkMode } from 'hooks/useDarkMode';
import { LogOut, Moon, Sun } from 'lucide-react';
import { useState } from 'react';
import { useNotifications } from 'hooks/useNotifications';
import { Moon, Sun } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { UserPreference } from 'types/api/preferences/preference';
import { showErrorNotification } from 'utils/error';
import Password from './Password';
import TimezoneAdaptation from './TimezoneAdaptation/TimezoneAdaptation';
import UserInfo from './UserInfo';
function MySettings(): JSX.Element {
const isDarkMode = useIsDarkMode();
const { toggleTheme } = useThemeMode();
const { userPreferences, updateUserPreferenceInContext } = useAppContext();
const { notifications } = useNotifications();
const [sideNavPinned, setSideNavPinned] = useState(false);
useEffect(() => {
if (userPreferences) {
setSideNavPinned(
userPreferences.find(
(preference) => preference.name === USER_PREFERENCES.SIDENAV_PINNED,
)?.value as boolean,
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [userPreferences]);
const {
mutate: updateUserPreferenceMutation,
isLoading: isUpdatingUserPreference,
} = useMutation(updateUserPreference, {
onSuccess: () => {
// No need to do anything on success since we've already updated the state optimistically
},
onError: (error) => {
showErrorNotification(notifications, error as AxiosError);
},
});
const themeOptions = [
{
@@ -39,57 +73,112 @@ function MySettings(): JSX.Element {
const [theme, setTheme] = useState(isDarkMode ? 'dark' : 'light');
const handleThemeChange = ({ target: { value } }: RadioChangeEvent): void => {
logEvent('Account Settings: Theme Changed', {
theme: value,
});
setTheme(value);
toggleTheme();
};
const handleSideNavPinnedChange = (checked: boolean): void => {
logEvent('Account Settings: Sidebar Pinned Changed', {
pinned: checked,
});
// Optimistically update the UI
setSideNavPinned(checked);
// Update the context immediately
const save = {
name: USER_PREFERENCES.SIDENAV_PINNED,
value: checked,
};
updateUserPreferenceInContext(save as UserPreference);
// Make the API call in the background
updateUserPreferenceMutation(
{
name: USER_PREFERENCES.SIDENAV_PINNED,
value: checked,
},
{
onError: (error) => {
// Revert the state if the API call fails
setSideNavPinned(!checked);
updateUserPreferenceInContext({
name: USER_PREFERENCES.SIDENAV_PINNED,
value: !checked,
} as UserPreference);
showErrorNotification(notifications, error as AxiosError);
},
},
);
};
return (
<Space
direction="vertical"
size="large"
style={{
margin: '16px 0',
}}
>
<div className="theme-selector">
<Typography.Title
level={5}
style={{
margin: '0 0 16px 0',
}}
>
{' '}
Theme{' '}
</Typography.Title>
<Radio.Group
options={themeOptions}
onChange={handleThemeChange}
value={theme}
optionType="button"
buttonStyle="solid"
data-testid="theme-selector"
/>
<div className="my-settings-container">
<div className="user-info-section">
<div className="user-info-section-header">
<div className="user-info-section-title">General </div>
<div className="user-info-section-subtitle">
Manage your account settings.
</div>
</div>
<div className="user-info-container">
<UserInfo />
</div>
</div>
<div className="user-info-container">
<UserInfo />
<div className="user-preference-section">
<div className="user-preference-section-header">
<div className="user-preference-section-title">User Preferences</div>
<div className="user-preference-section-subtitle">
Tailor the SigNoz console to work according to your needs.
</div>
</div>
<div className="user-preference-section-content">
<div className="user-preference-section-content-item theme-selector">
<div className="user-preference-section-content-item-title-action">
Select your theme
<Radio.Group
options={themeOptions}
onChange={handleThemeChange}
value={theme}
optionType="button"
buttonStyle="solid"
data-testid="theme-selector"
size="small"
/>
</div>
<div className="user-preference-section-content-item-description">
Select if SigNoz&apos;s appearance should be light or dark
</div>
</div>
<TimezoneAdaptation />
<div className="user-preference-section-content-item">
<div className="user-preference-section-content-item-title-action">
Keep the primary sidebar always open{' '}
<Switch
checked={sideNavPinned}
onChange={handleSideNavPinnedChange}
loading={isUpdatingUserPreference}
/>
</div>
<div className="user-preference-section-content-item-description">
Keep the primary sidebar always open by default, unless collapsed with
the keyboard shortcut
</div>
</div>
</div>
</div>
<div className="password-reset-container">
<Password />
</div>
<TimezoneAdaptation />
<Button
className="flexBtn"
onClick={(): void => Logout()}
type="primary"
data-testid="logout-button"
>
<LogOut size={12} /> Logout
</Button>
</Space>
</div>
);
}

View File

@@ -0,0 +1,17 @@
.new-explorer-cta {
display: flex;
align-items: center;
color: var(--bg-vanilla-400);
/* Bifrost (Ancient)/Content/sm */
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
.ant-btn-icon {
margin-inline-end: 0px;
}
}

View File

@@ -5,8 +5,8 @@ export const RIBBON_STYLES = {
};
export const buttonText: Record<string, string> = {
[ROUTES.LOGS_EXPLORER]: 'Switch to Old Logs Explorer',
[ROUTES.TRACE]: 'Try new Traces Explorer',
[ROUTES.OLD_LOGS_EXPLORER]: 'Switch to New Logs Explorer',
[ROUTES.TRACES_EXPLORER]: 'Switch to Old Trace Explorer',
[ROUTES.LOGS_EXPLORER]: 'Old Explorer',
[ROUTES.TRACE]: 'New Explorer',
[ROUTES.OLD_LOGS_EXPLORER]: 'New Explorer',
[ROUTES.TRACES_EXPLORER]: 'Old Explorer',
};

View File

@@ -1,7 +1,9 @@
import { CompassOutlined } from '@ant-design/icons';
import './NewExplorerCTA.styles.scss';
import { Badge, Button } from 'antd';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { Undo } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
@@ -34,11 +36,11 @@ function NewExplorerCTA(): JSX.Element | null {
const button = useMemo(
() => (
<Button
icon={<CompassOutlined />}
icon={<Undo size={16} />}
onClick={onClickHandler}
danger
data-testid="newExplorerCTA"
type="primary"
type="text"
className="periscope-btn link"
>
{buttonText[location.pathname]}
</Button>

View File

@@ -3,11 +3,12 @@ import './OnboardingQuestionaire.styles.scss';
import { NotificationInstance } from 'antd/es/notification/interface';
import logEvent from 'api/common/logEvent';
import updateProfileAPI from 'api/onboarding/updateProfile';
import getAllOrgPreferences from 'api/preferences/getAllOrgPreferences';
import updateOrgPreferenceAPI from 'api/preferences/updateOrgPreference';
import listOrgPreferences from 'api/v1/org/preferences/list';
import updateOrgPreferenceAPI from 'api/v1/org/preferences/name/update';
import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { FeatureKeys } from 'constants/features';
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes';
import { InviteTeamMembersProps } from 'container/OrganizationSettings/PendingInvitesContainer';
import { useNotifications } from 'hooks/useNotifications';
@@ -108,13 +109,13 @@ function OnboardingQuestionaire(): JSX.Element {
}, []);
const { refetch: refetchOrgPreferences } = useQuery({
queryFn: () => getAllOrgPreferences(),
queryFn: () => listOrgPreferences(),
queryKey: ['getOrgPreferences'],
enabled: false,
refetchOnWindowFocus: false,
onSuccess: (response) => {
if (response.payload && response.payload.data) {
updateOrgPreferences(response.payload.data);
if (response.data) {
updateOrgPreferences(response.data);
}
setUpdatingOrgOnboardingStatus(false);
@@ -196,7 +197,7 @@ function OnboardingQuestionaire(): JSX.Element {
setUpdatingOrgOnboardingStatus(true);
updateOrgPreference({
preferenceID: 'ORG_ONBOARDING',
name: ORG_PREFERENCES.ORG_ONBOARDING,
value: true,
});
};

View File

@@ -298,8 +298,6 @@
}
.onboarding-v2 {
margin: 0px -1rem;
.onboarding-header-container {
display: flex;
justify-content: space-between;

View File

@@ -1,7 +1,4 @@
import getFromLocalstorage from 'api/browser/localstorage/get';
import setToLocalstorage from 'api/browser/localstorage/set';
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { LOCALSTORAGE } from 'constants/localStorage';
import { LogViewMode } from 'container/LogsTable';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import useDebounce from 'hooks/useDebounce';
@@ -11,6 +8,7 @@ import {
AllTraceFilterKeys,
AllTraceFilterKeyValue,
} from 'pages/TracesExplorer/Filter/filterUtils';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useQueries } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
@@ -35,10 +33,10 @@ import {
import { getOptionsFromKeys } from './utils';
interface UseOptionsMenuProps {
storageKey?: string;
dataSource: DataSource;
aggregateOperator: string;
initialOptions?: InitialOptions;
storageKey: LOCALSTORAGE;
}
interface UseOptionsMenu {
@@ -48,22 +46,21 @@ interface UseOptionsMenu {
}
const useOptionsMenu = ({
storageKey,
dataSource,
aggregateOperator,
initialOptions = {},
}: UseOptionsMenuProps): UseOptionsMenu => {
const { notifications } = useNotifications();
const {
preferences,
updateColumns,
updateFormatting,
} = usePreferenceContext();
const [searchText, setSearchText] = useState<string>('');
const [isFocused, setIsFocused] = useState<boolean>(false);
const debouncedSearchText = useDebounce(searchText, 300);
const localStorageOptionsQuery = useMemo(
() => getFromLocalstorage(storageKey),
[storageKey],
);
const initialQueryParams = useMemo(
() => ({
searchText: '',
@@ -77,7 +74,6 @@ const useOptionsMenu = ({
const {
query: optionsQuery,
queryData: optionsQueryData,
redirectWithQuery: redirectWithOptionsData,
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
@@ -105,7 +101,9 @@ const useOptionsMenu = ({
);
const initialSelectedColumns = useMemo(() => {
if (!isFetchedInitialAttributes) return [];
if (!isFetchedInitialAttributes) {
return [];
}
const attributesData = initialAttributesResult?.reduce(
(acc, attributeResponse) => {
@@ -142,14 +140,12 @@ const useOptionsMenu = ({
})
.filter(Boolean) as BaseAutocompleteData[];
// this is the last point where we can set the default columns and if uptil now also we have an empty array then we will set the default columns
if (!initialSelected || !initialSelected?.length) {
initialSelected = defaultTraceSelectedColumns;
}
}
return initialSelected || [];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isFetchedInitialAttributes,
initialOptions?.selectColumns,
@@ -171,7 +167,6 @@ const useOptionsMenu = ({
const searchedAttributeKeys = useMemo(() => {
if (searchedAttributesData?.payload?.attributeKeys?.length) {
if (dataSource === DataSource.LOGS) {
// add timestamp and body to the list of attributes
return [
...defaultLogsSelectedColumns,
...searchedAttributesData.payload.attributeKeys.filter(
@@ -188,32 +183,35 @@ const useOptionsMenu = ({
return [];
}, [dataSource, searchedAttributesData?.payload?.attributeKeys]);
const initialOptionsQuery: OptionsQuery = useMemo(
() => ({
const initialOptionsQuery: OptionsQuery = useMemo(() => {
let defaultColumns = defaultOptionsQuery.selectColumns;
if (dataSource === DataSource.TRACES) {
defaultColumns = defaultTraceSelectedColumns;
} else if (dataSource === DataSource.LOGS) {
defaultColumns = defaultLogsSelectedColumns;
}
const finalSelectColumns = initialOptions?.selectColumns
? initialSelectedColumns
: defaultColumns;
return {
...defaultOptionsQuery,
...initialOptions,
// eslint-disable-next-line no-nested-ternary
selectColumns: initialOptions?.selectColumns
? initialSelectedColumns
: dataSource === DataSource.TRACES
? defaultTraceSelectedColumns
: defaultOptionsQuery.selectColumns,
}),
[dataSource, initialOptions, initialSelectedColumns],
);
selectColumns: finalSelectColumns,
};
}, [dataSource, initialOptions, initialSelectedColumns]);
const selectedColumnKeys = useMemo(
() => optionsQueryData?.selectColumns?.map(({ id }) => id) || [],
[optionsQueryData],
() => preferences?.columns?.map(({ id }) => id) || [],
[preferences?.columns],
);
const optionsFromAttributeKeys = useMemo(() => {
const filteredAttributeKeys = searchedAttributeKeys.filter((item) => {
// For other data sources, only filter out 'body' if it exists
if (dataSource !== DataSource.LOGS) {
return item.key !== 'body';
}
// For LOGS, keep all keys
return true;
});
@@ -223,10 +221,8 @@ const useOptionsMenu = ({
const handleRedirectWithOptionsData = useCallback(
(newQueryData: OptionsQuery) => {
redirectWithOptionsData(newQueryData);
setToLocalstorage(storageKey, JSON.stringify(newQueryData));
},
[storageKey, redirectWithOptionsData],
[redirectWithOptionsData],
);
const handleSelectColumns = useCallback(
@@ -235,7 +231,7 @@ const useOptionsMenu = ({
const newSelectedColumns = newSelectedColumnKeys.reduce((acc, key) => {
const column = [
...searchedAttributeKeys,
...optionsQueryData.selectColumns,
...(preferences?.columns || []),
].find(({ id }) => id === key);
if (!column) return acc;
@@ -243,75 +239,116 @@ const useOptionsMenu = ({
}, [] as BaseAutocompleteData[]);
const optionsData: OptionsQuery = {
...optionsQueryData,
...defaultOptionsQuery,
selectColumns: newSelectedColumns,
format: preferences?.formatting?.format || defaultOptionsQuery.format,
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
};
updateColumns(newSelectedColumns);
handleRedirectWithOptionsData(optionsData);
},
[
searchedAttributeKeys,
selectedColumnKeys,
optionsQueryData,
preferences,
handleRedirectWithOptionsData,
updateColumns,
],
);
const handleRemoveSelectedColumn = useCallback(
(columnKey: string) => {
const newSelectedColumns = optionsQueryData?.selectColumns?.filter(
const newSelectedColumns = preferences?.columns?.filter(
({ id }) => id !== columnKey,
);
if (!newSelectedColumns.length && dataSource !== DataSource.LOGS) {
if (!newSelectedColumns?.length && dataSource !== DataSource.LOGS) {
notifications.error({
message: 'There must be at least one selected column',
});
} else {
const optionsData: OptionsQuery = {
...optionsQueryData,
selectColumns: newSelectedColumns,
...defaultOptionsQuery,
selectColumns: newSelectedColumns || [],
format: preferences?.formatting?.format || defaultOptionsQuery.format,
maxLines:
preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
fontSize:
preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
};
updateColumns(newSelectedColumns || []);
handleRedirectWithOptionsData(optionsData);
}
},
[dataSource, notifications, optionsQueryData, handleRedirectWithOptionsData],
[
dataSource,
notifications,
preferences,
handleRedirectWithOptionsData,
updateColumns,
],
);
const handleFormatChange = useCallback(
(value: LogViewMode) => {
const optionsData: OptionsQuery = {
...optionsQueryData,
...defaultOptionsQuery,
selectColumns: preferences?.columns || [],
format: value,
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
};
updateFormatting({
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
format: value,
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
});
handleRedirectWithOptionsData(optionsData);
},
[handleRedirectWithOptionsData, optionsQueryData],
[handleRedirectWithOptionsData, preferences, updateFormatting],
);
const handleMaxLinesChange = useCallback(
(value: string | number | null) => {
const optionsData: OptionsQuery = {
...optionsQueryData,
...defaultOptionsQuery,
selectColumns: preferences?.columns || [],
format: preferences?.formatting?.format || defaultOptionsQuery.format,
maxLines: value as number,
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
};
updateFormatting({
maxLines: value as number,
format: preferences?.formatting?.format || defaultOptionsQuery.format,
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
});
handleRedirectWithOptionsData(optionsData);
},
[handleRedirectWithOptionsData, optionsQueryData],
[handleRedirectWithOptionsData, preferences, updateFormatting],
);
const handleFontSizeChange = useCallback(
(value: FontSize) => {
const optionsData: OptionsQuery = {
...optionsQueryData,
...defaultOptionsQuery,
selectColumns: preferences?.columns || [],
format: preferences?.formatting?.format || defaultOptionsQuery.format,
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
fontSize: value,
};
updateFormatting({
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
format: preferences?.formatting?.format || defaultOptionsQuery.format,
fontSize: value,
});
handleRedirectWithOptionsData(optionsData);
},
[handleRedirectWithOptionsData, optionsQueryData],
[handleRedirectWithOptionsData, preferences, updateFormatting],
);
const handleSearchAttribute = useCallback((value: string) => {
@@ -331,7 +368,7 @@ const useOptionsMenu = ({
() => ({
addColumn: {
isFetching: isSearchedAttributesFetching,
value: optionsQueryData?.selectColumns || defaultOptionsQuery.selectColumns,
value: preferences?.columns || defaultOptionsQuery.selectColumns,
options: optionsFromAttributeKeys || [],
onFocus: handleFocus,
onBlur: handleBlur,
@@ -340,24 +377,21 @@ const useOptionsMenu = ({
onSearch: handleSearchAttribute,
},
format: {
value: optionsQueryData.format || defaultOptionsQuery.format,
value: preferences?.formatting?.format || defaultOptionsQuery.format,
onChange: handleFormatChange,
},
maxLines: {
value: optionsQueryData.maxLines || defaultOptionsQuery.maxLines,
value: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
onChange: handleMaxLinesChange,
},
fontSize: {
value: optionsQueryData?.fontSize || defaultOptionsQuery.fontSize,
value: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
onChange: handleFontSizeChange,
},
}),
[
isSearchedAttributesFetching,
optionsQueryData?.selectColumns,
optionsQueryData.format,
optionsQueryData.maxLines,
optionsQueryData?.fontSize,
preferences,
optionsFromAttributeKeys,
handleSelectColumns,
handleRemoveSelectedColumn,
@@ -369,23 +403,25 @@ const useOptionsMenu = ({
);
useEffect(() => {
if (optionsQuery || !isFetchedInitialAttributes) return;
if (optionsQuery || !isFetchedInitialAttributes) {
return;
}
const nextOptionsQuery = localStorageOptionsQuery
? JSON.parse(localStorageOptionsQuery)
: initialOptionsQuery;
redirectWithOptionsData(nextOptionsQuery);
redirectWithOptionsData(initialOptionsQuery);
}, [
isFetchedInitialAttributes,
optionsQuery,
initialOptionsQuery,
localStorageOptionsQuery,
redirectWithOptionsData,
]);
return {
options: optionsQueryData,
options: {
selectColumns: preferences?.columns || [],
format: preferences?.formatting?.format || defaultOptionsQuery.format,
maxLines: preferences?.formatting?.maxLines || defaultOptionsQuery.maxLines,
fontSize: preferences?.formatting?.fontSize || defaultOptionsQuery.fontSize,
},
config: optionsMenuConfig,
handleOptionsChange: handleRedirectWithOptionsData,
};

View File

@@ -0,0 +1,21 @@
.organization-settings-container {
display: flex;
flex-direction: column;
gap: 16px;
margin: 16px auto;
padding: 16px;
width: 90%;
border: 1px solid var(--Slate-500, #161922);
background: var(--Ink-400, #121317);
border-radius: 3px;
}
.lightMode {
.organization-settings-container {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}

View File

@@ -14,19 +14,19 @@ function OrganizationSettings(): JSX.Element {
}
return (
<>
<div className="organization-settings-container">
<Space direction="vertical">
{org.map((e, index) => (
<DisplayName key={e.id} id={e.id} index={index} />
))}
</Space>
<Divider />
<PendingInvitesContainer />
<Divider />
<Members />
<Divider />
<AuthDomains />
</>
</div>
);
}

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