Compare commits

..

57 Commits

Author SHA1 Message Date
Shivanshu Raj Shrivastava
7ddf002d46 fix: update clickhouse quries
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:17:02 +05:30
Shivanshu Raj Shrivastava
31cd192f24 fix: fix clickhouse query
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:17:02 +05:30
Shivanshu Raj Shrivastava
3aacb99adc fix: update clickhouse queries
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:17:02 +05:30
Shivanshu Raj Shrivastava
40ca9adbfc fix: update all latency and duration to milliseconds precision
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:17:02 +05:30
Shivanshu Raj Shrivastava
bfa7a06e90 chore: sync with main
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:17:02 +05:30
Shivanshu Raj Shrivastava
189046865a fix: minor fixes
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:17:02 +05:30
Shivanshu Raj Shrivastava
d5ee6ca2c3 fix: minor fixes
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:17:02 +05:30
Shivanshu Raj Shrivastava
62fb05ac5a fix: further improve clickhouse queries
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:17:02 +05:30
Shivanshu Raj Shrivastava
5a3ed26f01 fix: improved clickhouse query performance
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:17:02 +05:30
Shivanshu Raj Shrivastava
8e490e4089 fix: improved clickhouse query performance
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:17:02 +05:30
Shivanshu Raj Shrivastava
d3adc319ad fix: improved clickhouse query performance
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:17:02 +05:30
Shivanshu Raj Shrivastava
6b58e859b5 chore: updated to global inner join
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:17:02 +05:30
ahmadshaheer
3d3a1eaaf2 chore: remove dev env check 2025-06-02 12:17:02 +05:30
Shivanshu Raj Shrivastava
361640fd22 chore: fix typo
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:17:02 +05:30
Shivanshu Raj Shrivastava
89de68f987 chore: tf testing
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:17:02 +05:30
ahmadshaheer
71b098776b feat: add timestamp to funnel description payload and update mutation type 2025-06-02 12:17:02 +05:30
ahmadshaheer
6b3e2759a1 refactor: update funnel description endpoint from POST /save to PUT /{funnel_id} 2025-06-02 12:17:02 +05:30
Shivanshu Raj Shrivastava
ea46639f59 feat: adds server and handler changes
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:17:02 +05:30
ahmadshaheer
69ed1b7d02 chore: remove the existing filters of a step on clicking replace button 2025-06-02 12:17:02 +05:30
ahmadshaheer
5b07705157 chore: temporarily hide latency pointer from funnel steps 2025-06-02 12:17:02 +05:30
ahmadshaheer
2fc8bb4585 fix: refetch funnel steps overview on clicking refresh 2025-06-02 12:17:02 +05:30
Shivanshu Raj Shrivastava
11d75940e8 chore: review comments
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:04 +05:30
Shivanshu Raj Shrivastava
3caaa51e08 fix: update access control
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:04 +05:30
Shivanshu Raj Shrivastava
cad3bf6883 chore: update unit test
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:04 +05:30
Shivanshu Raj Shrivastava
5b7ce41d0d chore: update migration number
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:04 +05:30
Shivanshu Raj Shrivastava
ee5120d4ed chore: restore routes
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:04 +05:30
Shivanshu Raj Shrivastava
c249620e8f chore: fix unit tests
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:04 +05:30
Shivanshu Raj Shrivastava
1c1811b216 chore: beautify
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:04 +05:30
Shivanshu Raj Shrivastava
86d69f74f3 chore: remove save API endpoint
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:04 +05:30
Shivanshu Raj Shrivastava
37b26a7116 chore: use errors package
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:04 +05:30
Shivanshu Raj Shrivastava
26ad89ed70 chore: better error handling
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:04 +05:30
Shivanshu Raj Shrivastava
94c7512a6a chore: beautify
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:04 +05:30
Shivanshu Raj Shrivastava
333ff86a6b chore: update claims from context
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:04 +05:30
Shivanshu Raj Shrivastava
a22d061ec1 fix: review comments and some changes
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:04 +05:30
Shivanshu Raj Shrivastava
22fdeb1381 fix: update funnel migration number
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
77b330cfe9 fix: minor fixes
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
78a5d7e39e fix: review comments
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
ea1f4e8253 fix: updated unit tests and mocks
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
5e0d6110b5 feat: trace funnel state management
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
14ce7f80e2 chore: fix error handling
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
f8341e8958 fix: optimize funnel creation by combining insert and update operations
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
7a7428d73e chore: added some improvements
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
77cd490e48 chore: update normalize funnel steps
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
9a96817a88 chore: fix naming convention
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
da9a2520a4 chore: fix package naming
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
c03bf9905c test: add more tests to utils
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
6676832c71 chore: add funnel validation while processing funnel steps
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
03e50d3bc3 chore: refactor handler and utils
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
95bc3987bb test: add trace funnel module tests
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
eb797edc53 test: add handler tests
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
c2d36480a2 test: add utility function tests
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
ab0d9918b2 chore: add utility functions
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
c364a3e695 feat: add db migrations
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
43337b6697 feat: db operations, module and handler implementation
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:03 +05:30
Shivanshu Raj Shrivastava
e258d70df5 feat: add required types for tracefunnels
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:02 +05:30
Shivanshu Raj Shrivastava
19ee5860cb feat: add tracefunnel module and handler
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:02 +05:30
Shivanshu Raj Shrivastava
235ea39d73 feat: adds server and handler changes
Signed-off-by: Shivanshu Raj Shrivastava <shivanshu1333@gmail.com>
2025-06-02 12:14:02 +05:30
492 changed files with 5294 additions and 80271 deletions

2
.github/CODEOWNERS vendored
View File

@@ -11,5 +11,5 @@
/pkg/errors/ @grandwizard28
/pkg/factory/ @grandwizard28
/pkg/types/ @grandwizard28
/pkg/sqlmigration/ @vikrantgupta25
.golangci.yml @grandwizard28
**/(zeus|licensing|sqlmigration)/ @vikrantgupta25

View File

@@ -74,8 +74,7 @@ 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/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}'
GO_CGO_ENABLED: 1
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
DOCKER_DOCKERFILE_PATH: ./pkg/query-service/Dockerfile.multi-arch

View File

@@ -108,8 +108,7 @@ 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/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1'
GO_CGO_ENABLED: 1
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
DOCKER_DOCKERFILE_PATH: ./ee/query-service/Dockerfile.multi-arch

View File

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

View File

@@ -8,7 +8,7 @@
<p align="center">All your logs, metrics, and traces in one place. Monitor your application, spot issues before they occur and troubleshoot downtime quickly with rich context. SigNoz is a cost-effective open-source alternative to Datadog and New Relic. Visit <a href="https://signoz.io" target="_blank">signoz.io</a> for the full documentation, tutorials, and guide.</p>
<p align="center">
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/signoz.svg?label=Docker%20Downloads"> </a>
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/query-service?label=Docker Downloads"> </a>
<img alt="GitHub issues" src="https://img.shields.io/github/issues/signoz/signoz"> </a>
<a href="https://twitter.com/intent/tweet?text=Monitor%20your%20applications%20and%20troubleshoot%20problems%20with%20SigNoz,%20an%20open-source%20alternative%20to%20DataDog,%20NewRelic.&url=https://signoz.io/&via=SigNozHQ&hashtags=opensource,signoz,observability">
<img alt="tweet" src="https://img.shields.io/twitter/url/http/shields.io.svg?style=social"> </a>

View File

@@ -165,6 +165,12 @@ alertmanager:
# Retention of the notification logs.
retention: 120h
##################### Analytics #####################
analytics:
# Whether to enable analytics.
enabled: false
##################### Emailing #####################
emailing:
# Whether to enable emailing.
@@ -209,18 +215,3 @@ 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.87.0
image: signoz/signoz:v0.85.3
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.87.0
image: signoz/signoz:v0.85.3
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.87.0}
image: signoz/signoz:${VERSION:-v0.85.3}
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.87.0}
image: signoz/signoz:${VERSION:-v0.85.3}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -1,51 +0,0 @@
# 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

@@ -1,106 +0,0 @@
# 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

