mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-28 13:34:18 +00:00
Compare commits
56 Commits
chore/tf/t
...
fixes-qb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16e0ef2515 | ||
|
|
fecf6667a3 | ||
|
|
bda2316377 | ||
|
|
fff7f8fc76 | ||
|
|
8cfeef4521 | ||
|
|
d85a1a21ac | ||
|
|
17f48d656d | ||
|
|
2d6774da68 | ||
|
|
6404e7388e | ||
|
|
62a9d7e602 | ||
|
|
3a2c7a7a68 | ||
|
|
33e70d1f37 | ||
|
|
85f04e4bae | ||
|
|
53f9e7d811 | ||
|
|
ad46e22561 | ||
|
|
e79195ccf1 | ||
|
|
f77bb888a8 | ||
|
|
baa15baea9 | ||
|
|
316e6821f1 | ||
|
|
a1fa2769e4 | ||
|
|
decb660992 | ||
|
|
0acbcf8322 | ||
|
|
11eabdc2ac | ||
|
|
eb94554f5a | ||
|
|
e8280dbea4 | ||
|
|
44ea237039 | ||
|
|
72b0214d1d | ||
|
|
386a215324 | ||
|
|
ba0ba4bbc9 | ||
|
|
d60c9ab36b | ||
|
|
90770b90bd | ||
|
|
a19874c1dd | ||
|
|
65ff460d63 | ||
|
|
b9d542a294 | ||
|
|
e75e5bdbdb | ||
|
|
0d03203977 | ||
|
|
28f6f42ac4 | ||
|
|
92f8e4d5b9 | ||
|
|
037eea5262 | ||
|
|
cd4df6280f | ||
|
|
ad2d4ed56c | ||
|
|
7955497a8d | ||
|
|
6ed30318bd | ||
|
|
c32dd9f17e | ||
|
|
c58cf67eb0 | ||
|
|
440c3d8386 | ||
|
|
d683b94344 | ||
|
|
6a629623bc | ||
|
|
982688ccc9 | ||
|
|
74bbb26033 | ||
|
|
3bb9e05681 | ||
|
|
61b2f8cb31 | ||
|
|
9d397d0867 | ||
|
|
5fb4206a99 | ||
|
|
dd11ba9f48 | ||
|
|
f9cb9f10be |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -11,5 +11,5 @@
|
||||
/pkg/errors/ @grandwizard28
|
||||
/pkg/factory/ @grandwizard28
|
||||
/pkg/types/ @grandwizard28
|
||||
/pkg/sqlmigration/ @vikrantgupta25
|
||||
.golangci.yml @grandwizard28
|
||||
**/(zeus|licensing|sqlmigration)/ @vikrantgupta25
|
||||
3
.github/workflows/build-community.yaml
vendored
3
.github/workflows/build-community.yaml
vendored
@@ -74,7 +74,8 @@ jobs:
|
||||
-X github.com/SigNoz/signoz/pkg/version.variant=community
|
||||
-X github.com/SigNoz/signoz/pkg/version.hash=${{ needs.prepare.outputs.hash }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.time=${{ needs.prepare.outputs.time }}
|
||||
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}'
|
||||
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}
|
||||
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
|
||||
GO_CGO_ENABLED: 1
|
||||
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
|
||||
DOCKER_DOCKERFILE_PATH: ./pkg/query-service/Dockerfile.multi-arch
|
||||
|
||||
3
.github/workflows/build-enterprise.yaml
vendored
3
.github/workflows/build-enterprise.yaml
vendored
@@ -108,7 +108,8 @@ jobs:
|
||||
-X github.com/SigNoz/signoz/ee/zeus.url=https://api.signoz.cloud
|
||||
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.signoz.io
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.signoz.cloud
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1'
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1
|
||||
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
|
||||
GO_CGO_ENABLED: 1
|
||||
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
|
||||
DOCKER_DOCKERFILE_PATH: ./ee/query-service/Dockerfile.multi-arch
|
||||
|
||||
3
.github/workflows/build-staging.yaml
vendored
3
.github/workflows/build-staging.yaml
vendored
@@ -107,7 +107,8 @@ jobs:
|
||||
-X github.com/SigNoz/signoz/ee/zeus.url=https://api.staging.signoz.cloud
|
||||
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.staging.signoz.cloud
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.staging.signoz.cloud
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.staging.signoz.cloud/api/v1'
|
||||
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.staging.signoz.cloud/api/v1
|
||||
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
|
||||
GO_CGO_ENABLED: 1
|
||||
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
|
||||
DOCKER_DOCKERFILE_PATH: ./ee/query-service/Dockerfile.multi-arch
|
||||
|
||||
@@ -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/query-service?label=Docker Downloads"> </a>
|
||||
<img alt="Downloads" src="https://img.shields.io/docker/pulls/signoz/signoz.svg?label=Docker%20Downloads"> </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>
|
||||
|
||||
@@ -165,12 +165,6 @@ alertmanager:
|
||||
# Retention of the notification logs.
|
||||
retention: 120h
|
||||
|
||||
|
||||
##################### Analytics #####################
|
||||
analytics:
|
||||
# Whether to enable analytics.
|
||||
enabled: false
|
||||
|
||||
##################### Emailing #####################
|
||||
emailing:
|
||||
# Whether to enable emailing.
|
||||
@@ -215,3 +209,18 @@ sharder:
|
||||
single:
|
||||
# The org id to which this instance belongs to.
|
||||
org_id: org_id
|
||||
|
||||
##################### Analytics #####################
|
||||
analytics:
|
||||
# Whether to enable analytics.
|
||||
enabled: false
|
||||
segment:
|
||||
# The key to use for segment.
|
||||
key: ""
|
||||
|
||||
##################### StatsReporter #####################
|
||||
statsreporter:
|
||||
# Whether to enable stats reporter. This is used to provide valuable insights to the SigNoz team. It does not collect any sensitive/PII data.
|
||||
enabled: true
|
||||
# The interval at which the stats are collected.
|
||||
interval: 6h
|
||||
|
||||
@@ -174,7 +174,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.85.3
|
||||
image: signoz/signoz:v0.87.0
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
|
||||
@@ -110,7 +110,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.85.3
|
||||
image: signoz/signoz:v0.87.0
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
ports:
|
||||
|
||||
@@ -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.85.3}
|
||||
image: signoz/signoz:${VERSION:-v0.87.0}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
|
||||
@@ -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.85.3}
|
||||
image: signoz/signoz:${VERSION:-v0.87.0}
|
||||
container_name: signoz
|
||||
command:
|
||||
- --config=/root/config/prometheus.yml
|
||||
|
||||
51
docs/contributing/go/endpoint.md
Normal file
51
docs/contributing/go/endpoint.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Endpoint
|
||||
|
||||
This guide outlines the recommended approach for designing endpoints, with a focus on entity relationships, RESTful structure, and examples from the codebase.
|
||||
|
||||
## How do we design an endpoint?
|
||||
|
||||
### Understand the core entities and their relationships
|
||||
|
||||
Start with understanding the core entities and their relationships. For example:
|
||||
|
||||
- **Organization**: an organization can have multiple users
|
||||
|
||||
### Structure Endpoints RESTfully
|
||||
|
||||
Endpoints should reflect the resource hierarchy and follow RESTful conventions. Use clear, **pluralized resource names** and versioning. For example:
|
||||
|
||||
- `POST /v1/organizations` — Create an organization
|
||||
- `GET /v1/organizations/:id` — Get an organization by id
|
||||
- `DELETE /v1/organizations/:id` — Delete an organization by id
|
||||
- `PUT /v1/organizations/:id` — Update an organization by id
|
||||
- `GET /v1/organizations/:id/users` — Get all users in an organization
|
||||
- `GET /v1/organizations/me/users` — Get all users in my organization
|
||||
|
||||
Think in terms of resource navigation in a file system. For example, to find your organization, you would navigate to the root of the file system and then to the `organizations` directory. To find a user in an organization, you would navigate to the `organizations` directory and then to the `id` directory.
|
||||
|
||||
```bash
|
||||
v1/
|
||||
├── organizations/
|
||||
│ └── 123/
|
||||
│ └── users/
|
||||
```
|
||||
|
||||
`me` endpoints are special. They are used to determine the actual id via some auth/external mechanism. For `me` endpoints, think of the `me` directory being symlinked to your organization directory. For example, if you are a part of the organization `123`, the `me` directory will be symlinked to `/v1/organizations/123`:
|
||||
|
||||
```bash
|
||||
v1/
|
||||
├── organizations/
|
||||
│ └── me/ -> symlink to /v1/organizations/123
|
||||
│ └── users/
|
||||
│ └── 123/
|
||||
│ └── users/
|
||||
```
|
||||
|
||||
> 💡 **Note**: There are various ways to structure endpoints. Some prefer to use singular resource names instead of `me`. Others prefer to use singular resource names for all endpoints. We have, however, chosen to standardize our endpoints in the manner described above.
|
||||
|
||||
## What should I remember?
|
||||
|
||||
- Use clear, **plural resource names**
|
||||
- Use `me` endpoints for determining the actual id via some auth mechanism
|
||||
|
||||
> 💡 **Note**: When in doubt, diagram the relationships and walk through the user flows as if navigating a file system. This will help you design endpoints that are both logical and user-friendly.
|
||||
106
docs/contributing/go/provider.md
Normal file
106
docs/contributing/go/provider.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# Provider
|
||||
|
||||
SigNoz is built on the provider pattern, a design approach where code is organized into providers that handle specific application responsibilities. Providers act as adapter components that integrate with external services and deliver required functionality to the application.
|
||||
|
||||
> 💡 **Note**: Coming from a DDD background? Providers are similar (not exactly the same) to adapter/infrastructure services.
|
||||
|
||||
## How to create a new provider?
|
||||
|
||||
To create a new provider, create a directory in the `pkg/` directory named after your provider. The provider package consists of four key components:
|
||||
|
||||
- **Interface** (`pkg/<name>/<name>.go`): Defines the provider's interface. Other packages should import this interface to use the provider.
|
||||
- **Config** (`pkg/<name>/config.go`): Contains provider configuration, implementing the `factory.Config` interface from [factory/config.go](/pkg/factory/config.go).
|
||||
- **Implementation** (`pkg/<name>/<implname><name>/provider.go`): Contains the provider implementation, including a `NewProvider` function that returns a `factory.Provider` interface from [factory/provider.go](/pkg/factory/provider.go).
|
||||
- **Mock** (`pkg/<name>/<name>test.go`): Provides mocks for the provider, typically used by dependent packages for unit testing.
|
||||
|
||||
For example, the [prometheus](/pkg/prometheus) provider delivers a prometheus engine to the application:
|
||||
|
||||
- `pkg/prometheus/prometheus.go` - Interface definition
|
||||
- `pkg/prometheus/config.go` - Configuration
|
||||
- `pkg/prometheus/clickhouseprometheus/provider.go` - Clickhouse-powered implementation
|
||||
- `pkg/prometheus/prometheustest/provider.go` - Mock implementation
|
||||
|
||||
## How to wire it up?
|
||||
|
||||
The `pkg/signoz` package contains the inversion of control container responsible for wiring providers. It handles instantiation, configuration, and assembly of providers based on configuration metadata.
|
||||
|
||||
> 💡 **Note**: Coming from a Java background? Providers are similar to Spring beans.
|
||||
|
||||
Wiring up a provider involves three steps:
|
||||
|
||||
1. Wiring up the configuration
|
||||
Add your config from `pkg/<name>/config.go` to the `pkg/signoz/config.Config` struct and in new factories:
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
...
|
||||
MyProvider myprovider.Config `mapstructure:"myprovider"`
|
||||
...
|
||||
}
|
||||
|
||||
func NewConfig(ctx context.Context, resolverConfig config.ResolverConfig, ....) (Config, error) {
|
||||
...
|
||||
configFactories := []factory.ConfigFactory{
|
||||
myprovider.NewConfigFactory(),
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
2. Wiring up the provider
|
||||
Add available provider implementations in `pkg/signoz/provider.go`:
|
||||
|
||||
```go
|
||||
func NewMyProviderFactories() factory.NamedMap[factory.ProviderFactory[myprovider.MyProvider, myprovider.Config]] {
|
||||
return factory.MustNewNamedMap(
|
||||
myproviderone.NewFactory(),
|
||||
myprovidertwo.NewFactory(),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
3. Instantiate the provider by adding it to the `SigNoz` struct in `pkg/signoz/signoz.go`:
|
||||
|
||||
```go
|
||||
type SigNoz struct {
|
||||
...
|
||||
MyProvider myprovider.MyProvider
|
||||
...
|
||||
}
|
||||
|
||||
func New(...) (*SigNoz, error) {
|
||||
...
|
||||
myprovider, err := myproviderone.New(ctx, settings, config.MyProvider, "one/two")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## How to use it?
|
||||
|
||||
To use a provider, import its interface. For example, to use the prometheus provider, import `pkg/prometheus/prometheus.go`:
|
||||
|
||||
```go
|
||||
import "github.com/SigNoz/signoz/pkg/prometheus/prometheus"
|
||||
|
||||
func CreateSomething(ctx context.Context, prometheus prometheus.Prometheus) {
|
||||
...
|
||||
prometheus.DoSomething()
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Why do we need this?
|
||||
|
||||
Like any dependency injection framework, providers decouple the codebase from implementation details. This is especially valuable in SigNoz's large codebase, where we need to swap implementations without changing dependent code. The provider pattern offers several benefits apart from the obvious one of decoupling:
|
||||
|
||||
- Configuration is **defined with each provider and centralized in one place**, making it easier to understand and manage through various methods (environment variables, config files, etc.)
|
||||
- Provider mocking is **straightforward for unit testing**, with a consistent pattern for locating mocks
|
||||
- **Multiple implementations** of the same provider are **supported**, as demonstrated by our sqlstore provider
|
||||
|
||||
## What should I remember?
|
||||
|
||||
- Use the provider pattern wherever applicable.
|
||||
- Always create a provider **irrespective of the number of implementations**. This makes it easier to add new implementations in the future.
|
||||
@@ -3,7 +3,6 @@ package httplicensing
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
|
||||
@@ -12,7 +11,6 @@ import (
|
||||
"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/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
@@ -88,13 +86,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -115,11 +106,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -139,28 +125,24 @@ 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 && !errors.Ast(err, errors.TypeNotFound) {
|
||||
if err != nil {
|
||||
if errors.Ast(err, errors.TypeNotFound) {
|
||||
return nil
|
||||
}
|
||||
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 {
|
||||
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)
|
||||
activeLicense.UpdateFeatures(licensetypes.BasicPlan)
|
||||
updatedStorableLicense := licensetypes.NewStorableLicenseFromLicense(activeLicense)
|
||||
err = provider.store.Update(ctx, organizationID, updatedStorableLicense)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
@@ -218,80 +200,27 @@ func (provider *provider) Portal(ctx context.Context, organizationID valuer.UUID
|
||||
return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
func (provider *provider) GetFeatureFlags(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.Feature, error) {
|
||||
license, err := provider.GetActive(ctx, organizationID)
|
||||
if err != nil {
|
||||
if errors.Ast(err, errors.TypeNotFound) {
|
||||
return licensetypes.BasicPlan, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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 license.Features, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
|
||||
activeLicense, err := provider.GetActive(ctx, orgID)
|
||||
if err != nil {
|
||||
if errors.Ast(err, errors.TypeNotFound) {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
return licensetypes.NewStatsFromLicense(activeLicense), nil
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ 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"
|
||||
)
|
||||
@@ -80,81 +79,3 @@ 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
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ builds:
|
||||
- -X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.signoz.io
|
||||
- -X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.signoz.cloud
|
||||
- -X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1
|
||||
- -X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr
|
||||
- >-
|
||||
{{- if eq .Os "linux" }}-linkmode external -extldflags '-static'{{- end }}
|
||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"time"
|
||||
@@ -13,6 +12,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
"github.com/SigNoz/signoz/pkg/apis/fields"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
querierAPI "github.com/SigNoz/signoz/pkg/querier"
|
||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
||||
@@ -59,8 +59,9 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler,
|
||||
FluxInterval: opts.FluxInterval,
|
||||
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
|
||||
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
|
||||
FieldsAPI: fields.NewAPI(signoz.TelemetryStore, signoz.Instrumentation.Logger()),
|
||||
FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore),
|
||||
Signoz: signoz,
|
||||
QuerierAPI: querierAPI.NewAPI(signoz.Querier),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -86,23 +87,12 @@ 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/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)
|
||||
router.HandleFunc("/api/v1/features", am.ViewAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
|
||||
|
||||
// paid plans specific routes
|
||||
router.HandleFunc("/api/v1/complete/saml", am.OpenAccess(ah.receiveSAML)).Methods(http.MethodPost)
|
||||
@@ -114,9 +104,6 @@ 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)
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
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")
|
||||
}
|
||||
@@ -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/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||
"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())
|
||||
featureSet, err := ah.Signoz.Licensing.GetFeatureFlags(r.Context(), orgID)
|
||||
if err != nil {
|
||||
ah.HandleError(w, err, http.StatusInternalServerError)
|
||||
return
|
||||
@@ -61,7 +61,15 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if ah.opts.PreferSpanMetrics {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == featuretypes.UseSpanMetrics {
|
||||
if feature.Name == licensetypes.UseSpanMetrics {
|
||||
featureSet[idx].Active = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if constants.IsDotMetricsEnabled {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == licensetypes.DotMetricsEnabled {
|
||||
featureSet[idx].Active = true
|
||||
}
|
||||
}
|
||||
@@ -72,7 +80,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) ([]*featuretypes.GettableFeature, error) {
|
||||
func fetchZeusFeatures(url, licenseKey string) ([]*licensetypes.Feature, error) {
|
||||
// Check if the URL is empty
|
||||
if url == "" {
|
||||
return nil, fmt.Errorf("url is empty")
|
||||
@@ -131,28 +139,28 @@ func fetchZeusFeatures(url, licenseKey string) ([]*featuretypes.GettableFeature,
|
||||
}
|
||||
|
||||
type ZeusFeaturesResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data []*featuretypes.GettableFeature `json:"data"`
|
||||
Status string `json:"status"`
|
||||
Data []*licensetypes.Feature `json:"data"`
|
||||
}
|
||||
|
||||
// MergeFeatureSets merges two FeatureSet arrays with precedence to zeusFeatures.
|
||||
func MergeFeatureSets(zeusFeatures, internalFeatures []*featuretypes.GettableFeature) []*featuretypes.GettableFeature {
|
||||
func MergeFeatureSets(zeusFeatures, internalFeatures []*licensetypes.Feature) []*licensetypes.Feature {
|
||||
// Create a map to store the merged features
|
||||
featureMap := make(map[string]*featuretypes.GettableFeature)
|
||||
featureMap := make(map[string]*licensetypes.Feature)
|
||||
|
||||
// Add all features from the otherFeatures set to the map
|
||||
for _, feature := range internalFeatures {
|
||||
featureMap[feature.Name] = feature
|
||||
featureMap[feature.Name.StringValue()] = 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] = feature
|
||||
featureMap[feature.Name.StringValue()] = feature
|
||||
}
|
||||
|
||||
// Convert the map back to a FeatureSet slice
|
||||
var mergedFeatures []*featuretypes.GettableFeature
|
||||
var mergedFeatures []*licensetypes.Feature
|
||||
for _, feature := range featureMap {
|
||||
mergedFeatures = append(mergedFeatures, feature)
|
||||
}
|
||||
|
||||
@@ -3,78 +3,79 @@ package api
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMergeFeatureSets(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
zeusFeatures []*featuretypes.GettableFeature
|
||||
internalFeatures []*featuretypes.GettableFeature
|
||||
expected []*featuretypes.GettableFeature
|
||||
zeusFeatures []*licensetypes.Feature
|
||||
internalFeatures []*licensetypes.Feature
|
||||
expected []*licensetypes.Feature
|
||||
}{
|
||||
{
|
||||
name: "empty zeusFeatures and internalFeatures",
|
||||
zeusFeatures: []*featuretypes.GettableFeature{},
|
||||
internalFeatures: []*featuretypes.GettableFeature{},
|
||||
expected: []*featuretypes.GettableFeature{},
|
||||
zeusFeatures: []*licensetypes.Feature{},
|
||||
internalFeatures: []*licensetypes.Feature{},
|
||||
expected: []*licensetypes.Feature{},
|
||||
},
|
||||
{
|
||||
name: "non-empty zeusFeatures and empty internalFeatures",
|
||||
zeusFeatures: []*featuretypes.GettableFeature{
|
||||
{Name: "Feature1", Active: true},
|
||||
{Name: "Feature2", Active: false},
|
||||
zeusFeatures: []*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},
|
||||
internalFeatures: []*licensetypes.Feature{},
|
||||
expected: []*licensetypes.Feature{
|
||||
{Name: valuer.NewString("Feature1"), Active: true},
|
||||
{Name: valuer.NewString("Feature2"), Active: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty zeusFeatures and non-empty internalFeatures",
|
||||
zeusFeatures: []*featuretypes.GettableFeature{},
|
||||
internalFeatures: []*featuretypes.GettableFeature{
|
||||
{Name: "Feature1", Active: true},
|
||||
{Name: "Feature2", Active: false},
|
||||
zeusFeatures: []*licensetypes.Feature{},
|
||||
internalFeatures: []*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},
|
||||
expected: []*licensetypes.Feature{
|
||||
{Name: valuer.NewString("Feature1"), Active: true},
|
||||
{Name: valuer.NewString("Feature2"), Active: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-empty zeusFeatures and non-empty internalFeatures with no conflicts",
|
||||
zeusFeatures: []*featuretypes.GettableFeature{
|
||||
{Name: "Feature1", Active: true},
|
||||
{Name: "Feature3", Active: false},
|
||||
zeusFeatures: []*licensetypes.Feature{
|
||||
{Name: valuer.NewString("Feature1"), Active: true},
|
||||
{Name: valuer.NewString("Feature3"), Active: false},
|
||||
},
|
||||
internalFeatures: []*featuretypes.GettableFeature{
|
||||
{Name: "Feature2", Active: true},
|
||||
{Name: "Feature4", Active: false},
|
||||
internalFeatures: []*licensetypes.Feature{
|
||||
{Name: valuer.NewString("Feature2"), Active: true},
|
||||
{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},
|
||||
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},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "non-empty zeusFeatures and non-empty internalFeatures with conflicts",
|
||||
zeusFeatures: []*featuretypes.GettableFeature{
|
||||
{Name: "Feature1", Active: true},
|
||||
{Name: "Feature2", Active: false},
|
||||
zeusFeatures: []*licensetypes.Feature{
|
||||
{Name: valuer.NewString("Feature1"), Active: true},
|
||||
{Name: valuer.NewString("Feature2"), Active: false},
|
||||
},
|
||||
internalFeatures: []*featuretypes.GettableFeature{
|
||||
{Name: "Feature1", Active: false},
|
||||
{Name: "Feature3", Active: true},
|
||||
internalFeatures: []*licensetypes.Feature{
|
||||
{Name: valuer.NewString("Feature1"), Active: false},
|
||||
{Name: valuer.NewString("Feature3"), Active: true},
|
||||
},
|
||||
expected: []*featuretypes.GettableFeature{
|
||||
{Name: "Feature1", Active: true},
|
||||
{Name: "Feature2", 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},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -294,6 +294,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
||||
apiHandler.RegisterQueryRangeV3Routes(r, am)
|
||||
apiHandler.RegisterInfraMetricsRoutes(r, am)
|
||||
apiHandler.RegisterQueryRangeV4Routes(r, am)
|
||||
apiHandler.RegisterQueryRangeV5Routes(r, am)
|
||||
apiHandler.RegisterWebSocketPaths(r, am)
|
||||
apiHandler.RegisterMessagingQueuesRoutes(r, am)
|
||||
apiHandler.RegisterThirdPartyApiRoutes(r, am)
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"uuid": "^8.3.2",
|
||||
"web-vitals": "^0.2.4",
|
||||
"webpack": "5.94.0",
|
||||
"webpack-dev-server": "^4.15.2",
|
||||
"webpack-dev-server": "^5.2.1",
|
||||
"webpack-retry-chunk-load-plugin": "3.1.1",
|
||||
"xstate": "^4.31.0"
|
||||
},
|
||||
@@ -197,7 +197,6 @@
|
||||
"babel-plugin-styled-components": "^1.12.0",
|
||||
"compression-webpack-plugin": "9.0.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"critters-webpack-plugin": "^3.0.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^16.1.4",
|
||||
@@ -235,7 +234,7 @@
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript-plugin-css-modules": "5.0.1",
|
||||
"webpack-bundle-analyzer": "^4.5.0",
|
||||
"webpack-cli": "^4.9.2"
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.(js|jsx|ts|tsx)": [
|
||||
|
||||
@@ -78,7 +78,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
const checkFirstTimeUser = useCallback((): boolean => {
|
||||
const users = usersData?.data || [];
|
||||
|
||||
const remainingUsers = users.filter(
|
||||
const remainingUsers = (Array.isArray(users) ? users : []).filter(
|
||||
(user) => user.email !== 'admin@signoz.cloud',
|
||||
);
|
||||
|
||||
@@ -95,7 +95,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
usersData.data
|
||||
) {
|
||||
const isOnboardingComplete = orgPreferences?.find(
|
||||
(preference: Record<string, any>) => preference.key === 'ORG_ONBOARDING',
|
||||
(preference: Record<string, any>) => preference.name === 'org_onboarding',
|
||||
)?.value;
|
||||
|
||||
const isFirstUser = checkFirstTimeUser();
|
||||
|
||||
@@ -28,6 +28,7 @@ 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';
|
||||
|
||||
@@ -171,11 +172,13 @@ 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(),
|
||||
);
|
||||
(activeLicenseFetchError &&
|
||||
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
|
||||
activeLicenseFetchError?.getHttpStatusCode(),
|
||||
)) ||
|
||||
(activeLicense?.status && activeLicense.status === LicenseStatus.INVALID);
|
||||
const isIdentifiedUser = getLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER);
|
||||
|
||||
if (isLoggedInState && user && user.id && user.email && !isIdentifiedUser) {
|
||||
@@ -190,6 +193,10 @@ function App(): JSX.Element {
|
||||
updatedRoutes = updatedRoutes.filter(
|
||||
(route) => route?.path !== ROUTES.BILLING,
|
||||
);
|
||||
|
||||
if (isEnterpriseSelfHostedUser) {
|
||||
updatedRoutes.push(LIST_LICENSES);
|
||||
}
|
||||
}
|
||||
// always add support route for cloud users
|
||||
updatedRoutes = [...updatedRoutes, SUPPORT_ROUTE];
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
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;
|
||||
@@ -1,9 +0,0 @@
|
||||
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;
|
||||
@@ -1,11 +0,0 @@
|
||||
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;
|
||||
@@ -1,8 +0,0 @@
|
||||
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);
|
||||
@@ -1,11 +0,0 @@
|
||||
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;
|
||||
@@ -1,11 +0,0 @@
|
||||
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;
|
||||
@@ -1,20 +0,0 @@
|
||||
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;
|
||||
@@ -1,10 +0,0 @@
|
||||
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;
|
||||
@@ -1,18 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { GetAllOrgPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
|
||||
|
||||
const getAllOrgPreferences = async (): Promise<
|
||||
SuccessResponse<GetAllOrgPreferencesResponseProps> | ErrorResponse
|
||||
> => {
|
||||
const response = await axios.get(`/org/preferences`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default getAllOrgPreferences;
|
||||
@@ -1,18 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { GetAllUserPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
|
||||
|
||||
const getAllUserPreferences = async (): Promise<
|
||||
SuccessResponse<GetAllUserPreferencesResponseProps> | ErrorResponse
|
||||
> => {
|
||||
const response = await axios.get(`/user/preferences`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default getAllUserPreferences;
|
||||
@@ -1,20 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { GetOrgPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
|
||||
|
||||
const getOrgPreference = async ({
|
||||
preferenceID,
|
||||
}: {
|
||||
preferenceID: string;
|
||||
}): Promise<SuccessResponse<GetOrgPreferenceResponseProps> | ErrorResponse> => {
|
||||
const response = await axios.get(`/org/preferences/${preferenceID}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default getOrgPreference;
|
||||
@@ -1,22 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { GetUserPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
|
||||
|
||||
const getUserPreference = async ({
|
||||
preferenceID,
|
||||
}: {
|
||||
preferenceID: string;
|
||||
}): Promise<
|
||||
SuccessResponse<GetUserPreferenceResponseProps> | ErrorResponse
|
||||
> => {
|
||||
const response = await axios.get(`/user/preferences/${preferenceID}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default getUserPreference;
|
||||
@@ -1,28 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
UpdateOrgPreferenceProps,
|
||||
UpdateOrgPreferenceResponseProps,
|
||||
} from 'types/api/preferences/userOrgPreferences';
|
||||
|
||||
const updateOrgPreference = async (
|
||||
preferencePayload: UpdateOrgPreferenceProps,
|
||||
): Promise<
|
||||
SuccessResponse<UpdateOrgPreferenceResponseProps> | ErrorResponse
|
||||
> => {
|
||||
const response = await axios.put(
|
||||
`/org/preferences/${preferencePayload.preferenceID}`,
|
||||
{
|
||||
preference_value: preferencePayload.value,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default updateOrgPreference;
|
||||
@@ -1,28 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
UpdateUserPreferenceProps,
|
||||
UpdateUserPreferenceResponseProps,
|
||||
} from 'types/api/preferences/userOrgPreferences';
|
||||
|
||||
const updateUserPreference = async (
|
||||
preferencePayload: UpdateUserPreferenceProps,
|
||||
): Promise<
|
||||
SuccessResponse<UpdateUserPreferenceResponseProps> | ErrorResponse
|
||||
> => {
|
||||
const response = await axios.put(
|
||||
`/user/preferences/${preferencePayload.preferenceID}`,
|
||||
{
|
||||
preference_value: preferencePayload.value,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default updateUserPreference;
|
||||
@@ -196,8 +196,6 @@ export interface FunnelOverviewResponse {
|
||||
avg_rate: number;
|
||||
conversion_rate: number | null;
|
||||
errors: number;
|
||||
// TODO(shaheer): remove p99_latency once we have support for latency
|
||||
p99_latency: number;
|
||||
latency: number;
|
||||
};
|
||||
}>;
|
||||
|
||||
23
frontend/src/api/v1/dashboards/create.ts
Normal file
23
frontend/src/api/v1/dashboards/create.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, 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;
|
||||
19
frontend/src/api/v1/dashboards/getAll.ts
Normal file
19
frontend/src/api/v1/dashboards/getAll.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
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;
|
||||
21
frontend/src/api/v1/dashboards/id/delete.ts
Normal file
21
frontend/src/api/v1/dashboards/id/delete.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, 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;
|
||||
20
frontend/src/api/v1/dashboards/id/get.ts
Normal file
20
frontend/src/api/v1/dashboards/id/get.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
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;
|
||||
23
frontend/src/api/v1/dashboards/id/lock.ts
Normal file
23
frontend/src/api/v1/dashboards/id/lock.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, 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;
|
||||
23
frontend/src/api/v1/dashboards/id/update.ts
Normal file
23
frontend/src/api/v1/dashboards/id/update.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { 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;
|
||||
23
frontend/src/api/v1/features/list.ts
Normal file
23
frontend/src/api/v1/features/list.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import {
|
||||
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;
|
||||
23
frontend/src/api/v1/org/preferences/list.ts
Normal file
23
frontend/src/api/v1/org/preferences/list.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/preferences/list';
|
||||
import { OrgPreference } from 'types/api/preferences/preference';
|
||||
|
||||
const listPreference = async (): Promise<
|
||||
SuccessResponseV2<OrgPreference[]>
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(`/org/preferences`);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default listPreference;
|
||||
25
frontend/src/api/v1/org/preferences/name/get.ts
Normal file
25
frontend/src/api/v1/org/preferences/name/get.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/preferences/get';
|
||||
import { OrgPreference } from 'types/api/preferences/preference';
|
||||
|
||||
const getPreference = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<OrgPreference>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(
|
||||
`/org/preferences/${props.name}`,
|
||||
);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default getPreference;
|
||||
22
frontend/src/api/v1/org/preferences/name/update.ts
Normal file
22
frontend/src/api/v1/org/preferences/name/update.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { Props } from 'types/api/preferences/update';
|
||||
|
||||
const update = async (props: Props): Promise<SuccessResponseV2<null>> => {
|
||||
try {
|
||||
const response = await axios.put(`/org/preferences/${props.name}`, {
|
||||
value: props.value,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: null,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default update;
|
||||
@@ -1,24 +0,0 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/user/getUserPreference';
|
||||
|
||||
const getPreference = async (): Promise<
|
||||
SuccessResponse<PayloadProps> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.get(`/user/preferences`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getPreference;
|
||||
21
frontend/src/api/v1/user/preferences/list.ts
Normal file
21
frontend/src/api/v1/user/preferences/list.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/preferences/list';
|
||||
import { UserPreference } from 'types/api/preferences/preference';
|
||||
|
||||
const list = async (): Promise<SuccessResponseV2<UserPreference[]>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(`/user/preferences`);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default list;
|
||||
25
frontend/src/api/v1/user/preferences/name/get.ts
Normal file
25
frontend/src/api/v1/user/preferences/name/get.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/preferences/get';
|
||||
import { UserPreference } from 'types/api/preferences/preference';
|
||||
|
||||
const get = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponseV2<UserPreference>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(
|
||||
`/user/preferences/${props.name}`,
|
||||
);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default get;
|
||||
22
frontend/src/api/v1/user/preferences/name/update.ts
Normal file
22
frontend/src/api/v1/user/preferences/name/update.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { Props } from 'types/api/preferences/update';
|
||||
|
||||
const update = async (props: Props): Promise<SuccessResponseV2<null>> => {
|
||||
try {
|
||||
const response = await axios.put(`/user/preferences/${props.name}`, {
|
||||
value: props.value,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: null,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default update;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -52,11 +53,32 @@ jest.mock('hooks/saveViews/useDeleteView', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock usePreferenceSync
|
||||
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
|
||||
usePreferenceSync: (): any => ({
|
||||
preferences: {
|
||||
columns: [],
|
||||
formatting: {
|
||||
maxLines: 2,
|
||||
format: 'table',
|
||||
fontSize: 'small',
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
updateColumns: jest.fn(),
|
||||
updateFormatting: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('ExplorerCard', () => {
|
||||
it('renders a card with a title and a description', () => {
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
<PreferenceContextProvider>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
</PreferenceContextProvider>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
expect(screen.queryByText('Query Builder')).not.toBeInTheDocument();
|
||||
@@ -65,7 +87,9 @@ describe('ExplorerCard', () => {
|
||||
it('renders a save view button', () => {
|
||||
render(
|
||||
<MockQueryClientProvider>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
<PreferenceContextProvider>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
</PreferenceContextProvider>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
expect(screen.queryByText('Save view')).not.toBeInTheDocument();
|
||||
|
||||
@@ -6,6 +6,7 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
DeleteViewHandlerProps,
|
||||
@@ -106,7 +107,11 @@ export const isQueryUpdatedInView = ({
|
||||
!isEqual(
|
||||
options?.selectColumns,
|
||||
extraData && JSON.parse(extraData)?.selectColumns,
|
||||
)
|
||||
) ||
|
||||
(stagedQuery?.builder?.queryData?.[0]?.dataSource === DataSource.LOGS &&
|
||||
(!isEqual(options?.format, extraData && JSON.parse(extraData)?.format) ||
|
||||
!isEqual(options?.maxLines, extraData && JSON.parse(extraData)?.maxLines) ||
|
||||
!isEqual(options?.fontSize, extraData && JSON.parse(extraData)?.fontSize)))
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ const formatMap = {
|
||||
'MM/dd HH:mm': DATE_TIME_FORMATS.SLASH_SHORT,
|
||||
'MM/DD': DATE_TIME_FORMATS.DATE_SHORT,
|
||||
'YY-MM': DATE_TIME_FORMATS.YEAR_MONTH,
|
||||
'MMM d, yyyy, h:mm:ss aaaa': DATE_TIME_FORMATS.DASH_DATETIME,
|
||||
YY: DATE_TIME_FORMATS.YEAR_SHORT,
|
||||
};
|
||||
|
||||
|
||||
@@ -26,6 +26,18 @@ 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 },
|
||||
@@ -78,6 +90,19 @@ 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,
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// keep this consistent with backend constants.go
|
||||
// keep this consistent with backend plan.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',
|
||||
THIRD_PARTY_API = 'THIRD_PARTY_API',
|
||||
TRACE_FUNNELS = 'TRACE_FUNNELS',
|
||||
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',
|
||||
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
|
||||
}
|
||||
|
||||
@@ -425,3 +425,79 @@ export const metricsEmptyTimeAggregateOperatorOptions: SelectOption<
|
||||
string,
|
||||
string
|
||||
>[] = [];
|
||||
|
||||
export const metricsUnknownTimeAggregateOperatorOptions: SelectOption<
|
||||
string,
|
||||
string
|
||||
>[] = [
|
||||
{
|
||||
value: MetricAggregateOperator.MAX,
|
||||
label: 'Max',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.MIN,
|
||||
label: 'Min',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.SUM,
|
||||
label: 'Sum',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.AVG,
|
||||
label: 'Avg',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.COUNT,
|
||||
label: 'Count',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.RATE,
|
||||
label: 'Rate',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.INCREASE,
|
||||
label: 'Increase',
|
||||
},
|
||||
];
|
||||
|
||||
export const metricsUnknownSpaceAggregateOperatorOptions: SelectOption<
|
||||
string,
|
||||
string
|
||||
>[] = [
|
||||
{
|
||||
value: MetricAggregateOperator.SUM,
|
||||
label: 'Sum',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.AVG,
|
||||
label: 'Avg',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.MIN,
|
||||
label: 'Min',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.MAX,
|
||||
label: 'Max',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.P50,
|
||||
label: 'P50',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.P75,
|
||||
label: 'P75',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.P90,
|
||||
label: 'P90',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.P95,
|
||||
label: 'P95',
|
||||
},
|
||||
{
|
||||
value: MetricAggregateOperator.P99,
|
||||
label: 'P99',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -7,7 +7,7 @@ import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
@@ -17,7 +17,6 @@ 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);
|
||||
|
||||
@@ -56,14 +55,19 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
|
||||
<Button onClick={(): void => onClickEditHandler(id)} type="link">
|
||||
{t('column_channel_edit')}
|
||||
</Button>
|
||||
<Delete id={id} setChannels={setChannels} notifications={notifications} />
|
||||
<Delete id={id} notifications={notifications} />
|
||||
</>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ResizeTable columns={columns} dataSource={channels} rowKey="id" bordered />
|
||||
<ResizeTable
|
||||
columns={columns}
|
||||
dataSource={allChannels}
|
||||
rowKey="id"
|
||||
bordered
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Button } from 'antd';
|
||||
import { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import deleteChannel from 'api/channels/delete';
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
function Delete({ notifications, setChannels, id }: DeleteProps): JSX.Element {
|
||||
function Delete({ notifications, id }: DeleteProps): JSX.Element {
|
||||
const { t } = useTranslation(['channels']);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const onClickHandler = async (): Promise<void> => {
|
||||
try {
|
||||
@@ -21,7 +22,8 @@ function Delete({ notifications, setChannels, id }: DeleteProps): JSX.Element {
|
||||
message: 'Success',
|
||||
description: t('channel_delete_success'),
|
||||
});
|
||||
setChannels((preChannels) => preChannels.filter((e) => e.id !== id));
|
||||
// Invalidate and refetch
|
||||
queryClient.invalidateQueries(['getChannels']);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
@@ -46,7 +48,6 @@ function Delete({ notifications, setChannels, id }: DeleteProps): JSX.Element {
|
||||
|
||||
interface DeleteProps {
|
||||
notifications: NotificationInstance;
|
||||
setChannels: Dispatch<SetStateAction<Channels[]>>;
|
||||
id: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -316,7 +316,6 @@ 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(
|
||||
|
||||
@@ -24,6 +24,10 @@ import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import ExportPanelContainer from 'container/ExportPanel/ExportPanelContainer';
|
||||
import {
|
||||
MetricsExplorerEventKeys,
|
||||
MetricsExplorerEvents,
|
||||
} from 'container/MetricsExplorer/events';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import {
|
||||
defaultLogsSelectedColumns,
|
||||
@@ -50,6 +54,7 @@ import {
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { FormattingOptions } from 'providers/preferences/types';
|
||||
import {
|
||||
CSSProperties,
|
||||
Dispatch,
|
||||
@@ -140,7 +145,9 @@ function ExplorerOptions({
|
||||
panelType,
|
||||
});
|
||||
} else if (isMetricsExplorer) {
|
||||
logEvent('Metrics Explorer: Save view clicked', {
|
||||
logEvent(MetricsExplorerEvents.SaveViewClicked, {
|
||||
[MetricsExplorerEventKeys.Tab]: 'explorer',
|
||||
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
|
||||
panelType,
|
||||
});
|
||||
}
|
||||
@@ -184,8 +191,10 @@ function ExplorerOptions({
|
||||
panelType,
|
||||
});
|
||||
} else if (isMetricsExplorer) {
|
||||
logEvent('Metrics Explorer: Create alert', {
|
||||
logEvent(MetricsExplorerEvents.AddToAlertClicked, {
|
||||
panelType,
|
||||
[MetricsExplorerEventKeys.Tab]: 'explorer',
|
||||
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -218,11 +227,14 @@ function ExplorerOptions({
|
||||
panelType,
|
||||
});
|
||||
} else if (isMetricsExplorer) {
|
||||
logEvent('Metrics Explorer: Add to dashboard clicked', {
|
||||
logEvent(MetricsExplorerEvents.AddToDashboardClicked, {
|
||||
panelType,
|
||||
[MetricsExplorerEventKeys.Tab]: 'explorer',
|
||||
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
|
||||
});
|
||||
}
|
||||
setIsExport(true);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLogsExplorer, isMetricsExplorer, panelType, setIsExport, sourcepage]);
|
||||
|
||||
const {
|
||||
@@ -259,17 +271,26 @@ function ExplorerOptions({
|
||||
const getUpdatedExtraData = (
|
||||
extraData: string | undefined,
|
||||
newSelectedColumns: BaseAutocompleteData[],
|
||||
formattingOptions?: FormattingOptions,
|
||||
): string => {
|
||||
let updatedExtraData;
|
||||
|
||||
if (extraData) {
|
||||
const parsedExtraData = JSON.parse(extraData);
|
||||
parsedExtraData.selectColumns = newSelectedColumns;
|
||||
if (formattingOptions) {
|
||||
parsedExtraData.format = formattingOptions.format;
|
||||
parsedExtraData.maxLines = formattingOptions.maxLines;
|
||||
parsedExtraData.fontSize = formattingOptions.fontSize;
|
||||
}
|
||||
updatedExtraData = JSON.stringify(parsedExtraData);
|
||||
} else {
|
||||
updatedExtraData = JSON.stringify({
|
||||
color: Color.BG_SIENNA_500,
|
||||
selectColumns: newSelectedColumns,
|
||||
format: formattingOptions?.format,
|
||||
maxLines: formattingOptions?.maxLines,
|
||||
fontSize: formattingOptions?.fontSize,
|
||||
});
|
||||
}
|
||||
return updatedExtraData;
|
||||
@@ -278,6 +299,14 @@ function ExplorerOptions({
|
||||
const updatedExtraData = getUpdatedExtraData(
|
||||
extraData,
|
||||
options?.selectColumns,
|
||||
// pass this only for logs
|
||||
sourcepage === DataSource.LOGS
|
||||
? {
|
||||
format: options?.format,
|
||||
maxLines: options?.maxLines,
|
||||
fontSize: options?.fontSize,
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -506,6 +535,14 @@ function ExplorerOptions({
|
||||
color,
|
||||
selectColumns: options.selectColumns,
|
||||
version: 1,
|
||||
...// pass this only for logs
|
||||
(sourcepage === DataSource.LOGS
|
||||
? {
|
||||
format: options?.format,
|
||||
maxLines: options?.maxLines,
|
||||
fontSize: options?.fontSize,
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
notifications,
|
||||
panelType: panelType || PANEL_TYPES.LIST,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Button, Typography } from 'antd';
|
||||
import createDashboard from 'api/dashboard/create';
|
||||
import createDashboard from 'api/v1/dashboards/create';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
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 {
|
||||
@@ -33,26 +34,28 @@ function ExportPanelContainer({
|
||||
refetch,
|
||||
} = useGetAllDashboard();
|
||||
|
||||
const handleError = useAxiosError();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const {
|
||||
mutate: createNewDashboard,
|
||||
isLoading: createDashboardLoading,
|
||||
} = useMutation(createDashboard, {
|
||||
onSuccess: (data) => {
|
||||
if (data.payload) {
|
||||
onExport(data?.payload, true);
|
||||
if (data.data) {
|
||||
onExport(data?.data, true);
|
||||
}
|
||||
refetch();
|
||||
},
|
||||
onError: handleError,
|
||||
onError: (error) => {
|
||||
showErrorModal(error as APIError);
|
||||
},
|
||||
});
|
||||
|
||||
const options = useMemo(() => getSelectOptions(data || []), [data]);
|
||||
const options = useMemo(() => getSelectOptions(data?.data || []), [data]);
|
||||
|
||||
const handleExportClick = useCallback((): void => {
|
||||
const currentSelectedDashboard = data?.find(
|
||||
({ uuid }) => uuid === selectedDashboardId,
|
||||
const currentSelectedDashboard = data?.data?.find(
|
||||
({ id }) => id === selectedDashboardId,
|
||||
);
|
||||
|
||||
onExport(currentSelectedDashboard || null, false);
|
||||
@@ -66,14 +69,18 @@ function ExportPanelContainer({
|
||||
);
|
||||
|
||||
const handleNewDashboard = useCallback(async () => {
|
||||
createNewDashboard({
|
||||
title: t('new_dashboard_title', {
|
||||
ns: 'dashboard',
|
||||
}),
|
||||
uploadedGrafana: false,
|
||||
version: ENTITY_VERSION_V4,
|
||||
});
|
||||
}, [t, createNewDashboard]);
|
||||
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]);
|
||||
|
||||
const isDashboardLoading = isAllDashboardsLoading || createDashboardLoading;
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { SelectProps } from 'antd';
|
||||
import { PayloadProps as AllDashboardsData } from 'types/api/dashboard/getAll';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
export const getSelectOptions = (
|
||||
data: AllDashboardsData,
|
||||
): SelectProps['options'] =>
|
||||
data.map(({ uuid, data }) => ({
|
||||
export const getSelectOptions = (data: Dashboard[]): SelectProps['options'] =>
|
||||
data.map(({ id, data }) => ({
|
||||
label: data.title,
|
||||
value: uuid,
|
||||
value: id,
|
||||
}));
|
||||
|
||||
export const filterOptions: SelectProps['filterOption'] = (
|
||||
|
||||
@@ -36,7 +36,6 @@ 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);
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function DashboardEmptyState(): JSX.Element {
|
||||
setSelectedRowWidgetId(null);
|
||||
handleToggleDashboardSlider(true);
|
||||
logEvent('Dashboard Detail: Add new panel clicked', {
|
||||
dashboardId: selectedDashboard?.uuid,
|
||||
dashboardId: selectedDashboard?.id,
|
||||
dashboardName: selectedDashboard?.data.title,
|
||||
numberOfPanels: selectedDashboard?.data.widgets?.length,
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ 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';
|
||||
@@ -189,24 +190,26 @@ describe('WidgetGraphComponent', () => {
|
||||
it('should show correct menu items when hovering over more options while loading', async () => {
|
||||
const { getByTestId, findByRole, getByText, container } = render(
|
||||
<MockQueryClientProvider>
|
||||
<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>
|
||||
</ErrorModalProvider>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ 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';
|
||||
@@ -31,7 +30,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { Props } from 'types/api/dashboard/update';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@@ -119,29 +118,23 @@ function WidgetGraphComponent({
|
||||
const updatedLayout =
|
||||
selectedDashboard.data.layout?.filter((e) => e.i !== widget.id) || [];
|
||||
|
||||
const updatedSelectedDashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
const updatedSelectedDashboard: Props = {
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
widgets: updatedWidgets,
|
||||
layout: updatedLayout,
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
id: selectedDashboard.id,
|
||||
};
|
||||
|
||||
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
|
||||
if (setSelectedDashboard && updatedDashboard.payload) {
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
|
||||
if (setSelectedDashboard && updatedDashboard.data) {
|
||||
setSelectedDashboard(updatedDashboard.data);
|
||||
}
|
||||
setDeleteModal(false);
|
||||
},
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: SOMETHING_WENT_WRONG,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -166,7 +159,8 @@ function WidgetGraphComponent({
|
||||
|
||||
updateDashboardMutation.mutateAsync(
|
||||
{
|
||||
...selectedDashboard,
|
||||
id: selectedDashboard.id,
|
||||
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
layout,
|
||||
@@ -183,9 +177,9 @@ function WidgetGraphComponent({
|
||||
},
|
||||
{
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
|
||||
if (setSelectedDashboard && updatedDashboard.payload) {
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
|
||||
if (setSelectedDashboard && updatedDashboard.data) {
|
||||
setSelectedDashboard(updatedDashboard.data);
|
||||
}
|
||||
notifications.success({
|
||||
message: 'Panel cloned successfully, redirecting to new copy.',
|
||||
@@ -252,7 +246,11 @@ function WidgetGraphComponent({
|
||||
|
||||
const graphClick = useGraphClickToShowButton({
|
||||
graphRef: currentGraphRef?.current ? currentGraphRef : graphRef,
|
||||
isButtonEnabled: (widget?.query?.builder?.queryData ?? []).some(
|
||||
isButtonEnabled: (widget?.query?.builder?.queryData &&
|
||||
Array.isArray(widget.query.builder.queryData)
|
||||
? widget.query.builder.queryData
|
||||
: []
|
||||
).some(
|
||||
(q) =>
|
||||
q.dataSource === DataSource.TRACES || q.dataSource === DataSource.LOGS,
|
||||
),
|
||||
|
||||
@@ -160,8 +160,6 @@ 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([
|
||||
|
||||
@@ -31,7 +31,9 @@ export const getLocalStorageGraphVisibilityState = ({
|
||||
name: string;
|
||||
}): GraphVisibilityLegendEntryProps => {
|
||||
const visibilityStateAndLegendEntry: GraphVisibilityLegendEntryProps = {
|
||||
graphVisibilityStates: Array(apiResponse.length + 1).fill(true),
|
||||
graphVisibilityStates: Array(
|
||||
(Array.isArray(apiResponse) ? apiResponse.length : 0) + 1,
|
||||
).fill(true),
|
||||
legendEntry: [
|
||||
{
|
||||
label: 'Timestamp',
|
||||
|
||||
@@ -6,7 +6,6 @@ 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';
|
||||
@@ -14,7 +13,6 @@ 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';
|
||||
@@ -36,7 +34,8 @@ import { ItemCallback, Layout } from 'react-grid-layout';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Props } from 'types/api/dashboard/update';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { ComponentTypes } from 'utils/permission';
|
||||
|
||||
@@ -107,7 +106,6 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
|
||||
const updateDashboardMutation = useUpdateDashboard();
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
let permissions: ComponentTypes[] = ['save_layout', 'add_panel'];
|
||||
@@ -158,20 +156,20 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current && !isUndefined(data)) {
|
||||
logEvent('Dashboard Detail: Opened', {
|
||||
dashboardId: data.uuid,
|
||||
dashboardId: selectedDashboard?.id,
|
||||
dashboardName: data.title,
|
||||
numberOfPanels: data.widgets?.length,
|
||||
numberOfVariables: Object.keys(data?.variables || {}).length || 0,
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
}, [data]);
|
||||
}, [data, selectedDashboard?.id]);
|
||||
|
||||
const onSaveHandler = (): void => {
|
||||
if (!selectedDashboard) return;
|
||||
|
||||
const updatedDashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
const updatedDashboard: Props = {
|
||||
id: selectedDashboard.id,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
panelMap: { ...currentPanelMap },
|
||||
@@ -186,24 +184,18 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
return widget;
|
||||
}),
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
|
||||
updateDashboardMutation.mutate(updatedDashboard, {
|
||||
onSuccess: (updatedDashboard) => {
|
||||
setSelectedRowWidgetId(null);
|
||||
if (updatedDashboard.payload) {
|
||||
if (updatedDashboard.payload.data.layout)
|
||||
setLayouts(sortLayout(updatedDashboard.payload.data.layout));
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
||||
if (updatedDashboard.data) {
|
||||
if (updatedDashboard.data.data.layout)
|
||||
setLayouts(sortLayout(updatedDashboard.data.data.layout));
|
||||
setSelectedDashboard(updatedDashboard.data);
|
||||
setPanelMap(updatedDashboard.data?.data?.panelMap || {});
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: SOMETHING_WENT_WRONG,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -286,33 +278,25 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
|
||||
updatedWidgets?.push(currentWidget);
|
||||
|
||||
const updatedSelectedDashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
const updatedSelectedDashboard: Props = {
|
||||
id: selectedDashboard.id,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
widgets: updatedWidgets,
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
|
||||
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
|
||||
if (setSelectedDashboard && updatedDashboard.payload) {
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
|
||||
if (setSelectedDashboard && updatedDashboard.data) {
|
||||
setSelectedDashboard(updatedDashboard.data);
|
||||
}
|
||||
if (setPanelMap)
|
||||
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
||||
if (setPanelMap) setPanelMap(updatedDashboard.data?.data?.panelMap || {});
|
||||
form.setFieldValue('title', '');
|
||||
setIsSettingsModalOpen(false);
|
||||
setCurrentSelectRowId(null);
|
||||
},
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: SOMETHING_WENT_WRONG,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -415,12 +399,14 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
};
|
||||
|
||||
const handleDragStop: ItemCallback = (_, oldItem, newItem): void => {
|
||||
if (currentPanelMap[oldItem.i]) {
|
||||
if (oldItem?.i && 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]: {
|
||||
@@ -447,34 +433,26 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
const updatedPanelMap = { ...currentPanelMap };
|
||||
delete updatedPanelMap[currentSelectRowId];
|
||||
|
||||
const updatedSelectedDashboard: Dashboard = {
|
||||
...selectedDashboard,
|
||||
const updatedSelectedDashboard: Props = {
|
||||
id: selectedDashboard.id,
|
||||
data: {
|
||||
...selectedDashboard.data,
|
||||
widgets: updatedWidgets,
|
||||
layout: updatedLayout,
|
||||
panelMap: updatedPanelMap,
|
||||
},
|
||||
uuid: selectedDashboard.uuid,
|
||||
};
|
||||
|
||||
updateDashboardMutation.mutateAsync(updatedSelectedDashboard, {
|
||||
onSuccess: (updatedDashboard) => {
|
||||
if (setLayouts) setLayouts(updatedDashboard.payload?.data?.layout || []);
|
||||
if (setSelectedDashboard && updatedDashboard.payload) {
|
||||
setSelectedDashboard(updatedDashboard.payload);
|
||||
if (setLayouts) setLayouts(updatedDashboard.data?.data?.layout || []);
|
||||
if (setSelectedDashboard && updatedDashboard.data) {
|
||||
setSelectedDashboard(updatedDashboard.data);
|
||||
}
|
||||
if (setPanelMap)
|
||||
setPanelMap(updatedDashboard.payload?.data?.panelMap || {});
|
||||
if (setPanelMap) setPanelMap(updatedDashboard.data?.data?.panelMap || {});
|
||||
setIsDeleteModalOpen(false);
|
||||
setCurrentSelectRowId(null);
|
||||
},
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: SOMETHING_WENT_WRONG,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
const isDashboardEmpty = useMemo(
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function Dashboards({
|
||||
useEffect(() => {
|
||||
if (!dashboardsList) return;
|
||||
|
||||
const sortedDashboards = dashboardsList.sort((a, b) => {
|
||||
const sortedDashboards = dashboardsList.data.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.uuid}`;
|
||||
const getLink = (): string => `${ROUTES.ALL_DASHBOARD}/${dashboard.id}`;
|
||||
|
||||
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={
|
||||
dashboard.id % 2 === 0
|
||||
Math.random() % 2 === 0
|
||||
? '/Icons/eight-ball.svg'
|
||||
: '/Icons/circus-tent.svg'
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import { Alert, Button, Popover } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
|
||||
import { K8sPodsListPayload } from 'api/infraMonitoring/getK8sPodsList';
|
||||
import getAllUserPreferences from 'api/preferences/getAllUserPreference';
|
||||
import updateUserPreferenceAPI from 'api/preferences/updateUserPreference';
|
||||
import listUserPreferences from 'api/v1/user/preferences/list';
|
||||
import updateUserPreferenceAPI from 'api/v1/user/preferences/name/update';
|
||||
import Header from 'components/Header/Header';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
@@ -29,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,7 +185,7 @@ export default function Home(): JSX.Element {
|
||||
|
||||
const processUserPreferences = (userPreferences: UserPreference[]): void => {
|
||||
const checklistSkipped = userPreferences?.find(
|
||||
(preference) => preference.key === 'WELCOME_CHECKLIST_DO_LATER',
|
||||
(preference) => preference.name === 'welcome_checklist_do_later',
|
||||
)?.value;
|
||||
|
||||
const updatedChecklistItems = cloneDeep(checklistItems);
|
||||
@@ -194,7 +194,7 @@ export default function Home(): JSX.Element {
|
||||
const newItem = { ...item };
|
||||
newItem.isSkipped =
|
||||
userPreferences?.find(
|
||||
(preference) => preference.key === item.skippedPreferenceKey,
|
||||
(preference) => preference.name === item.skippedPreferenceKey,
|
||||
)?.value || false;
|
||||
return newItem;
|
||||
});
|
||||
@@ -206,13 +206,13 @@ export default function Home(): JSX.Element {
|
||||
|
||||
// Fetch User Preferences
|
||||
const { refetch: refetchUserPreferences } = useQuery({
|
||||
queryFn: () => getAllUserPreferences(),
|
||||
queryFn: () => listUserPreferences(),
|
||||
queryKey: ['getUserPreferences'],
|
||||
enabled: true,
|
||||
refetchOnWindowFocus: false,
|
||||
onSuccess: (response) => {
|
||||
if (response.payload && response.payload.data) {
|
||||
processUserPreferences(response.payload.data);
|
||||
if (response.data) {
|
||||
processUserPreferences(response.data);
|
||||
}
|
||||
|
||||
setLoadingUserPreferences(false);
|
||||
@@ -239,7 +239,7 @@ export default function Home(): JSX.Element {
|
||||
setUpdatingUserPreferences(true);
|
||||
|
||||
updateUserPreference({
|
||||
preferenceID: 'WELCOME_CHECKLIST_DO_LATER',
|
||||
name: 'welcome_checklist_do_later',
|
||||
value: true,
|
||||
});
|
||||
};
|
||||
@@ -249,7 +249,7 @@ export default function Home(): JSX.Element {
|
||||
setUpdatingUserPreferences(true);
|
||||
|
||||
updateUserPreference({
|
||||
preferenceID: item.skippedPreferenceKey,
|
||||
name: item.skippedPreferenceKey,
|
||||
value: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,15 +3,15 @@ import ROUTES from 'constants/routes';
|
||||
import { ChecklistItem } from './HomeChecklist/HomeChecklist';
|
||||
|
||||
export const checkListStepToPreferenceKeyMap = {
|
||||
WILL_DO_LATER: 'WELCOME_CHECKLIST_DO_LATER',
|
||||
SEND_LOGS: 'WELCOME_CHECKLIST_SEND_LOGS_SKIPPED',
|
||||
SEND_TRACES: 'WELCOME_CHECKLIST_SEND_TRACES_SKIPPED',
|
||||
SEND_INFRA_METRICS: 'WELCOME_CHECKLIST_SEND_INFRA_METRICS_SKIPPED',
|
||||
SETUP_DASHBOARDS: 'WELCOME_CHECKLIST_SETUP_DASHBOARDS_SKIPPED',
|
||||
SETUP_ALERTS: 'WELCOME_CHECKLIST_SETUP_ALERTS_SKIPPED',
|
||||
SETUP_SAVED_VIEWS: 'WELCOME_CHECKLIST_SETUP_SAVED_VIEW_SKIPPED',
|
||||
SETUP_WORKSPACE: 'WELCOME_CHECKLIST_SETUP_WORKSPACE_SKIPPED',
|
||||
ADD_DATA_SOURCE: 'WELCOME_CHECKLIST_ADD_DATA_SOURCE_SKIPPED',
|
||||
WILL_DO_LATER: '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 = {
|
||||
|
||||
@@ -41,7 +41,6 @@ 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,
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from 'antd';
|
||||
import { TableProps } from 'antd/lib';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import createDashboard from 'api/dashboard/create';
|
||||
import createDashboard from 'api/v1/dashboards/create';
|
||||
import { AxiosError } from 'axios';
|
||||
import cx from 'classnames';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
@@ -63,6 +63,7 @@ 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,
|
||||
@@ -83,6 +84,7 @@ import {
|
||||
WidgetRow,
|
||||
Widgets,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
|
||||
import ImportJSON from './ImportJSON';
|
||||
@@ -226,7 +228,7 @@ function DashboardsList(): JSX.Element {
|
||||
useEffect(() => {
|
||||
const filteredDashboards = filterDashboard(
|
||||
searchString,
|
||||
dashboardListResponse || [],
|
||||
dashboardListResponse?.data || [],
|
||||
);
|
||||
if (sortOrder.columnKey === 'updatedAt') {
|
||||
sortDashboardsByUpdatedAt(filteredDashboards || []);
|
||||
@@ -256,17 +258,19 @@ function DashboardsList(): JSX.Element {
|
||||
errorMessage: '',
|
||||
});
|
||||
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const data: Data[] =
|
||||
dashboards?.map((e) => ({
|
||||
createdAt: e.createdAt,
|
||||
description: e.data.description || '',
|
||||
id: e.uuid,
|
||||
id: e.id,
|
||||
lastUpdatedTime: e.updatedAt,
|
||||
name: e.data.title,
|
||||
tags: e.data.tags || [],
|
||||
key: e.uuid,
|
||||
key: e.id,
|
||||
createdBy: e.createdBy,
|
||||
isLocked: !!e.isLocked || false,
|
||||
isLocked: !!e.locked || false,
|
||||
lastUpdatedBy: e.updatedBy,
|
||||
image: e.data.image || Base64Icons[0],
|
||||
variables: e.data.variables,
|
||||
@@ -292,28 +296,20 @@ function DashboardsList(): JSX.Element {
|
||||
version: ENTITY_VERSION_V4,
|
||||
});
|
||||
|
||||
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',
|
||||
});
|
||||
}
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: response.data.id,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
setNewDashboardState({
|
||||
...newDashboardState,
|
||||
error: true,
|
||||
errorMessage: (error as AxiosError).toString() || 'Something went Wrong',
|
||||
});
|
||||
}
|
||||
}, [newDashboardState, safeNavigate, t]);
|
||||
}, [newDashboardState, safeNavigate, showErrorModal, t]);
|
||||
|
||||
const onModalHandler = (uploadedGrafana: boolean): void => {
|
||||
logEvent('Dashboard List: Import JSON clicked', {});
|
||||
@@ -327,7 +323,7 @@ function DashboardsList(): JSX.Element {
|
||||
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
|
||||
const filteredDashboards = filterDashboard(
|
||||
searchText,
|
||||
dashboardListResponse || [],
|
||||
dashboardListResponse?.data || [],
|
||||
);
|
||||
setDashboards(filteredDashboards);
|
||||
setIsFilteringDashboards(false);
|
||||
@@ -677,7 +673,7 @@ function DashboardsList(): JSX.Element {
|
||||
!isUndefined(dashboardListResponse)
|
||||
) {
|
||||
logEvent('Dashboard List: Page visited', {
|
||||
number: dashboardListResponse?.length,
|
||||
number: dashboardListResponse?.data?.length,
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
|
||||
@@ -14,19 +14,21 @@ import {
|
||||
UploadProps,
|
||||
} from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import createDashboard from 'api/dashboard/create';
|
||||
import createDashboard from 'api/v1/dashboards/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,
|
||||
@@ -74,6 +76,8 @@ function ImportJSON({
|
||||
}
|
||||
};
|
||||
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const onClickLoadJsonHandler = async (): Promise<void> => {
|
||||
try {
|
||||
setDashboardCreating(true);
|
||||
@@ -81,11 +85,6 @@ 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 {
|
||||
@@ -97,28 +96,19 @@ function ImportJSON({
|
||||
uploadedGrafana,
|
||||
});
|
||||
|
||||
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',
|
||||
}),
|
||||
});
|
||||
}
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: response.data.id,
|
||||
}),
|
||||
);
|
||||
logEvent('Dashboard List: New dashboard imported successfully', {
|
||||
dashboardId: response.data?.id,
|
||||
dashboardName: response.data?.data?.title,
|
||||
});
|
||||
|
||||
setDashboardCreating(false);
|
||||
} catch (error) {
|
||||
showErrorModal(error as APIError);
|
||||
setDashboardCreating(false);
|
||||
setIsCreateDashboardError(true);
|
||||
notifications.error({
|
||||
|
||||
@@ -6,8 +6,7 @@ import { executeSearchQueries } from '../utils';
|
||||
|
||||
describe('executeSearchQueries', () => {
|
||||
const firstDashboard: Dashboard = {
|
||||
id: 11111,
|
||||
uuid: uuid(),
|
||||
id: uuid(),
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
createdBy: '',
|
||||
@@ -18,8 +17,7 @@ describe('executeSearchQueries', () => {
|
||||
},
|
||||
};
|
||||
const secondDashboard: Dashboard = {
|
||||
id: 22222,
|
||||
uuid: uuid(),
|
||||
id: uuid(),
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
createdBy: '',
|
||||
@@ -30,8 +28,7 @@ describe('executeSearchQueries', () => {
|
||||
},
|
||||
};
|
||||
const thirdDashboard: Dashboard = {
|
||||
id: 333333,
|
||||
uuid: uuid(),
|
||||
id: uuid(),
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
createdBy: '',
|
||||
|
||||
@@ -59,7 +59,7 @@ export function DeleteButton({
|
||||
onClick: (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
deleteDashboardMutation.mutateAsync(undefined, {
|
||||
deleteDashboardMutation.mutate(undefined, {
|
||||
onSuccess: () => {
|
||||
notifications.success({
|
||||
message: t('dashboard:delete_dashboard_success', {
|
||||
|
||||
@@ -14,7 +14,7 @@ export const generateSearchData = (
|
||||
|
||||
dashboards.forEach((dashboard) => {
|
||||
dashboardSearchData.push({
|
||||
id: dashboard.uuid,
|
||||
id: dashboard.id,
|
||||
title: dashboard.data.title,
|
||||
description: dashboard.data.description,
|
||||
tags: dashboard.data.tags || [],
|
||||
|
||||
@@ -280,7 +280,7 @@ export const parseFieldValue = (value: string): string => {
|
||||
// now we do not want to render colors everywhere like in tooltip and monaco editor hence we remove such codes to make
|
||||
// the log line readable
|
||||
export const removeEscapeCharacters = (str: string): string =>
|
||||
str
|
||||
(str ?? '')
|
||||
.replace(/\\x1[bB][[0-9;]*m/g, '')
|
||||
.replace(/\\u001[bB][[0-9;]*m/g, '')
|
||||
.replace(/\\x[0-9A-Fa-f]{2}/g, '')
|
||||
@@ -292,7 +292,7 @@ export const removeEscapeCharacters = (str: string): string =>
|
||||
//
|
||||
// so we need to remove this escapes to render the color properly
|
||||
export const unescapeString = (str: string): string =>
|
||||
str
|
||||
(str ?? '')
|
||||
.replace(/\\n/g, '\n') // Replaces escaped newlines
|
||||
.replace(/\\r/g, '\r') // Replaces escaped carriage returns
|
||||
.replace(/\\t/g, '\t') // Replaces escaped tabs
|
||||
|
||||
@@ -62,6 +62,8 @@ function LogsExplorerChart({
|
||||
urlQuery.set(QueryParams.startTime, minTime.toString());
|
||||
urlQuery.set(QueryParams.endTime, maxTime.toString());
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
// Remove Hidden Filters from URL query parameters on time change
|
||||
urlQuery.delete(QueryParams.activeLogId);
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
},
|
||||
|
||||
@@ -28,16 +28,12 @@ import LogsExplorerTable from 'container/LogsExplorerTable';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||
import dayjs from 'dayjs';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import useClickOutside from 'hooks/useClickOutside';
|
||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
@@ -98,7 +94,6 @@ function LogsExplorerViews({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
chartQueryKeyRef: MutableRefObject<any>;
|
||||
}): JSX.Element {
|
||||
const { notifications } = useNotifications();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
// this is to respect the panel type present in the URL rather than defaulting it to list always.
|
||||
@@ -119,7 +114,6 @@ function LogsExplorerViews({
|
||||
|
||||
// Context
|
||||
const {
|
||||
initialDataSource,
|
||||
currentQuery,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
@@ -141,8 +135,6 @@ function LogsExplorerViews({
|
||||
const [queryId, setQueryId] = useState<string>(v4());
|
||||
const [queryStats, setQueryStats] = useState<WsDataEvent>();
|
||||
|
||||
const handleAxisError = useAxiosError();
|
||||
|
||||
const listQuery = useMemo(() => {
|
||||
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) return null;
|
||||
|
||||
@@ -151,7 +143,7 @@ function LogsExplorerViews({
|
||||
|
||||
const { options, config } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||
dataSource: initialDataSource || DataSource.LOGS,
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: listQuery?.aggregateOperator || StringOperators.NOOP,
|
||||
});
|
||||
|
||||
@@ -195,6 +187,26 @@ function LogsExplorerViews({
|
||||
},
|
||||
],
|
||||
legend: '{{severity_text}}',
|
||||
...(activeLogId && {
|
||||
filters: {
|
||||
...listQuery?.filters,
|
||||
items: [
|
||||
...(listQuery?.filters?.items || []),
|
||||
{
|
||||
id: v4(),
|
||||
key: {
|
||||
key: 'id',
|
||||
type: '',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
op: OPERATORS['<='],
|
||||
value: activeLogId,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const modifiedQuery: Query = {
|
||||
@@ -209,7 +221,7 @@ function LogsExplorerViews({
|
||||
};
|
||||
|
||||
return modifiedQuery;
|
||||
}, [stagedQuery, listQuery]);
|
||||
}, [stagedQuery, listQuery, activeLogId]);
|
||||
|
||||
const exportDefaultQuery = useMemo(
|
||||
() =>
|
||||
@@ -294,12 +306,12 @@ function LogsExplorerViews({
|
||||
});
|
||||
|
||||
// Add filter for activeLogId if present
|
||||
let updatedFilters = paginateData.filters;
|
||||
let updatedFilters = params.filters;
|
||||
if (activeLogId) {
|
||||
updatedFilters = {
|
||||
...paginateData.filters,
|
||||
...params.filters,
|
||||
items: [
|
||||
...(paginateData.filters?.items || []),
|
||||
...(params.filters?.items || []),
|
||||
{
|
||||
id: v4(),
|
||||
key: {
|
||||
@@ -396,11 +408,6 @@ function LogsExplorerViews({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data?.payload]);
|
||||
|
||||
const {
|
||||
mutate: updateDashboard,
|
||||
isLoading: isUpdateDashboardLoading,
|
||||
} = useUpdateDashboard();
|
||||
|
||||
const getUpdatedQueryForExport = useCallback((): Query => {
|
||||
const updatedQuery = cloneDeep(currentQuery);
|
||||
|
||||
@@ -424,68 +431,22 @@ function LogsExplorerViews({
|
||||
? getUpdatedQueryForExport()
|
||||
: exportDefaultQuery;
|
||||
|
||||
const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery(
|
||||
dashboard,
|
||||
query,
|
||||
widgetId,
|
||||
panelTypeParam,
|
||||
options.selectColumns,
|
||||
);
|
||||
|
||||
logEvent('Logs Explorer: Add to dashboard successful', {
|
||||
panelType,
|
||||
isNewDashboard,
|
||||
dashboardName: dashboard?.data?.title,
|
||||
});
|
||||
|
||||
updateDashboard(updatedDashboard, {
|
||||
onSuccess: (data) => {
|
||||
if (data.error) {
|
||||
const message =
|
||||
data.error === 'feature usage exceeded' ? (
|
||||
<span>
|
||||
Panel limit exceeded for {DataSource.LOGS} in community edition. Please
|
||||
checkout our paid plans{' '}
|
||||
<a
|
||||
href="https://signoz.io/pricing/?utm_source=product&utm_medium=dashboard-limit"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
data.error
|
||||
);
|
||||
notifications.error({
|
||||
message,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const dashboardEditView = generateExportToDashboardLink({
|
||||
query,
|
||||
panelType: panelTypeParam,
|
||||
dashboardId: data.payload?.uuid || '',
|
||||
widgetId,
|
||||
});
|
||||
|
||||
safeNavigate(dashboardEditView);
|
||||
},
|
||||
onError: handleAxisError,
|
||||
const dashboardEditView = generateExportToDashboardLink({
|
||||
query,
|
||||
panelType: panelTypeParam,
|
||||
dashboardId: dashboard.id,
|
||||
widgetId,
|
||||
});
|
||||
|
||||
safeNavigate(dashboardEditView);
|
||||
},
|
||||
[
|
||||
getUpdatedQueryForExport,
|
||||
exportDefaultQuery,
|
||||
options.selectColumns,
|
||||
safeNavigate,
|
||||
notifications,
|
||||
panelType,
|
||||
updateDashboard,
|
||||
handleAxisError,
|
||||
],
|
||||
[getUpdatedQueryForExport, exportDefaultQuery, safeNavigate, panelType],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -811,7 +772,6 @@ function LogsExplorerViews({
|
||||
<ExplorerOptionWrapper
|
||||
disabled={!stagedQuery}
|
||||
query={exportDefaultQuery}
|
||||
isLoading={isUpdateDashboardLoading}
|
||||
onExport={handleExport}
|
||||
sourcepage={DataSource.LOGS}
|
||||
/>
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||
import { logsQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { QueryBuilderContext } from 'providers/QueryBuilder';
|
||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
import { fireEvent, render, RenderResult } from 'tests/test-utils';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import LogsExplorerViews from '..';
|
||||
import { logsQueryRangeSuccessNewFormatResponse } from './mock';
|
||||
import {
|
||||
logsQueryRangeSuccessNewFormatResponse,
|
||||
mockQueryBuilderContextValue,
|
||||
} from './mock';
|
||||
|
||||
const queryRangeURL = 'http://localhost/api/v3/query_range';
|
||||
|
||||
const ACTIVE_LOG_ID = 'test-log-id';
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string } => ({
|
||||
@@ -81,6 +88,31 @@ jest.mock('hooks/useSafeNavigate', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock usePreferenceSync
|
||||
jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
|
||||
usePreferenceSync: (): any => ({
|
||||
preferences: {
|
||||
columns: [],
|
||||
formatting: {
|
||||
maxLines: 2,
|
||||
format: 'table',
|
||||
fontSize: 'small',
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
updateColumns: jest.fn(),
|
||||
updateFormatting: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/logs/useCopyLogLink', () => ({
|
||||
useCopyLogLink: jest.fn().mockReturnValue({
|
||||
activeLogId: ACTIVE_LOG_ID,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Set up the specific behavior for useGetExplorerQueryRange in individual test cases
|
||||
beforeEach(() => {
|
||||
(useGetExplorerQueryRange as jest.Mock).mockReturnValue({
|
||||
@@ -93,13 +125,15 @@ const renderer = (): RenderResult =>
|
||||
<VirtuosoMockContext.Provider
|
||||
value={{ viewportHeight: 300, itemHeight: 100 }}
|
||||
>
|
||||
<LogsExplorerViews
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
showFrequencyChart
|
||||
setIsLoadingQueries={(): void => {}}
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
/>
|
||||
<PreferenceContextProvider>
|
||||
<LogsExplorerViews
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
showFrequencyChart
|
||||
setIsLoadingQueries={(): void => {}}
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</VirtuosoMockContext.Provider>,
|
||||
);
|
||||
|
||||
@@ -162,4 +196,49 @@ describe('LogsExplorerViews -', () => {
|
||||
queryByText('Something went wrong. Please try again or contact support.'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should add activeLogId filter when present in URL', () => {
|
||||
// Mock useCopyLogLink to return an activeLogId
|
||||
(useCopyLogLink as jest.Mock).mockReturnValue({
|
||||
activeLogId: ACTIVE_LOG_ID,
|
||||
});
|
||||
|
||||
lodsQueryServerRequest();
|
||||
render(
|
||||
<QueryBuilderContext.Provider value={mockQueryBuilderContextValue}>
|
||||
<PreferenceContextProvider>
|
||||
<LogsExplorerViews
|
||||
selectedView={SELECTED_VIEWS.SEARCH}
|
||||
showFrequencyChart
|
||||
setIsLoadingQueries={(): void => {}}
|
||||
listQueryKeyRef={{ current: {} }}
|
||||
chartQueryKeyRef={{ current: {} }}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</QueryBuilderContext.Provider>,
|
||||
);
|
||||
|
||||
// Get the query data from the first call to useGetExplorerQueryRange
|
||||
const {
|
||||
queryData,
|
||||
} = (useGetExplorerQueryRange as jest.Mock).mock.calls[0][0].builder;
|
||||
const firstQuery = queryData[0];
|
||||
|
||||
// Get the original number of filters from mock data
|
||||
const originalFiltersLength =
|
||||
mockQueryBuilderContextValue.currentQuery.builder.queryData[0].filters?.items
|
||||
.length || 0;
|
||||
const expectedFiltersLength = originalFiltersLength + 1; // +1 for activeLogId filter
|
||||
|
||||
// Verify that the activeLogId filter is present
|
||||
expect(
|
||||
firstQuery.filters?.items.some(
|
||||
(item: TagFilterItem) =>
|
||||
item.key?.key === 'id' && item.op === '<=' && item.value === ACTIVE_LOG_ID,
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
// Verify the total number of filters (original + 1 new activeLogId filter)
|
||||
expect(firstQuery.filters?.items.length).toBe(expectedFiltersLength);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
import {
|
||||
initialQueriesMap,
|
||||
initialQueryBuilderFormValues,
|
||||
OPERATORS,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { noop } from 'lodash-es';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const logsQueryRangeSuccessNewFormatResponse = {
|
||||
data: {
|
||||
result: [],
|
||||
@@ -49,3 +59,148 @@ export const logsQueryRangeSuccessNewFormatResponse = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockQueryBuilderContextValue = {
|
||||
isDefaultQuery: (): boolean => false,
|
||||
currentQuery: {
|
||||
...initialQueriesMap.logs,
|
||||
builder: {
|
||||
...initialQueriesMap.logs.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValues,
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: {
|
||||
key: 'service',
|
||||
type: '',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
op: OPERATORS['='],
|
||||
value: 'frontend',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
key: {
|
||||
key: 'log_level',
|
||||
type: '',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
op: OPERATORS['='],
|
||||
value: 'INFO',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
initialQueryBuilderFormValues,
|
||||
],
|
||||
},
|
||||
},
|
||||
setSupersetQuery: jest.fn(),
|
||||
supersetQuery: {
|
||||
...initialQueriesMap.logs,
|
||||
builder: {
|
||||
...initialQueriesMap.logs.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValues,
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: {
|
||||
key: 'service',
|
||||
type: '',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
op: OPERATORS['='],
|
||||
value: 'frontend',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
key: {
|
||||
key: 'log_level',
|
||||
type: '',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
op: OPERATORS['='],
|
||||
value: 'INFO',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
initialQueryBuilderFormValues,
|
||||
],
|
||||
},
|
||||
},
|
||||
stagedQuery: {
|
||||
...initialQueriesMap.logs,
|
||||
builder: {
|
||||
...initialQueriesMap.logs.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValues,
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: {
|
||||
key: 'service',
|
||||
type: '',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
op: OPERATORS['='],
|
||||
value: 'frontend',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
key: {
|
||||
key: 'log_level',
|
||||
type: '',
|
||||
dataType: DataTypes.String,
|
||||
isColumn: true,
|
||||
},
|
||||
op: OPERATORS['='],
|
||||
value: 'INFO',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
initialQueryBuilderFormValues,
|
||||
],
|
||||
},
|
||||
},
|
||||
initialDataSource: null,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
isEnabledQuery: false,
|
||||
lastUsedQuery: 0,
|
||||
setLastUsedQuery: noop,
|
||||
handleSetQueryData: noop,
|
||||
handleSetFormulaData: noop,
|
||||
handleSetQueryItemData: noop,
|
||||
handleSetConfig: noop,
|
||||
removeQueryBuilderEntityByIndex: noop,
|
||||
removeQueryTypeItemByIndex: noop,
|
||||
addNewBuilderQuery: noop,
|
||||
cloneQuery: noop,
|
||||
addNewFormula: noop,
|
||||
addNewQueryItem: noop,
|
||||
redirectWithQueryBuilderData: noop,
|
||||
handleRunQuery: noop,
|
||||
resetQuery: noop,
|
||||
updateAllQueriesOperators: (): Query => initialQueriesMap.logs,
|
||||
updateQueriesData: (): Query => initialQueriesMap.logs,
|
||||
initQueryBuilderData: noop,
|
||||
handleOnUnitsChange: noop,
|
||||
isStagedQueryUpdated: (): boolean => false,
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import { logsPaginationQueryRangeSuccessResponse } from 'mocks-server/__mockdata
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from 'ReactI18';
|
||||
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
@@ -108,11 +109,13 @@ describe('LogsPanelComponent', () => {
|
||||
render(
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<DashboardProvider>
|
||||
<NewWidget
|
||||
selectedGraph={PANEL_TYPES.LIST}
|
||||
fillSpans={undefined}
|
||||
yAxisUnit={undefined}
|
||||
/>
|
||||
<PreferenceContextProvider>
|
||||
<NewWidget
|
||||
selectedGraph={PANEL_TYPES.LIST}
|
||||
fillSpans={undefined}
|
||||
yAxisUnit={undefined}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</DashboardProvider>
|
||||
</I18nextProvider>,
|
||||
);
|
||||
|
||||
@@ -2,21 +2,16 @@ import './Explorer.styles.scss';
|
||||
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Switch } from 'antd';
|
||||
import axios from 'axios';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
@@ -24,6 +19,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||
import QuerySection from './QuerySection';
|
||||
import TimeSeries from './TimeSeries';
|
||||
import { ExplorerTabs } from './types';
|
||||
@@ -39,13 +35,6 @@ function Explorer(): JSX.Element {
|
||||
currentQuery,
|
||||
} = useQueryBuilder();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { notifications } = useNotifications();
|
||||
const { mutate: updateDashboard, isLoading } = useUpdateDashboard();
|
||||
const { options } = useOptionsMenu({
|
||||
storageKey: LOCALSTORAGE.METRICS_LIST_OPTIONS,
|
||||
dataSource: DataSource.METRICS,
|
||||
aggregateOperator: 'noop',
|
||||
});
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const isOneChartPerQueryEnabled =
|
||||
@@ -86,59 +75,16 @@ function Explorer(): JSX.Element {
|
||||
|
||||
const widgetId = uuid();
|
||||
|
||||
const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery(
|
||||
dashboard,
|
||||
queryToExport || exportDefaultQuery,
|
||||
const dashboardEditView = generateExportToDashboardLink({
|
||||
query: queryToExport || exportDefaultQuery,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
dashboardId: dashboard.id,
|
||||
widgetId,
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
options.selectColumns,
|
||||
);
|
||||
|
||||
updateDashboard(updatedDashboard, {
|
||||
onSuccess: (data) => {
|
||||
if (data.error) {
|
||||
const message =
|
||||
data.error === 'feature usage exceeded' ? (
|
||||
<span>
|
||||
Panel limit exceeded for {DataSource.METRICS} in community edition.
|
||||
Please checkout our paid plans{' '}
|
||||
<a
|
||||
href="https://signoz.io/pricing/?utm_source=product&utm_medium=dashboard-limit"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
</span>
|
||||
) : (
|
||||
data.error
|
||||
);
|
||||
notifications.error({
|
||||
message,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
const dashboardEditView = generateExportToDashboardLink({
|
||||
query: queryToExport || exportDefaultQuery,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
dashboardId: data.payload?.uuid || '',
|
||||
widgetId,
|
||||
});
|
||||
|
||||
safeNavigate(dashboardEditView);
|
||||
},
|
||||
onError: (error) => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
notifications.error({
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
safeNavigate(dashboardEditView);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[exportDefaultQuery, notifications, updateDashboard],
|
||||
[exportDefaultQuery, safeNavigate],
|
||||
);
|
||||
|
||||
const splitedQueries = useMemo(
|
||||
@@ -149,6 +95,12 @@ function Explorer(): JSX.Element {
|
||||
[stagedQuery],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(MetricsExplorerEvents.TabChanged, {
|
||||
[MetricsExplorerEventKeys.Tab]: 'explorer',
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<div className="metrics-explorer-explore-container">
|
||||
@@ -201,7 +153,6 @@ function Explorer(): JSX.Element {
|
||||
<ExplorerOptionWrapper
|
||||
disabled={!stagedQuery}
|
||||
query={exportDefaultQuery}
|
||||
isLoading={isLoading}
|
||||
sourcepage={DataSource.METRICS}
|
||||
onExport={handleExport}
|
||||
isOneChartPerQuery={showOneChartPerQuery}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import { ButtonWrapper } from 'container/TracesExplorer/QuerySection/styles';
|
||||
@@ -6,6 +7,8 @@ import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQ
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||
|
||||
function QuerySection(): JSX.Element {
|
||||
const { handleRunQuery } = useQueryBuilder();
|
||||
|
||||
@@ -19,7 +22,15 @@ function QuerySection(): JSX.Element {
|
||||
version="v4"
|
||||
actions={
|
||||
<ButtonWrapper>
|
||||
<Button onClick={(): void => handleRunQuery()} type="primary">
|
||||
<Button
|
||||
onClick={(): void => {
|
||||
handleRunQuery();
|
||||
logEvent(MetricsExplorerEvents.QueryBuilderQueryChanged, {
|
||||
[MetricsExplorerEventKeys.Tab]: 'explorer',
|
||||
});
|
||||
}}
|
||||
type="primary"
|
||||
>
|
||||
Run Query
|
||||
</Button>
|
||||
</ButtonWrapper>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Card, Tooltip, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
|
||||
import classNames from 'classnames';
|
||||
import ResizeTable from 'components/ResizeTable/ResizeTable';
|
||||
@@ -11,6 +12,7 @@ import { DataType } from 'container/LogDetailedView/TableView';
|
||||
import { ArrowDownCircle, ArrowRightCircle, Focus } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||
import {
|
||||
SPACE_AGGREGATION_OPTIONS_FOR_EXPANDED_VIEW,
|
||||
TIME_AGGREGATION_OPTIONS,
|
||||
@@ -39,6 +41,21 @@ function ExpandedView({
|
||||
setSelectedTimeSeries,
|
||||
] = useState<InspectMetricsSeries | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(MetricsExplorerEvents.InspectPointClicked, {
|
||||
[MetricsExplorerEventKeys.Modal]: 'inspect',
|
||||
[MetricsExplorerEventKeys.Filters]: metricInspectionOptions.filters,
|
||||
[MetricsExplorerEventKeys.TimeAggregationInterval]:
|
||||
metricInspectionOptions.timeAggregationInterval,
|
||||
[MetricsExplorerEventKeys.TimeAggregationOption]:
|
||||
metricInspectionOptions.timeAggregationOption,
|
||||
[MetricsExplorerEventKeys.SpaceAggregationOption]:
|
||||
metricInspectionOptions.spaceAggregationOption,
|
||||
[MetricsExplorerEventKeys.SpaceAggregationLabels]:
|
||||
metricInspectionOptions.spaceAggregationLabels,
|
||||
});
|
||||
}, [metricInspectionOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (step !== InspectionStep.COMPLETED) {
|
||||
setSelectedTimeSeries(options?.timeSeries ?? null);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Skeleton, Switch, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import Uplot from 'components/Uplot';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
@@ -8,6 +9,7 @@ import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
|
||||
import { METRIC_TYPE_TO_COLOR_MAP, METRIC_TYPE_TO_ICON_MAP } from './constants';
|
||||
import GraphPopover from './GraphPopover';
|
||||
@@ -203,7 +205,14 @@ function GraphView({
|
||||
<div className="view-toggle-button">
|
||||
<Switch
|
||||
checked={viewType === 'graph'}
|
||||
onChange={(checked): void => setViewType(checked ? 'graph' : 'table')}
|
||||
onChange={(checked): void => {
|
||||
const newViewType = checked ? 'graph' : 'table';
|
||||
setViewType(newViewType);
|
||||
logEvent(MetricsExplorerEvents.InspectViewChanged, {
|
||||
[MetricsExplorerEventKeys.Tab]: 'inspect',
|
||||
[MetricsExplorerEventKeys.InspectView]: newViewType,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Typography.Text>
|
||||
{viewType === 'graph' ? 'Graph View' : 'Table View'}
|
||||
|
||||
@@ -3,6 +3,7 @@ import './Inspect.styles.scss';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Drawer, Empty, Skeleton, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useGetMetricDetails } from 'hooks/metricsExplorer/useGetMetricDetails';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
@@ -11,11 +12,16 @@ import { Compass } from 'lucide-react';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||
import ExpandedView from './ExpandedView';
|
||||
import GraphView from './GraphView';
|
||||
import QueryBuilder from './QueryBuilder';
|
||||
import Stepper from './Stepper';
|
||||
import { GraphPopoverOptions, InspectProps } from './types';
|
||||
import {
|
||||
GraphPopoverOptions,
|
||||
InspectProps,
|
||||
MetricInspectionAction,
|
||||
} from './types';
|
||||
import { useInspectMetrics } from './useInspectMetrics';
|
||||
|
||||
function Inspect({
|
||||
@@ -92,6 +98,25 @@ function Inspect({
|
||||
reset,
|
||||
} = useInspectMetrics(metricName);
|
||||
|
||||
const handleDispatchMetricInspectionOptions = useCallback(
|
||||
(action: MetricInspectionAction): void => {
|
||||
dispatchMetricInspectionOptions(action);
|
||||
logEvent(MetricsExplorerEvents.InspectQueryChanged, {
|
||||
[MetricsExplorerEventKeys.Modal]: 'inspect',
|
||||
[MetricsExplorerEventKeys.Filters]: metricInspectionOptions.filters,
|
||||
[MetricsExplorerEventKeys.TimeAggregationInterval]:
|
||||
metricInspectionOptions.timeAggregationInterval,
|
||||
[MetricsExplorerEventKeys.TimeAggregationOption]:
|
||||
metricInspectionOptions.timeAggregationOption,
|
||||
[MetricsExplorerEventKeys.SpaceAggregationOption]:
|
||||
metricInspectionOptions.spaceAggregationOption,
|
||||
[MetricsExplorerEventKeys.SpaceAggregationLabels]:
|
||||
metricInspectionOptions.spaceAggregationLabels,
|
||||
});
|
||||
},
|
||||
[dispatchMetricInspectionOptions, metricInspectionOptions],
|
||||
);
|
||||
|
||||
const selectedMetricType = useMemo(
|
||||
() => metricDetailsData?.payload?.data?.metadata?.metric_type,
|
||||
[metricDetailsData],
|
||||
@@ -186,7 +211,7 @@ function Inspect({
|
||||
setMetricName={setMetricName}
|
||||
spaceAggregationLabels={spaceAggregationLabels}
|
||||
metricInspectionOptions={metricInspectionOptions}
|
||||
dispatchMetricInspectionOptions={dispatchMetricInspectionOptions}
|
||||
dispatchMetricInspectionOptions={handleDispatchMetricInspectionOptions}
|
||||
inspectionStep={inspectionStep}
|
||||
inspectMetricsTimeSeries={inspectMetricsTimeSeries}
|
||||
searchQuery={searchQuery}
|
||||
@@ -227,12 +252,18 @@ function Inspect({
|
||||
popoverOptions,
|
||||
metricInspectionOptions,
|
||||
spaceAggregationLabels,
|
||||
dispatchMetricInspectionOptions,
|
||||
handleDispatchMetricInspectionOptions,
|
||||
searchQuery,
|
||||
expandedViewOptions,
|
||||
timeAggregatedSeriesMap,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(MetricsExplorerEvents.ModalOpened, {
|
||||
[MetricsExplorerEventKeys.Modal]: 'inspect',
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||
<Drawer
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { Card, Input, Select, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
import classNames from 'classnames';
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||
import {
|
||||
SPACE_AGGREGATION_OPTIONS,
|
||||
TIME_AGGREGATION_OPTIONS,
|
||||
@@ -135,6 +137,9 @@ export function MetricFilters({
|
||||
}}
|
||||
onChange={(value): void => {
|
||||
handleChangeQueryData('filters', value);
|
||||
logEvent(MetricsExplorerEvents.FilterApplied, {
|
||||
[MetricsExplorerEventKeys.Modal]: 'inspect',
|
||||
});
|
||||
dispatchMetricInspectionOptions({
|
||||
type: 'SET_FILTERS',
|
||||
payload: value,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button, Collapse, Input, Menu, Popover, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { DataType } from 'container/LogDetailedView/TableView';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@@ -10,6 +11,7 @@ import { useCopyToClipboard } from 'react-use';
|
||||
import { PANEL_TYPES } from '../../../constants/queryBuilder';
|
||||
import ROUTES from '../../../constants/routes';
|
||||
import { useHandleExplorerTabChange } from '../../../hooks/useHandleExplorerTabChange';
|
||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||
import { AllAttributesProps, AllAttributesValueProps } from './types';
|
||||
import { getMetricDetailsQuery } from './utils';
|
||||
|
||||
@@ -135,9 +137,16 @@ function AllAttributes({
|
||||
},
|
||||
ROUTES.METRICS_EXPLORER_EXPLORER,
|
||||
);
|
||||
logEvent(MetricsExplorerEvents.OpenInExplorerClicked, {
|
||||
[MetricsExplorerEventKeys.MetricName]: metricName,
|
||||
[MetricsExplorerEventKeys.Tab]: 'summary',
|
||||
[MetricsExplorerEventKeys.Modal]: 'metric-details',
|
||||
[MetricsExplorerEventKeys.AttributeKey]: groupBy,
|
||||
});
|
||||
},
|
||||
[metricName, metricType, handleExplorerTabChange],
|
||||
);
|
||||
|
||||
const goToMetricsExploreWithAppliedAttribute = useCallback(
|
||||
(key: string, value: string) => {
|
||||
const compositeQuery = getMetricDetailsQuery(metricName, metricType, {
|
||||
@@ -153,6 +162,13 @@ function AllAttributes({
|
||||
},
|
||||
ROUTES.METRICS_EXPLORER_EXPLORER,
|
||||
);
|
||||
logEvent(MetricsExplorerEvents.OpenInExplorerClicked, {
|
||||
[MetricsExplorerEventKeys.MetricName]: metricName,
|
||||
[MetricsExplorerEventKeys.Tab]: 'summary',
|
||||
[MetricsExplorerEventKeys.Modal]: 'metric-details',
|
||||
[MetricsExplorerEventKeys.AttributeKey]: key,
|
||||
[MetricsExplorerEventKeys.AttributeValue]: value,
|
||||
});
|
||||
},
|
||||
[metricName, metricType, handleExplorerTabChange],
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button, Collapse, Input, Select, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
import { UpdateMetricMetadataProps } from 'api/metricsExplorer/updateMetricMetadata';
|
||||
@@ -11,6 +12,7 @@ import { useNotifications } from 'hooks/useNotifications';
|
||||
import { Edit2, Save, X } from 'lucide-react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||
import {
|
||||
METRIC_TYPE_LABEL_MAP,
|
||||
METRIC_TYPE_VALUES_MAP,
|
||||
@@ -170,6 +172,11 @@ function Metadata({
|
||||
{
|
||||
onSuccess: (response): void => {
|
||||
if (response?.statusCode === 200) {
|
||||
logEvent(MetricsExplorerEvents.MetricMetadataUpdated, {
|
||||
[MetricsExplorerEventKeys.MetricName]: metricName,
|
||||
[MetricsExplorerEventKeys.Tab]: 'summary',
|
||||
[MetricsExplorerEventKeys.Modal]: 'metric-details',
|
||||
});
|
||||
notifications.success({
|
||||
message: 'Metadata updated successfully',
|
||||
});
|
||||
|
||||
@@ -11,14 +11,16 @@ import {
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useGetMetricDetails } from 'hooks/metricsExplorer/useGetMetricDetails';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { Compass, Crosshair, X } from 'lucide-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { PANEL_TYPES } from '../../../constants/queryBuilder';
|
||||
import ROUTES from '../../../constants/routes';
|
||||
import { useHandleExplorerTabChange } from '../../../hooks/useHandleExplorerTabChange';
|
||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||
import { isInspectEnabled } from '../Inspect/utils';
|
||||
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
|
||||
import AllAttributes from './AllAttributes';
|
||||
@@ -95,11 +97,22 @@ function MetricDetails({
|
||||
},
|
||||
ROUTES.METRICS_EXPLORER_EXPLORER,
|
||||
);
|
||||
logEvent(MetricsExplorerEvents.OpenInExplorerClicked, {
|
||||
[MetricsExplorerEventKeys.MetricName]: metricName,
|
||||
[MetricsExplorerEventKeys.Tab]: 'summary',
|
||||
[MetricsExplorerEventKeys.Modal]: 'metric-details',
|
||||
});
|
||||
}
|
||||
}, [metricName, handleExplorerTabChange, metric?.metadata?.metric_type]);
|
||||
|
||||
const isMetricDetailsError = metricDetailsError || !metric;
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(MetricsExplorerEvents.ModalOpened, {
|
||||
[MetricsExplorerEventKeys.Modal]: 'metric-details',
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width="60%"
|
||||
|
||||
@@ -10,24 +10,21 @@ import {
|
||||
} from 'antd';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetMetricsListFilterValues } from 'hooks/metricsExplorer/useGetMetricsListFilterValues';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { Search } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { COMPOSITE_QUERY_KEY } from './constants';
|
||||
import { SUMMARY_FILTERS_KEY } from './constants';
|
||||
|
||||
function MetricNameSearch(): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
const [, setSearchParams] = useSearchParams();
|
||||
function MetricNameSearch({
|
||||
queryFilters,
|
||||
}: {
|
||||
queryFilters: TagFilter;
|
||||
}): JSX.Element {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
const [searchString, setSearchString] = useState<string>('');
|
||||
@@ -70,9 +67,9 @@ function MetricNameSearch(): JSX.Element {
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(selectedMetricName: string): void => {
|
||||
const newFilter = {
|
||||
const newFilters = {
|
||||
items: [
|
||||
...currentQuery.builder.queryData[0].filters.items,
|
||||
...queryFilters.items,
|
||||
{
|
||||
id: 'metric_name',
|
||||
op: 'CONTAINS',
|
||||
@@ -84,27 +81,15 @@ function MetricNameSearch(): JSX.Element {
|
||||
value: selectedMetricName,
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
op: 'and',
|
||||
};
|
||||
const compositeQuery = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...currentQuery.builder.queryData[0],
|
||||
filters: newFilter,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
handleChangeQueryData('filters', newFilter);
|
||||
setSearchParams({
|
||||
[COMPOSITE_QUERY_KEY]: JSON.stringify(compositeQuery),
|
||||
...Object.fromEntries(searchParams.entries()),
|
||||
[SUMMARY_FILTERS_KEY]: JSON.stringify(newFilters),
|
||||
});
|
||||
setIsPopoverOpen(false);
|
||||
},
|
||||
[currentQuery, handleChangeQueryData, setSearchParams],
|
||||
[queryFilters.items, setSearchParams, searchParams],
|
||||
);
|
||||
|
||||
const metricNameFilterValues = useMemo(
|
||||
@@ -198,7 +183,7 @@ function MetricNameSearch(): JSX.Element {
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const value = e.target.value.trim().toLowerCase();
|
||||
const value = e.target.value.trim();
|
||||
setSearchString(value);
|
||||
debouncedUpdate(value);
|
||||
},
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import { Button, Menu, Popover, Tooltip } from 'antd';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Search } from 'lucide-react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
COMPOSITE_QUERY_KEY,
|
||||
METRIC_TYPE_LABEL_MAP,
|
||||
METRIC_TYPE_VALUES_MAP,
|
||||
SUMMARY_FILTERS_KEY,
|
||||
} from './constants';
|
||||
|
||||
function MetricTypeSearch(): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query: currentQuery.builder.queryData[0],
|
||||
entityVersion: '',
|
||||
});
|
||||
function MetricTypeSearch({
|
||||
queryFilters,
|
||||
}: {
|
||||
queryFilters: TagFilter;
|
||||
}): JSX.Element {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const [, setSearchParams] = useSearchParams();
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
|
||||
const menuItems = useMemo(
|
||||
@@ -40,9 +37,9 @@ function MetricTypeSearch(): JSX.Element {
|
||||
const handleSelect = useCallback(
|
||||
(selectedMetricType: string): void => {
|
||||
if (selectedMetricType !== 'all') {
|
||||
const newFilter = {
|
||||
const newFilters = {
|
||||
items: [
|
||||
...currentQuery.builder.queryData[0].filters.items,
|
||||
...queryFilters.items,
|
||||
{
|
||||
id: 'metric_type',
|
||||
op: '=',
|
||||
@@ -56,49 +53,23 @@ function MetricTypeSearch(): JSX.Element {
|
||||
],
|
||||
op: 'AND',
|
||||
};
|
||||
const compositeQuery = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...currentQuery.builder.queryData[0],
|
||||
filters: newFilter,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
handleChangeQueryData('filters', newFilter);
|
||||
setSearchParams({
|
||||
[COMPOSITE_QUERY_KEY]: JSON.stringify(compositeQuery),
|
||||
...Object.fromEntries(searchParams.entries()),
|
||||
[SUMMARY_FILTERS_KEY]: JSON.stringify(newFilters),
|
||||
});
|
||||
} else {
|
||||
const newFilter = {
|
||||
items: currentQuery.builder.queryData[0].filters.items.filter(
|
||||
(item) => item.id !== 'metric_type',
|
||||
),
|
||||
const newFilters = {
|
||||
items: queryFilters.items.filter((item) => item.id !== 'metric_type'),
|
||||
op: 'AND',
|
||||
};
|
||||
const compositeQuery = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...currentQuery.builder.queryData[0],
|
||||
filters: newFilter,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
handleChangeQueryData('filters', newFilter);
|
||||
setSearchParams({
|
||||
[COMPOSITE_QUERY_KEY]: JSON.stringify(compositeQuery),
|
||||
...Object.fromEntries(searchParams.entries()),
|
||||
[SUMMARY_FILTERS_KEY]: JSON.stringify(newFilters),
|
||||
});
|
||||
}
|
||||
setIsPopoverOpen(false);
|
||||
},
|
||||
[currentQuery, handleChangeQueryData, setSearchParams],
|
||||
[queryFilters.items, setSearchParams, searchParams],
|
||||
);
|
||||
|
||||
const menu = (
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Info } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { MetricsListItemRowData, MetricsTableProps } from './types';
|
||||
import { metricsTableColumns } from './utils';
|
||||
import { getMetricsTableColumns } from './utils';
|
||||
|
||||
function MetricsTable({
|
||||
isLoading,
|
||||
@@ -24,6 +24,7 @@ function MetricsTable({
|
||||
setOrderBy,
|
||||
totalCount,
|
||||
openMetricDetails,
|
||||
queryFilters,
|
||||
}: MetricsTableProps): JSX.Element {
|
||||
const handleTableChange: TableProps<MetricsListItemRowData>['onChange'] = useCallback(
|
||||
(
|
||||
@@ -74,7 +75,7 @@ function MetricsTable({
|
||||
),
|
||||
}}
|
||||
dataSource={data}
|
||||
columns={metricsTableColumns}
|
||||
columns={getMetricsTableColumns(queryFilters)}
|
||||
locale={{
|
||||
emptyText: isLoading ? null : (
|
||||
<div
|
||||
@@ -107,7 +108,7 @@ function MetricsTable({
|
||||
total: totalCount,
|
||||
}}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => openMetricDetails(record.key),
|
||||
onClick: (): void => openMetricDetails(record.key, 'list'),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user