@@ -3,16 +3,16 @@ package httplicensing
import (
"context"
"encoding/json"
"github.com/SigNoz/signoz/ee/query-service/constants"
"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/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/zeus"
@@ -25,17 +25,16 @@ 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, analytics analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter) 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, analytics)
return New(ctx, providerSettings, config, store, zeus, orgGetter)
})
}
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) {
func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter) (licensing.Licensing, error) {
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/ee/licensing/httplicensing")
licensestore := sqllicensingstore.New(sqlstore)
return &provider{
@@ -45,7 +44,6 @@ func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Conf
settings: settings,
orgGetter: orgGetter,
stopChan: make(chan struct{}),
analytics: analytics,
}, nil
}
@@ -90,6 +88,13 @@ func (provider *provider) Validate(ctx context.Context) error {
}
}
if len(organizations) == 0 {
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
if err != nil {
return err
}
}
return nil
}
@@ -110,6 +115,11 @@ func (provider *provider) Activate(ctx context.Context, organizationID valuer.UU
return err
}
err = provider.InitFeatures(ctx, license.Features)
if err != nil {
return err
}
return nil
}
@@ -129,24 +139,28 @@ func (provider *provider) GetActive(ctx context.Context, organizationID valuer.U
func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUID) error {
activeLicense, err := provider.GetActive(ctx, organizationID)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return nil
}
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
provider.settings.Logger().ErrorContext(ctx, "license validation failed", "org_id", organizationID.StringValue())
return err
}
if err != nil && errors.Ast(err, errors.TypeNotFound) {
provider.settings.Logger().DebugContext(ctx, "no active license found, defaulting to basic plan", "org_id", organizationID.StringValue())
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
if err != nil {
return err
}
return nil
}
data, err := provider.zeus.GetLicense(ctx, activeLicense.Key)
if err != nil {
if time.Since(activeLicense.LastValidatedAt) > time.Duration(provider.config.FailureThreshold)*provider.config.PollInterval {
activeLicense.UpdateFeatures(licensetypes.BasicPlan)
updatedStorableLicense := licensetypes.NewStorableLicenseFromLicense(activeLicense)
err = provider.store.Update(ctx, organizationID, updatedStorableLicense)
provider.settings.Logger().ErrorContext(ctx, "license validation failed for consecutive poll intervals, defaulting to basic plan", "failure_threshold", provider.config.FailureThreshold, "license_id", activeLicense.ID.StringValue(), "org_id", organizationID.StringValue())
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
if err != nil {
return err
}
return nil
}
return err
@@ -163,25 +177,6 @@ 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
}
@@ -223,27 +218,80 @@ func (provider *provider) Portal(ctx context.Context, organizationID valuer.UUID
return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
}
func (provider *provider) GetFeatureFlags(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.Feature, error) {
license, err := provider.GetActive(ctx, organizationID)
// feature surrogate
func (provider *provider) CheckFeature(ctx context.Context, key string) error {
feature, err := provider.store.GetFeature(ctx, key)
if err != nil {
return err
}
if feature.Active {
return nil
}
return errors.Newf(errors.TypeUnsupported, licensing.ErrCodeFeatureUnavailable, "feature unavailable: %s", key)
}
func (provider *provider) GetFeatureFlag(ctx context.Context, key string) (*featuretypes.GettableFeature, error) {
featureStatus, err := provider.store.GetFeature(ctx, key)
if err != nil {
return nil, err
}
return &featuretypes.GettableFeature{
Name: featureStatus.Name,
Active: featureStatus.Active,
Usage: int64(featureStatus.Usage),
UsageLimit: int64(featureStatus.UsageLimit),
Route: featureStatus.Route,
}, nil
}
func (provider *provider) GetFeatureFlags(ctx context.Context) ([]*featuretypes.GettableFeature, error) {
storableFeatures, err := provider.store.GetAllFeatures(ctx)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return licensetypes.BasicPlan, nil
}
return nil, err
}
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
gettableFeatures := make([]*featuretypes.GettableFeature, len(storableFeatures))
for idx, gettableFeature := range storableFeatures {
gettableFeatures[idx] = &featuretypes.GettableFeature{
Name: gettableFeature.Name,
Active: gettableFeature.Active,
Usage: int64(gettableFeature.Usage),
UsageLimit: int64(gettableFeature.UsageLimit),
Route: gettableFeature.Route,
}
return nil, err
}
return licensetypes.NewStatsFromLicense(activeLicense), nil
if constants.IsDotMetricsEnabled {
gettableFeatures = append(gettableFeatures, &featuretypes.GettableFeature{
Name: featuretypes.DotMetricsEnabled,
Active: true,
})
}
return gettableFeatures, nil
}
func (provider *provider) InitFeatures(ctx context.Context, features []*featuretypes.GettableFeature) error {
featureStatus := make([]*featuretypes.StorableFeature, len(features))
for i, f := range features {
featureStatus[i] = &featuretypes.StorableFeature{
Name: f.Name,
Active: f.Active,
Usage: int(f.Usage),
UsageLimit: int(f.UsageLimit),
Route: f.Route,
}
}
return provider.store.InitFeatures(ctx, featureStatus)
}
func (provider *provider) UpdateFeatureFlag(ctx context.Context, feature *featuretypes.GettableFeature) error {
return provider.store.UpdateFeature(ctx, &featuretypes.StorableFeature{
Name: feature.Name,
Active: feature.Active,
Usage: int(feature.Usage),
UsageLimit: int(feature.UsageLimit),
Route: feature.Route,
})
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -79,3 +80,81 @@ func (store *store) Update(ctx context.Context, organizationID valuer.UUID, stor
return nil
}
func (store *store) CreateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error {
_, err := store.
sqlstore.
BunDB().
NewInsert().
Model(storableFeature).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "feature with name:%s already exists", storableFeature.Name)
}
return nil
}
func (store *store) GetFeature(ctx context.Context, key string) (*featuretypes.StorableFeature, error) {
storableFeature := new(featuretypes.StorableFeature)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(storableFeature).
Where("name = ?", key).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "feature with name:%s does not exist", key)
}
return storableFeature, nil
}
func (store *store) GetAllFeatures(ctx context.Context) ([]*featuretypes.StorableFeature, error) {
storableFeatures := make([]*featuretypes.StorableFeature, 0)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(&storableFeatures).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "features do not exist")
}
return storableFeatures, nil
}
func (store *store) InitFeatures(ctx context.Context, storableFeatures []*featuretypes.StorableFeature) error {
_, err := store.
sqlstore.
BunDB().
NewInsert().
Model(&storableFeatures).
On("CONFLICT (name) DO UPDATE").
Set("active = EXCLUDED.active").
Set("usage = EXCLUDED.usage").
Set("usage_limit = EXCLUDED.usage_limit").
Set("route = EXCLUDED.route").
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to initialise features")
}
return nil
}
func (store *store) UpdateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error {
_, err := store.
sqlstore.
BunDB().
NewUpdate().
Model(storableFeature).
Exec(ctx)
if err != nil {
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to update feature with key: %s", storableFeature.Name)
}
return nil
}

View File

@@ -39,7 +39,6 @@ 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

@@ -1,6 +1,7 @@
package api
import (
"context"
"net/http"
"net/http/httputil"
"time"
@@ -12,7 +13,6 @@ 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"
@@ -59,9 +59,8 @@ 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.Instrumentation.ToProviderSettings(), signoz.TelemetryStore),
FieldsAPI: fields.NewAPI(signoz.TelemetryStore, signoz.Instrumentation.Logger()),
Signoz: signoz,
QuerierAPI: querierAPI.NewAPI(signoz.Querier),
})
if err != nil {
@@ -87,12 +86,23 @@ func (ah *APIHandler) Gateway() *httputil.ReverseProxy {
return ah.opts.Gateway
}
func (ah *APIHandler) CheckFeature(ctx context.Context, key string) bool {
err := ah.Signoz.Licensing.CheckFeature(ctx, key)
return err == nil
}
// RegisterRoutes registers routes for this handler on the given router
func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
// note: add ee override methods first
// routes available only in ee version
router.HandleFunc("/api/v1/features", am.ViewAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/loginPrecheck", am.OpenAccess(ah.Signoz.Handlers.User.LoginPrecheck)).Methods(http.MethodGet)
// invite
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(ah.Signoz.Handlers.User.GetInvite)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/invite/accept", am.OpenAccess(ah.Signoz.Handlers.User.AcceptInvite)).Methods(http.MethodPost)
// paid plans specific routes
router.HandleFunc("/api/v1/complete/saml", am.OpenAccess(ah.receiveSAML)).Methods(http.MethodPost)
@@ -104,6 +114,9 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingAPI.Portal)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/dashboards/{uuid}/lock", am.EditAccess(ah.lockDashboard)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/dashboards/{uuid}/unlock", am.EditAccess(ah.unlockDashboard)).Methods(http.MethodPut)
// v3
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Activate)).Methods(http.MethodPost)
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Refresh)).Methods(http.MethodPut)

View File

@@ -0,0 +1,62 @@
package api
import (
"net/http"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/gorilla/mux"
)
func (ah *APIHandler) lockDashboard(w http.ResponseWriter, r *http.Request) {
ah.lockUnlockDashboard(w, r, true)
}
func (ah *APIHandler) unlockDashboard(w http.ResponseWriter, r *http.Request) {
ah.lockUnlockDashboard(w, r, false)
}
func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request, lock bool) {
// Locking can only be done by the owner of the dashboard
// or an admin
// - Fetch the dashboard
// - Check if the user is the owner or an admin
// - If yes, lock/unlock the dashboard
// - If no, return 403
// Get the dashboard UUID from the request
uuid := mux.Vars(r)["uuid"]
if strings.HasPrefix(uuid, "integration") {
render.Error(w, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "dashboards created by integrations cannot be modified"))
return
}
claims, err := authtypes.ClaimsFromContext(r.Context())
if err != nil {
render.Error(w, errors.Newf(errors.TypeUnauthenticated, errors.CodeUnauthenticated, "unauthenticated"))
return
}
dashboard, err := ah.Signoz.Modules.Dashboard.Get(r.Context(), claims.OrgID, uuid)
if err != nil {
render.Error(w, err)
return
}
if err := claims.IsAdmin(); err != nil && (dashboard.CreatedBy != claims.Email) {
render.Error(w, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "You are not authorized to lock/unlock this dashboard"))
return
}
// Lock/Unlock the dashboard
err = ah.Signoz.Modules.Dashboard.LockUnlock(r.Context(), claims.OrgID, uuid, lock)
if err != nil {
render.Error(w, err)
return
}
ah.Respond(w, "Dashboard updated successfully")
}

View File

@@ -12,7 +12,7 @@ import (
pkgError "github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/valuer"
"go.uber.org/zap"
)
@@ -31,7 +31,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
return
}
featureSet, err := ah.Signoz.Licensing.GetFeatureFlags(r.Context(), orgID)
featureSet, err := ah.Signoz.Licensing.GetFeatureFlags(r.Context())
if err != nil {
ah.HandleError(w, err, http.StatusInternalServerError)
return
@@ -61,15 +61,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
if ah.opts.PreferSpanMetrics {
for idx, feature := range featureSet {
if feature.Name == licensetypes.UseSpanMetrics {
featureSet[idx].Active = true
}
}
}
if constants.IsDotMetricsEnabled {
for idx, feature := range featureSet {
if feature.Name == licensetypes.DotMetricsEnabled {
if feature.Name == featuretypes.UseSpanMetrics {
featureSet[idx].Active = true
}
}
@@ -80,7 +72,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
// fetchZeusFeatures makes an HTTP GET request to the /zeusFeatures endpoint
// and returns the FeatureSet.
func fetchZeusFeatures(url, licenseKey string) ([]*licensetypes.Feature, error) {
func fetchZeusFeatures(url, licenseKey string) ([]*featuretypes.GettableFeature, error) {
// Check if the URL is empty
if url == "" {
return nil, fmt.Errorf("url is empty")
@@ -139,28 +131,28 @@ func fetchZeusFeatures(url, licenseKey string) ([]*licensetypes.Feature, error)
}
type ZeusFeaturesResponse struct {
Status string `json:"status"`
Data []*licensetypes.Feature `json:"data"`
Status string `json:"status"`
Data []*featuretypes.GettableFeature `json:"data"`
}
// MergeFeatureSets merges two FeatureSet arrays with precedence to zeusFeatures.
func MergeFeatureSets(zeusFeatures, internalFeatures []*licensetypes.Feature) []*licensetypes.Feature {
func MergeFeatureSets(zeusFeatures, internalFeatures []*featuretypes.GettableFeature) []*featuretypes.GettableFeature {
// Create a map to store the merged features
featureMap := make(map[string]*licensetypes.Feature)
featureMap := make(map[string]*featuretypes.GettableFeature)
// Add all features from the otherFeatures set to the map
for _, feature := range internalFeatures {
featureMap[feature.Name.StringValue()] = feature
featureMap[feature.Name] = feature
}
// Add all features from the zeusFeatures set to the map
// If a feature already exists (i.e., same name), the zeusFeature will overwrite it
for _, feature := range zeusFeatures {
featureMap[feature.Name.StringValue()] = feature
featureMap[feature.Name] = feature
}
// Convert the map back to a FeatureSet slice
var mergedFeatures []*licensetypes.Feature
var mergedFeatures []*featuretypes.GettableFeature
for _, feature := range featureMap {
mergedFeatures = append(mergedFeatures, feature)
}

View File

@@ -3,79 +3,78 @@ package api
import (
"testing"
"github.com/SigNoz/signoz/pkg/types/licensetypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/stretchr/testify/assert"
)
func TestMergeFeatureSets(t *testing.T) {
tests := []struct {
name string
zeusFeatures []*licensetypes.Feature
internalFeatures []*licensetypes.Feature
expected []*licensetypes.Feature
zeusFeatures []*featuretypes.GettableFeature
internalFeatures []*featuretypes.GettableFeature
expected []*featuretypes.GettableFeature
}{
{
name: "empty zeusFeatures and internalFeatures",
zeusFeatures: []*licensetypes.Feature{},
internalFeatures: []*licensetypes.Feature{},
expected: []*licensetypes.Feature{},
zeusFeatures: []*featuretypes.GettableFeature{},
internalFeatures: []*featuretypes.GettableFeature{},
expected: []*featuretypes.GettableFeature{},
},
{
name: "non-empty zeusFeatures and empty internalFeatures",
zeusFeatures: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: false},
zeusFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
internalFeatures: []*licensetypes.Feature{},
expected: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: false},
internalFeatures: []*featuretypes.GettableFeature{},
expected: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
},
{
name: "empty zeusFeatures and non-empty internalFeatures",
zeusFeatures: []*licensetypes.Feature{},
internalFeatures: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: false},
zeusFeatures: []*featuretypes.GettableFeature{},
internalFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
expected: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: false},
expected: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
},
{
name: "non-empty zeusFeatures and non-empty internalFeatures with no conflicts",
zeusFeatures: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature3"), Active: false},
zeusFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature3", Active: false},
},
internalFeatures: []*licensetypes.Feature{
{Name: valuer.NewString("Feature2"), Active: true},
{Name: valuer.NewString("Feature4"), Active: false},
internalFeatures: []*featuretypes.GettableFeature{
{Name: "Feature2", Active: true},
{Name: "Feature4", Active: false},
},
expected: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: true},
{Name: valuer.NewString("Feature3"), Active: false},
{Name: valuer.NewString("Feature4"), Active: false},
expected: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: true},
{Name: "Feature3", Active: false},
{Name: "Feature4", Active: false},
},
},
{
name: "non-empty zeusFeatures and non-empty internalFeatures with conflicts",
zeusFeatures: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: false},
zeusFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
},
internalFeatures: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: false},
{Name: valuer.NewString("Feature3"), Active: true},
internalFeatures: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: false},
{Name: "Feature3", Active: true},
},
expected: []*licensetypes.Feature{
{Name: valuer.NewString("Feature1"), Active: true},
{Name: valuer.NewString("Feature2"), Active: false},
{Name: valuer.NewString("Feature3"), Active: true},
expected: []*featuretypes.GettableFeature{
{Name: "Feature1", Active: true},
{Name: "Feature2", Active: false},
{Name: "Feature3", Active: true},
},
},
}

View File

@@ -294,7 +294,6 @@ 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,7 +12,6 @@ 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"
@@ -135,8 +134,8 @@ func main() {
zeus.Config(),
httpzeus.NewProviderFactory(),
licensing.Config(24*time.Hour, 3),
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)
func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus, orgGetter organization.Getter) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] {
return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter)
},
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.5",
"http-proxy-middleware": "3.0.3",
"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": "^5.2.1",
"webpack-dev-server": "^4.15.2",
"webpack-retry-chunk-load-plugin": "3.1.1",
"xstate": "^4.31.0"
},
@@ -197,6 +197,7 @@
"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",
@@ -234,7 +235,7 @@
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "5.0.1",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^5.1.4"
"webpack-cli": "^4.9.2"
},
"lint-staged": {
"*.(js|jsx|ts|tsx)": [
@@ -250,7 +251,7 @@
"xml2js": "0.5.0",
"phin": "^3.7.1",
"body-parser": "1.20.3",
"http-proxy-middleware": "3.0.5",
"http-proxy-middleware": "3.0.3",
"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 Channel",
"page_title_edit": "Edit Notification Channel",
"page_title_create": "New Notification Channels",
"page_title_edit": "Edit Notification Channels",
"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 Channel",
"page_title_edit": "Edit Notification Channel",
"page_title_create": "New Notification Channels",
"page_title_edit": "Edit Notification Channels",
"button_save_channel": "Save",
"button_test_channel": "Test",
"button_return": "Back",

View File

@@ -3,7 +3,6 @@ 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';
@@ -15,7 +14,6 @@ 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';
@@ -80,7 +78,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
const checkFirstTimeUser = useCallback((): boolean => {
const users = usersData?.data || [];
const remainingUsers = (Array.isArray(users) ? users : []).filter(
const remainingUsers = users.filter(
(user) => user.email !== 'admin@signoz.cloud',
);
@@ -97,8 +95,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
usersData.data
) {
const isOnboardingComplete = orgPreferences?.find(
(preference: OrgPreference) =>
preference.name === ORG_PREFERENCES.ORG_ONBOARDING,
(preference: Record<string, any>) => preference.key === 'ORG_ONBOARDING',
)?.value;
const isFirstUser = checkFirstTimeUser();

View File

@@ -28,7 +28,6 @@ import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { Suspense, useCallback, useEffect, useState } from 'react';
import { Route, Router, Switch } from 'react-router-dom';
import { CompatRouter } from 'react-router-dom-v5-compat';
import { LicenseStatus } from 'types/api/licensesV3/getActive';
import { Userpilot } from 'userpilot';
import { extractDomain } from 'utils/app';
@@ -172,13 +171,11 @@ function App(): JSX.Element {
user &&
!!user.email
) {
// either the active API returns error with 404 or 501 and if it returns a terminated license means it's on basic plan
const isOnBasicPlan =
(activeLicenseFetchError &&
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
activeLicenseFetchError?.getHttpStatusCode(),
)) ||
(activeLicense?.status && activeLicense.status === LicenseStatus.INVALID);
activeLicenseFetchError &&
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
activeLicenseFetchError?.getHttpStatusCode(),
);
const isIdentifiedUser = getLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER);
if (isLoggedInState && user && user.id && user.email && !isIdentifiedUser) {
@@ -194,11 +191,6 @@ function App(): JSX.Element {
(route) => route?.path !== ROUTES.BILLING,
);
}
if (isEnterpriseSelfHostedUser) {
updatedRoutes.push(LIST_LICENSES);
}
// always add support route for cloud users
updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE];
} else {

View File

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

View File

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

@@ -0,0 +1,27 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/create';
const createDashboard = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const url = props.uploadedGrafana ? '/dashboards/grafana' : '/dashboards';
try {
const response = await axios.post(url, {
...props,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default createDashboard;

View File

@@ -0,0 +1,9 @@
import axios from 'api';
import { PayloadProps, Props } from 'types/api/dashboard/delete';
const deleteDashboard = (props: Props): Promise<PayloadProps> =>
axios
.delete<PayloadProps>(`/dashboards/${props.uuid}`)
.then((response) => response.data);
export default deleteDashboard;

View File

@@ -0,0 +1,11 @@
import axios from 'api';
import { ApiResponse } from 'types/api';
import { Props } from 'types/api/dashboard/get';
import { Dashboard } from 'types/api/dashboard/getAll';
const getDashboard = (props: Props): Promise<Dashboard> =>
axios
.get<ApiResponse<Dashboard>>(`/dashboards/${props.uuid}`)
.then((res) => res.data.data);
export default getDashboard;

View File

@@ -0,0 +1,8 @@
import axios from 'api';
import { ApiResponse } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
export const getAllDashboardList = (): Promise<Dashboard[]> =>
axios
.get<ApiResponse<Dashboard[]>>('/dashboards')
.then((res) => res.data.data);

View File

@@ -0,0 +1,11 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
interface LockDashboardProps {
uuid: string;
}
const lockDashboard = (props: LockDashboardProps): Promise<AxiosResponse> =>
axios.put(`/dashboards/${props.uuid}/lock`);
export default lockDashboard;

View File

@@ -0,0 +1,11 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
interface UnlockDashboardProps {
uuid: string;
}
const unlockDashboard = (props: UnlockDashboardProps): Promise<AxiosResponse> =>
axios.put(`/dashboards/${props.uuid}/unlock`);
export default unlockDashboard;

View File

@@ -0,0 +1,20 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/dashboard/update';
const updateDashboard = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
const response = await axios.put(`/dashboards/${props.uuid}`, {
...props.data,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateDashboard;

View File

@@ -0,0 +1,10 @@
import axios from 'api';
import { ApiResponse } from 'types/api';
import { FeatureFlagProps } from 'types/api/features/getFeaturesFlags';
const getFeaturesFlags = (): Promise<FeatureFlagProps[]> =>
axios
.get<ApiResponse<FeatureFlagProps[]>>(`/featureFlags`)
.then((response) => response.data.data);
export default getFeaturesFlags;

View File

@@ -0,0 +1,18 @@
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

@@ -0,0 +1,18 @@
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

@@ -0,0 +1,20 @@
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

@@ -0,0 +1,22 @@
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

@@ -0,0 +1,28 @@
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

@@ -0,0 +1,28 @@
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,6 +196,8 @@ 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

@@ -1,23 +0,0 @@
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/dashboard/create';
import { Dashboard } from 'types/api/dashboard/getAll';
const create = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
try {
const response = await axios.post<PayloadProps>('/dashboards', {
...props,
});
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default create;

View File

@@ -1,19 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Dashboard, PayloadProps } from 'types/api/dashboard/getAll';
const getAll = async (): Promise<SuccessResponseV2<Dashboard[]>> => {
try {
const response = await axios.get<PayloadProps>('/dashboards');
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getAll;

View File

@@ -1,21 +0,0 @@
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/dashboard/delete';
const deleteDashboard = async (
props: Props,
): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.delete<PayloadProps>(`/dashboards/${props.id}`);
return {
httpStatusCode: response.status,
data: null,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default deleteDashboard;

View File

@@ -1,20 +0,0 @@
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/dashboard/get';
import { Dashboard } from 'types/api/dashboard/getAll';
const get = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
try {
const response = await axios.get<PayloadProps>(`/dashboards/${props.id}`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default get;

View File

@@ -1,23 +0,0 @@
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/dashboard/lockUnlock';
const lock = async (props: Props): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.put<PayloadProps>(
`/dashboards/${props.id}/lock`,
{ lock: props.lock },
);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default lock;

View File

@@ -1,23 +0,0 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
import { PayloadProps, Props } from 'types/api/dashboard/update';
const update = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
try {
const response = await axios.put<PayloadProps>(`/dashboards/${props.id}`, {
...props.data,
});
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default update;

View File

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

View File

@@ -1,23 +0,0 @@
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

@@ -1,25 +0,0 @@
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

@@ -1,22 +0,0 @@
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

@@ -0,0 +1,24 @@
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

@@ -1,21 +0,0 @@
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

@@ -1,25 +0,0 @@
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

@@ -1,22 +0,0 @@
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,6 +1,5 @@
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';
@@ -53,32 +52,11 @@ 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>
<PreferenceContextProvider>
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
</PreferenceContextProvider>
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
</MockQueryClientProvider>,
);
expect(screen.queryByText('Query Builder')).not.toBeInTheDocument();
@@ -87,9 +65,7 @@ describe('ExplorerCard', () => {
it('renders a save view button', () => {
render(
<MockQueryClientProvider>
<PreferenceContextProvider>
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
</PreferenceContextProvider>
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
</MockQueryClientProvider>,
);
expect(screen.queryByText('Save view')).not.toBeInTheDocument();

View File

@@ -6,7 +6,6 @@ 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,
@@ -107,11 +106,7 @@ 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,7 +74,6 @@ 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,12 +1,7 @@
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,
@@ -14,38 +9,19 @@ 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 = routesWithParams.find((e) => e.key === activeRoute);
const selectedRoute = routes.find((e) => e.key === activeRoute);
if (selectedRoute) {
history.push(selectedRoute.route);
}
};
const items = routesWithParams.map(({ Component, name, route, key }) => ({
const items = routes.map(({ Component, name, route, key }) => ({
label: name,
key,
tabKey: route,
@@ -56,8 +32,8 @@ function RouteTab({
<Tabs
onChange={onChange}
destroyInactiveTabPane
activeKey={currentRoute?.key || activeKey}
defaultActiveKey={currentRoute?.key || activeKey}
activeKey={activeKey}
defaultActiveKey={activeKey}
animated
items={items}
// eslint-disable-next-line react/jsx-props-no-spreading

View File

@@ -26,18 +26,6 @@ export interface UplotProps {
resetScales?: boolean;
}
function isAlignedData(data: unknown): data is uPlot.AlignedData {
return Array.isArray(data) && data.length > 0;
}
function isUplotOptions(options: unknown): options is uPlot.Options {
return options !== null && typeof options === 'object';
}
function isHTMLElement(el: unknown): el is HTMLElement {
return el instanceof HTMLElement;
}
const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
(
{ options, data, onDelete, onCreate, resetScales = true },
@@ -90,19 +78,6 @@ const Uplot = forwardRef<ToggleGraphProps | undefined, UplotProps>(
};
}
if (
!isUplotOptions(propOptionsRef.current) ||
!isAlignedData(propDataRef.current) ||
!isHTMLElement(targetRef.current)
) {
console.error('Uplot: Invalid options, data, or target element', {
options: propOptionsRef.current,
data: propDataRef.current,
target: targetRef.current,
});
return;
}
const newChart = new UPlot(
propOptionsRef.current,
propDataRef.current,

View File

@@ -1,12 +1,14 @@
// keep this consistent with backend plan.go
// keep this consistent with backend constants.go
export enum FeatureKeys {
SSO = 'sso',
USE_SPAN_METRICS = 'use_span_metrics',
ONBOARDING = 'onboarding',
CHAT_SUPPORT = 'chat_support',
GATEWAY = 'gateway',
PREMIUM_SUPPORT = 'premium_support',
ANOMALY_DETECTION = 'anomaly_detection',
ONBOARDING_V3 = 'onboarding_v3',
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
SSO = 'SSO',
USE_SPAN_METRICS = 'USE_SPAN_METRICS',
ONBOARDING = 'ONBOARDING',
CHAT_SUPPORT = 'CHAT_SUPPORT',
GATEWAY = 'GATEWAY',
PREMIUM_SUPPORT = 'PREMIUM_SUPPORT',
ANOMALY_DETECTION = 'ANOMALY_DETECTION',
ONBOARDING_V3 = 'ONBOARDING_V3',
THIRD_PARTY_API = 'THIRD_PARTY_API',
TRACE_FUNNELS = 'TRACE_FUNNELS',
DOT_METRICS_ENABLED = 'DOT_METRICS_ENABLED',
}

View File

@@ -1,18 +0,0 @@
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,79 +425,3 @@ 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/edit/:id',
CHANNELS_EDIT: '/settings/channels/: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: '/settings/billing',
BILLING: '/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: '/settings/shortcuts',
SHORTCUTS: '/shortcuts',
INTEGRATIONS: '/integrations',
MESSAGING_QUEUES_KAFKA: '/messaging-queues/kafka',
MESSAGING_QUEUES_KAFKA_DETAIL: '/messaging-queues/kafka/detail',

View File

@@ -1,4 +0,0 @@
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 } from 'react';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { Channels } from 'types/api/channels/getAll';
@@ -17,11 +17,12 @@ 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.push(
history.replace(
generatePath(ROUTES.CHANNELS_EDIT, {
id,
}),
@@ -55,19 +56,14 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
<Button onClick={(): void => onClickEditHandler(id)} type="link">
{t('column_channel_edit')}
</Button>
<Delete id={id} notifications={notifications} />
<Delete id={id} setChannels={setChannels} notifications={notifications} />
</>
),
});
}
return (
<ResizeTable
columns={columns}
dataSource={allChannels}
rowKey="id"
bordered
/>
<ResizeTable columns={columns} dataSource={channels} rowKey="id" bordered />
);
}

View File

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

View File

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

View File

@@ -316,6 +316,7 @@ describe('Create Alert Channel (Normal User)', () => {
expect(screen.getByText('Microsoft Teams')).toBeInTheDocument();
});
// TODO[vikrantgupta25]: check with Shaheer
it.skip('Should check if the upgrade plan message is shown', () => {
expect(screen.getByText('Upgrade to a Paid Plan')).toBeInTheDocument();
expect(

View File

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

View File

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

View File

@@ -18,7 +18,6 @@ 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';
@@ -28,6 +27,7 @@ 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 { useLocation } from 'react-router-dom';
import { matchPath, useLocation } from 'react-router-dom';
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import {
@@ -80,7 +80,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
userPreferences,
} = useAppContext();
const { notifications } = useNotifications();
@@ -331,6 +330,53 @@ 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');
@@ -547,10 +593,6 @@ 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>
@@ -603,15 +645,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
)}
<Flex
className={cx(
'app-layout',
isDarkMode ? 'darkMode dark' : 'lightMode',
sideNavPinned ? 'side-nav-pinned' : '',
)}
className={cx('app-layout', isDarkMode ? 'darkMode dark' : 'lightMode')}
>
{isToDisplayLayout && !renderFullScreen && (
<SideNav isPinned={sideNavPinned} />
)}
{isToDisplayLayout && !renderFullScreen && <SideNav />}
<div
className={cx('app-content', {
'full-screen-content': renderFullScreen,
@@ -621,7 +657,32 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<LayoutContent data-overlayscrollbars-initialize>
<OverlayScrollbar>
<ChildrenContainer>
<ChildrenContainer
style={{
margin:
isHome() ||
isLogsView() ||
isTracesView() ||
isDashboardView() ||
isDashboardWidgetView() ||
isDashboardListView() ||
isAlertHistory() ||
isAlertOverview() ||
isMessagingQueues() ||
isCloudIntegrationPage() ||
isInfraMonitoring() ||
isApiMonitoringView() ||
isExceptionsView()
? 0
: '0 1rem',
...(isTraceDetailsView() ||
isTracesFunnels() ||
isTracesFunnelDetails()
? { margin: 0 }
: {}),
}}
>
{isToDisplayLayout && !renderFullScreen && <TopNav />}
{children}
</ChildrenContainer>

View File

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

View File

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

View File

@@ -24,10 +24,6 @@ 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,
@@ -54,7 +50,6 @@ import {
X,
} from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { FormattingOptions } from 'providers/preferences/types';
import {
CSSProperties,
Dispatch,
@@ -145,9 +140,7 @@ function ExplorerOptions({
panelType,
});
} else if (isMetricsExplorer) {
logEvent(MetricsExplorerEvents.SaveViewClicked, {
[MetricsExplorerEventKeys.Tab]: 'explorer',
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
logEvent('Metrics Explorer: Save view clicked', {
panelType,
});
}
@@ -191,10 +184,8 @@ function ExplorerOptions({
panelType,
});
} else if (isMetricsExplorer) {
logEvent(MetricsExplorerEvents.AddToAlertClicked, {
logEvent('Metrics Explorer: Create alert', {
panelType,
[MetricsExplorerEventKeys.Tab]: 'explorer',
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
});
}
@@ -227,14 +218,11 @@ function ExplorerOptions({
panelType,
});
} else if (isMetricsExplorer) {
logEvent(MetricsExplorerEvents.AddToDashboardClicked, {
logEvent('Metrics Explorer: Add to dashboard clicked', {
panelType,
[MetricsExplorerEventKeys.Tab]: 'explorer',
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
});
}
setIsExport(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLogsExplorer, isMetricsExplorer, panelType, setIsExport, sourcepage]);
const {
@@ -271,26 +259,17 @@ 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;
@@ -299,14 +278,6 @@ 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 {
@@ -535,14 +506,6 @@ 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

@@ -1,12 +1,11 @@
import { Button, Typography } from 'antd';
import createDashboard from 'api/v1/dashboards/create';
import createDashboard from 'api/dashboard/create';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
import { useErrorModal } from 'providers/ErrorModalProvider';
import useAxiosError from 'hooks/useAxiosError';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
import APIError from 'types/api/error';
import { ExportPanelProps } from '.';
import {
@@ -34,28 +33,26 @@ function ExportPanelContainer({
refetch,
} = useGetAllDashboard();
const { showErrorModal } = useErrorModal();
const handleError = useAxiosError();
const {
mutate: createNewDashboard,
isLoading: createDashboardLoading,
} = useMutation(createDashboard, {
onSuccess: (data) => {
if (data.data) {
onExport(data?.data, true);
if (data.payload) {
onExport(data?.payload, true);
}
refetch();
},
onError: (error) => {
showErrorModal(error as APIError);
},
onError: handleError,
});
const options = useMemo(() => getSelectOptions(data?.data || []), [data]);
const options = useMemo(() => getSelectOptions(data || []), [data]);
const handleExportClick = useCallback((): void => {
const currentSelectedDashboard = data?.data?.find(
({ id }) => id === selectedDashboardId,
const currentSelectedDashboard = data?.find(
({ uuid }) => uuid === selectedDashboardId,
);
onExport(currentSelectedDashboard || null, false);
@@ -69,18 +66,14 @@ function ExportPanelContainer({
);
const handleNewDashboard = useCallback(async () => {
try {
await createNewDashboard({
title: t('new_dashboard_title', {
ns: 'dashboard',
}),
uploadedGrafana: false,
version: ENTITY_VERSION_V4,
});
} catch (error) {
showErrorModal(error as APIError);
}
}, [createNewDashboard, t, showErrorModal]);
createNewDashboard({
title: t('new_dashboard_title', {
ns: 'dashboard',
}),
uploadedGrafana: false,
version: ENTITY_VERSION_V4,
});
}, [t, createNewDashboard]);
const isDashboardLoading = isAllDashboardsLoading || createDashboardLoading;

View File

@@ -1,10 +1,12 @@
import { SelectProps } from 'antd';
import { Dashboard } from 'types/api/dashboard/getAll';
import { PayloadProps as AllDashboardsData } from 'types/api/dashboard/getAll';
export const getSelectOptions = (data: Dashboard[]): SelectProps['options'] =>
data.map(({ id, data }) => ({
export const getSelectOptions = (
data: AllDashboardsData,
): SelectProps['options'] =>
data.map(({ uuid, data }) => ({
label: data.title,
value: id,
value: uuid,
}));
export const filterOptions: SelectProps['filterOption'] = (

View File

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

View File

@@ -36,6 +36,7 @@ function QuerySection({
const { t } = useTranslation('alerts');
const [currentTab, setCurrentTab] = useState(queryCategory);
// TODO[vikrantgupta25] : check if this is still required ??
const handleQueryCategoryChange = (queryType: string): void => {
setQueryCategory(queryType as EQueryType);
setCurrentTab(queryType as EQueryType);

View File

@@ -85,13 +85,7 @@ function LabelSelect({
}, [handleBlur]);
const handleLabelChange = (event: ChangeEvent<HTMLInputElement>): void => {
// 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,
);
setCurrentVal(event.target?.value.replace(':', ''));
};
const handleClose = (key: string): void => {

View File

@@ -38,7 +38,7 @@ export default function DashboardEmptyState(): JSX.Element {
setSelectedRowWidgetId(null);
handleToggleDashboardSlider(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.id,
dashboardId: selectedDashboard?.uuid,
dashboardName: selectedDashboard?.data.title,
numberOfPanels: selectedDashboard?.data.widgets?.length,
});

View File

@@ -2,7 +2,6 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { AppProvider } from 'providers/App/App';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import { Provider } from 'react-redux';
import store from 'store';
@@ -190,26 +189,24 @@ describe('WidgetGraphComponent', () => {
it('should show correct menu items when hovering over more options while loading', async () => {
const { getByTestId, findByRole, getByText, container } = render(
<MockQueryClientProvider>
<ErrorModalProvider>
<Provider store={store}>
<AppProvider>
<WidgetGraphComponent
widget={mockProps.widget}
queryResponse={mockProps.queryResponse}
errorMessage={mockProps.errorMessage}
version={mockProps.version}
headerMenuList={mockProps.headerMenuList}
isWarning={mockProps.isWarning}
isFetchingResponse={mockProps.isFetchingResponse}
setRequestData={mockProps.setRequestData}
onClickHandler={mockProps.onClickHandler}
onDragSelect={mockProps.onDragSelect}
openTracesButton={mockProps.openTracesButton}
onOpenTraceBtnClick={mockProps.onOpenTraceBtnClick}
/>
</AppProvider>
</Provider>
</ErrorModalProvider>
<Provider store={store}>
<AppProvider>
<WidgetGraphComponent
widget={mockProps.widget}
queryResponse={mockProps.queryResponse}
errorMessage={mockProps.errorMessage}
version={mockProps.version}
headerMenuList={mockProps.headerMenuList}
isWarning={mockProps.isWarning}
isFetchingResponse={mockProps.isFetchingResponse}
setRequestData={mockProps.setRequestData}
onClickHandler={mockProps.onClickHandler}
onDragSelect={mockProps.onDragSelect}
openTracesButton={mockProps.openTracesButton}
onOpenTraceBtnClick={mockProps.onOpenTraceBtnClick}
/>
</AppProvider>
</Provider>
</MockQueryClientProvider>,
);

View File

@@ -4,6 +4,7 @@ import { Skeleton, Tooltip, Typography } from 'antd';
import cx from 'classnames';
import { useNavigateToExplorer } from 'components/CeleryTask/useNavigateToExplorer';
import { ToggleGraphProps } from 'components/Graph/types';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { placeWidgetAtBottom } from 'container/NewWidget/utils';
@@ -30,7 +31,7 @@ import {
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
import { Props } from 'types/api/dashboard/update';
import { Dashboard } from 'types/api/dashboard/getAll';
import { DataSource } from 'types/common/queryBuilder';
import { v4 } from 'uuid';
@@ -118,23 +119,29 @@ function WidgetGraphComponent({
const updatedLayout =
selectedDashboard.data.layout?.filter((e) => e.i !== widget.id) || [];
const updatedSelectedDashboard: Props = {
const updatedSelectedDashboard: Dashboard = {
...selectedDashboard,
data: {
...selectedDashboard.data,
widgets: updatedWidgets,
layout: updatedLayout,
},
id: selectedDashboard.id,
uuid: selectedDashboard.uuid,
};
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
onSuccess: (updatedDashboard) => {
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.payload) {
setSelectedDashboard(updatedDashboard.payload);
}
setDeleteModal(false);
},
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
};
@@ -159,8 +166,7 @@ function WidgetGraphComponent({
updateDashboardMutation.mutateAsync(
{
id: selectedDashboard.id,
...selectedDashboard,
data: {
...selectedDashboard.data,
layout,
@@ -177,9 +183,9 @@ function WidgetGraphComponent({
},
{
onSuccess: (updatedDashboard) => {
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.payload) {
setSelectedDashboard(updatedDashboard.payload);
}
notifications.success({
message: 'Panel cloned successfully, redirecting to new copy.',
@@ -246,11 +252,7 @@ function WidgetGraphComponent({
const graphClick = useGraphClickToShowButton({
graphRef: currentGraphRef?.current ? currentGraphRef : graphRef,
isButtonEnabled: (widget?.query?.builder?.queryData &&
Array.isArray(widget.query.builder.queryData)
? widget.query.builder.queryData
: []
).some(
isButtonEnabled: (widget?.query?.builder?.queryData ?? []).some(
(q) =>
q.dataSource === DataSource.TRACES || q.dataSource === DataSource.LOGS,
),

View File

@@ -160,6 +160,8 @@ function GridCardGraph({
};
});
// TODO [vikrantgupta25] remove this useEffect with refactor as this is prone to race condition
// this is added to tackle the case of async communication between VariableItem.tsx and GridCard.tsx
useEffect(() => {
if (variablesToGetUpdated.length > 0) {
queryClient.cancelQueries([

View File

@@ -31,9 +31,7 @@ export const getLocalStorageGraphVisibilityState = ({
name: string;
}): GraphVisibilityLegendEntryProps => {
const visibilityStateAndLegendEntry: GraphVisibilityLegendEntryProps = {
graphVisibilityStates: Array(
(Array.isArray(apiResponse) ? apiResponse.length : 0) + 1,
).fill(true),
graphVisibilityStates: Array(apiResponse.length + 1).fill(true),
legendEntry: [
{
label: 'Timestamp',

View File

@@ -6,6 +6,7 @@ import { Button, Form, Input, Modal, Typography } from 'antd';
import { useForm } from 'antd/es/form/Form';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import { themeColors } from 'constants/theme';
@@ -13,6 +14,7 @@ import { DEFAULT_ROW_NAME } from 'container/NewDashboard/DashboardDescription/ut
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import useComponentPermission from 'hooks/useComponentPermission';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { defaultTo, isUndefined } from 'lodash-es';
@@ -34,8 +36,7 @@ import { ItemCallback, Layout } from 'react-grid-layout';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { Widgets } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
@@ -106,6 +107,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
const updateDashboardMutation = useUpdateDashboard();
const { notifications } = useNotifications();
const urlQuery = useUrlQuery();
let permissions: ComponentTypes[] = ['save_layout', 'add_panel'];
@@ -156,20 +158,20 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data)) {
logEvent('Dashboard Detail: Opened', {
dashboardId: selectedDashboard?.id,
dashboardId: data.uuid,
dashboardName: data.title,
numberOfPanels: data.widgets?.length,
numberOfVariables: Object.keys(data?.variables || {}).length || 0,
});
logEventCalledRef.current = true;
}
}, [data, selectedDashboard?.id]);
}, [data]);
const onSaveHandler = (): void => {
if (!selectedDashboard) return;
const updatedDashboard: Props = {
id: selectedDashboard.id,
const updatedDashboard: Dashboard = {
...selectedDashboard,
data: {
...selectedDashboard.data,
panelMap: { ...currentPanelMap },
@@ -184,18 +186,24 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
return widget;
}),
},
uuid: selectedDashboard.uuid,
};
updateDashboardMutation.mutate(updatedDashboard, {
onSuccess: (updatedDashboard) => {
setSelectedRowWidgetId(null);
if (updatedDashboard.data) {
if (updatedDashboard.data.data.layout)
setLayouts(sortLayout(updatedDashboard.data.data.layout));
setSelectedDashboard(updatedDashboard.data);
setPanelMap(updatedDashboard.data?.data?.panelMap || {});
if (updatedDashboard.payload) {
if (updatedDashboard.payload.data.layout)
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
setSelectedDashboard(updatedDashboard.payload);
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
}
},
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
};
@@ -278,25 +286,33 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
updatedWidgets?.push(currentWidget);
const updatedSelectedDashboard: Props = {
id: selectedDashboard.id,
const updatedSelectedDashboard: Dashboard = {
...selectedDashboard,
data: {
...selectedDashboard.data,
widgets: updatedWidgets,
},
uuid: selectedDashboard.uuid,
};
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
onSuccess: (updatedDashboard) => {
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.payload) {
setSelectedDashboard(updatedDashboard.payload);
}
if (setPanelMap) setPanelMap(updatedDashboard.data?.data?.panelMap || {});
if (setPanelMap)
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
form.setFieldValue('title', '');
setIsSettingsModalOpen(false);
setCurrentSelectRowId(null);
},
// eslint-disable-next-line sonarjs/no-identical-functions
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
};
@@ -399,14 +415,12 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
};
const handleDragStop: ItemCallback = (_, oldItem, newItem): void => {
if (oldItem?.i && currentPanelMap?.[oldItem.i]) {
if (currentPanelMap[oldItem.i]) {
const differenceY = newItem.y - oldItem.y;
const widgetsInsideRow = (currentPanelMap[oldItem.i]?.widgets ?? []).map(
(w) => ({
...w,
y: w.y + differenceY,
}),
);
const widgetsInsideRow = currentPanelMap[oldItem.i].widgets.map((w) => ({
...w,
y: w.y + differenceY,
}));
setCurrentPanelMap((prev) => ({
...prev,
[oldItem.i]: {
@@ -433,26 +447,34 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
const updatedPanelMap = { ...currentPanelMap };
delete updatedPanelMap[currentSelectRowId];
const updatedSelectedDashboard: Props = {
id: selectedDashboard.id,
const updatedSelectedDashboard: Dashboard = {
...selectedDashboard,
data: {
...selectedDashboard.data,
widgets: updatedWidgets,
layout: updatedLayout,
panelMap: updatedPanelMap,
},
uuid: selectedDashboard.uuid,
};
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
onSuccess: (updatedDashboard) => {
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.data) {
setSelectedDashboard(updatedDashboard.data);
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
if (setSelectedDashboard && updatedDashboard.payload) {
setSelectedDashboard(updatedDashboard.payload);
}
if (setPanelMap) setPanelMap(updatedDashboard.data?.data?.panelMap || {});
if (setPanelMap)
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
setIsDeleteModalOpen(false);
setCurrentSelectRowId(null);
},
// eslint-disable-next-line sonarjs/no-identical-functions
onError: () => {
notifications.error({
message: SOMETHING_WENT_WRONG,
});
},
});
};
const isDashboardEmpty = useMemo(

View File

@@ -102,11 +102,11 @@ export function updateStepInterval(
return {
...query,
builder: {
...query?.builder,
...query.builder,
queryData: [
...(query?.builder?.queryData ?? []).map((queryData) => ({
...query.builder.queryData.map((queryData) => ({
...queryData,
stepInterval: stepIntervalPoints || queryData?.stepInterval || 60,
stepInterval: stepIntervalPoints || queryData.stepInterval || 60,
})),
],
},

View File

@@ -110,24 +110,24 @@ export function getQueryLegend(
switch (currentQuery.queryType) {
case EQueryType.QUERY_BUILDER:
// check if the value is present in the queries
legend = currentQuery?.builder?.queryData?.find(
legend = currentQuery.builder.queryData.find(
(query) => query.queryName === queryName,
)?.legend;
if (!legend) {
// check if the value is present in the formula
legend = currentQuery?.builder?.queryFormulas?.find(
legend = currentQuery.builder.queryFormulas.find(
(query) => query.queryName === queryName,
)?.legend;
}
break;
case EQueryType.CLICKHOUSE:
legend = currentQuery?.clickhouse_sql?.find(
legend = currentQuery.clickhouse_sql.find(
(query) => query.name === queryName,
)?.legend;
break;
case EQueryType.PROM:
legend = currentQuery?.promql?.find((query) => query.name === queryName)
legend = currentQuery.promql.find((query) => query.name === queryName)
?.legend;
break;
default:

View File

@@ -33,7 +33,7 @@ export default function Dashboards({
useEffect(() => {
if (!dashboardsList) return;
const sortedDashboards = dashboardsList.data.sort((a, b) => {
const sortedDashboards = dashboardsList.sort((a, b) => {
const aUpdateAt = new Date(a.updatedAt).getTime();
const bUpdateAt = new Date(b.updatedAt).getTime();
return bUpdateAt - aUpdateAt;
@@ -103,7 +103,7 @@ export default function Dashboards({
<div className="home-dashboards-list-container home-data-item-container">
<div className="dashboards-list">
{sortedDashboards.slice(0, 5).map((dashboard) => {
const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${dashboard.id}`;
const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${dashboard.uuid}`;
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
event.stopPropagation();
@@ -134,7 +134,7 @@ export default function Dashboards({
<div className="dashboard-item-name-container home-data-item-name-container">
<img
src={
Math.random() % 2 === 0
dashboard.id % 2 === 0
? '/Icons/eight-ball.svg'
: '/Icons/circus-tent.svg'
}

View File

@@ -6,13 +6,12 @@ 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 listUserPreferences from 'api/v1/user/preferences/list';
import updateUserPreferenceAPI from 'api/v1/user/preferences/name/update';
import getAllUserPreferences from 'api/preferences/getAllUserPreference';
import updateUserPreferenceAPI from 'api/preferences/updateUserPreference';
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';
@@ -30,8 +29,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';
@@ -185,25 +184,18 @@ export default function Home(): JSX.Element {
);
const processUserPreferences = (userPreferences: UserPreference[]): void => {
const checklistSkipped = Boolean(
userPreferences?.find(
(preference) =>
preference.name === ORG_PREFERENCES.WELCOME_CHECKLIST_DO_LATER,
)?.value,
);
const checklistSkipped = userPreferences?.find(
(preference) => preference.key === 'WELCOME_CHECKLIST_DO_LATER',
)?.value;
const updatedChecklistItems = cloneDeep(checklistItems);
const newChecklistItems = updatedChecklistItems.map((item) => {
const newItem = { ...item };
const isSkipped = Boolean(
newItem.isSkipped =
userPreferences?.find(
(preference) => preference.name === item.skippedPreferenceKey,
)?.value,
);
newItem.isSkipped = isSkipped || false;
(preference) => preference.key === item.skippedPreferenceKey,
)?.value || false;
return newItem;
});
@@ -214,13 +206,13 @@ export default function Home(): JSX.Element {
// Fetch User Preferences
const { refetch: refetchUserPreferences } = useQuery({
queryFn: () => listUserPreferences(),
queryFn: () => getAllUserPreferences(),
queryKey: ['getUserPreferences'],
enabled: true,
refetchOnWindowFocus: false,
onSuccess: (response) => {
if (response.data) {
processUserPreferences(response.data);
if (response.payload && response.payload.data) {
processUserPreferences(response.payload.data);
}
setLoadingUserPreferences(false);
@@ -247,7 +239,7 @@ export default function Home(): JSX.Element {
setUpdatingUserPreferences(true);
updateUserPreference({
name: ORG_PREFERENCES.WELCOME_CHECKLIST_DO_LATER,
preferenceID: 'WELCOME_CHECKLIST_DO_LATER',
value: true,
});
};
@@ -257,7 +249,7 @@ export default function Home(): JSX.Element {
setUpdatingUserPreferences(true);
updateUserPreference({
name: item.skippedPreferenceKey,
preferenceID: item.skippedPreferenceKey,
value: true,
});
}

View File

@@ -1,19 +1,17 @@
import { ORG_PREFERENCES } from 'constants/orgPreferences';
import ROUTES from 'constants/routes';
import { ChecklistItem } from './HomeChecklist/HomeChecklist';
export const checkListStepToPreferenceKeyMap = {
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,
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',
};
export const DOCS_LINKS = {

View File

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

View File

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

View File

@@ -41,6 +41,7 @@ const { Search } = Input;
function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
const { t } = useTranslation('common');
const { user } = useAppContext();
// TODO[vikrantgupta25]: check with sagar on cleanup
const [addNewAlert, action] = useComponentPermission(
['add_new_alert', 'action'],
user.role,

View File

@@ -22,7 +22,7 @@ import {
} from 'antd';
import { TableProps } from 'antd/lib';
import logEvent from 'api/common/logEvent';
import createDashboard from 'api/v1/dashboards/create';
import createDashboard from 'api/dashboard/create';
import { AxiosError } from 'axios';
import cx from 'classnames';
import { ENTITY_VERSION_V4 } from 'constants/app';
@@ -63,7 +63,6 @@ import {
import { handleContactSupport } from 'pages/Integrations/utils';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useTimezone } from 'providers/Timezone';
import {
ChangeEvent,
@@ -84,7 +83,6 @@ import {
WidgetRow,
Widgets,
} from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
import ImportJSON from './ImportJSON';
@@ -228,7 +226,7 @@ function DashboardsList(): JSX.Element {
useEffect(() => {
const filteredDashboards = filterDashboard(
searchString,
dashboardListResponse?.data || [],
dashboardListResponse || [],
);
if (sortOrder.columnKey === 'updatedAt') {
sortDashboardsByUpdatedAt(filteredDashboards || []);
@@ -258,19 +256,17 @@ function DashboardsList(): JSX.Element {
errorMessage: '',
});
const { showErrorModal } = useErrorModal();
const data: Data[] =
dashboards?.map((e) => ({
createdAt: e.createdAt,
description: e.data.description || '',
id: e.id,
id: e.uuid,
lastUpdatedTime: e.updatedAt,
name: e.data.title,
tags: e.data.tags || [],
key: e.id,
key: e.uuid,
createdBy: e.createdBy,
isLocked: !!e.locked || false,
isLocked: !!e.isLocked || false,
lastUpdatedBy: e.updatedBy,
image: e.data.image || Base64Icons[0],
variables: e.data.variables,
@@ -296,20 +292,28 @@ function DashboardsList(): JSX.Element {
version: ENTITY_VERSION_V4,
});
safeNavigate(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.data.id,
}),
);
if (response.statusCode === 200) {
safeNavigate(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.payload.uuid,
}),
);
} else {
setNewDashboardState({
...newDashboardState,
loading: false,
error: true,
errorMessage: response.error || 'Something went wrong',
});
}
} catch (error) {
showErrorModal(error as APIError);
setNewDashboardState({
...newDashboardState,
error: true,
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
});
}
}, [newDashboardState, safeNavigate, showErrorModal, t]);
}, [newDashboardState, safeNavigate, t]);
const onModalHandler = (uploadedGrafana: boolean): void => {
logEvent('Dashboard List: Import JSON clicked', {});
@@ -323,7 +327,7 @@ function DashboardsList(): JSX.Element {
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
const filteredDashboards = filterDashboard(
searchText,
dashboardListResponse?.data || [],
dashboardListResponse || [],
);
setDashboards(filteredDashboards);
setIsFilteringDashboards(false);
@@ -673,7 +677,7 @@ function DashboardsList(): JSX.Element {
!isUndefined(dashboardListResponse)
) {
logEvent('Dashboard List: Page visited', {
number: dashboardListResponse?.data?.length,
number: dashboardListResponse?.length,
});
logEventCalledRef.current = true;
}

View File

@@ -14,21 +14,19 @@ import {
UploadProps,
} from 'antd';
import logEvent from 'api/common/logEvent';
import createDashboard from 'api/v1/dashboards/create';
import createDashboard from 'api/dashboard/create';
import ROUTES from 'constants/routes';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
import { ExternalLink, Github, MonitorDot, MoveRight, X } from 'lucide-react';
import { useErrorModal } from 'providers/ErrorModalProvider';
// #TODO: Lucide will be removing brand icons like GitHub in the future. In that case, we can use Simple Icons. https://simpleicons.org/
// See more: https://github.com/lucide-icons/lucide/issues/94
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { DashboardData } from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
function ImportJSON({
isImportJSONModalVisible,
@@ -76,8 +74,6 @@ function ImportJSON({
}
};
const { showErrorModal } = useErrorModal();
const onClickLoadJsonHandler = async (): Promise<void> => {
try {
setDashboardCreating(true);
@@ -85,6 +81,11 @@ function ImportJSON({
const dashboardData = JSON.parse(editorValue) as DashboardData;
// Remove uuid from the dashboard data, in all cases - empty, duplicate or any valid not duplicate uuid
if (dashboardData.uuid !== undefined) {
delete dashboardData.uuid;
}
if (dashboardData?.layout) {
dashboardData.layout = getUpdatedLayout(dashboardData.layout);
} else {
@@ -96,19 +97,28 @@ function ImportJSON({
uploadedGrafana,
});
safeNavigate(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.data.id,
}),
);
logEvent('Dashboard List: New dashboard imported successfully', {
dashboardId: response.data?.id,
dashboardName: response.data?.data?.title,
});
if (response.statusCode === 200) {
safeNavigate(
generatePath(ROUTES.DASHBOARD, {
dashboardId: response.payload.uuid,
}),
);
logEvent('Dashboard List: New dashboard imported successfully', {
dashboardId: response.payload?.uuid,
dashboardName: response.payload?.data?.title,
});
} else {
setIsCreateDashboardError(true);
notifications.error({
message:
response.error ||
t('something_went_wrong', {
ns: 'common',
}),
});
}
setDashboardCreating(false);
} catch (error) {
showErrorModal(error as APIError);
setDashboardCreating(false);
setIsCreateDashboardError(true);
notifications.error({

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