mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-27 14:10:30 +01:00
Compare commits
3 Commits
chore/dril
...
platform-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2cab3d825 | ||
|
|
145587f1b0 | ||
|
|
79e3340292 |
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"oxc.typeAware": true,
|
||||
"oxc.tsConfigPath": "./frontend/tsconfig.json",
|
||||
"oxc.configPath": "./frontend/.oxlintrc.json",
|
||||
"oxc.fmt.configPath": "./frontend/.oxfmtrc.json",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode",
|
||||
"editor.codeActionsOnSave": {
|
||||
@@ -21,3 +19,4 @@
|
||||
"python-envs.defaultEnvManager": "ms-python.python:system",
|
||||
"python-envs.pythonProjects": []
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/auditor"
|
||||
"github.com/SigNoz/signoz/pkg/authn"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/authz/authzregistry"
|
||||
"github.com/SigNoz/signoz/pkg/authz/openfgaauthz"
|
||||
"github.com/SigNoz/signoz/pkg/authz/openfgaschema"
|
||||
"github.com/SigNoz/signoz/pkg/authz/openfgaserver"
|
||||
@@ -92,13 +93,14 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
|
||||
return signoz.NewAuthNs(ctx, providerSettings, store, licensing)
|
||||
},
|
||||
func(ctx context.Context, sqlstore sqlstore.SQLStore, _ licensing.Licensing, _ []authz.OnBeforeRoleDelete, _ dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
|
||||
func(ctx context.Context, sqlstore sqlstore.SQLStore, _ licensing.Licensing, _ []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
|
||||
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore), nil
|
||||
authzRegistry := authzregistry.NewAuthzRegistry()
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, authzRegistry), nil
|
||||
},
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module {
|
||||
return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser)
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/auditor"
|
||||
"github.com/SigNoz/signoz/pkg/authn"
|
||||
"github.com/SigNoz/signoz/pkg/authz"
|
||||
"github.com/SigNoz/signoz/pkg/authz/authzregistry"
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
@@ -137,12 +138,14 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
|
||||
return authNs, nil
|
||||
},
|
||||
func(ctx context.Context, sqlstore sqlstore.SQLStore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, dashboardModule dashboard.Module) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
|
||||
func(ctx context.Context, sqlstore sqlstore.SQLStore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete) (factory.ProviderFactory[authz.AuthZ, authz.Config], error) {
|
||||
openfgaDataStore, err := openfgaserver.NewSQLStore(sqlstore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, onBeforeRoleDelete, dashboardModule), nil
|
||||
|
||||
authzRegistry := authzregistry.NewAuthzRegistry()
|
||||
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx), openfgaDataStore, licensing, onBeforeRoleDelete, authzRegistry), nil
|
||||
},
|
||||
func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module {
|
||||
return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, queryParser, querier, licensing)
|
||||
|
||||
@@ -8523,9 +8523,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- service-accounts:list
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- service-accounts:list
|
||||
summary: List service accounts
|
||||
tags:
|
||||
- serviceaccount
|
||||
@@ -8585,9 +8585,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- service-accounts:create
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- service-accounts:create
|
||||
summary: Create service account
|
||||
tags:
|
||||
- serviceaccount
|
||||
@@ -8635,9 +8635,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- service-account:delete
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- service-account:delete
|
||||
summary: Deletes a service account
|
||||
tags:
|
||||
- serviceaccount
|
||||
@@ -8692,9 +8692,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- service-account:read
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- service-account:read
|
||||
summary: Gets a service account
|
||||
tags:
|
||||
- serviceaccount
|
||||
@@ -8752,9 +8752,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- service-account:update
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- service-account:update
|
||||
summary: Updates a service account
|
||||
tags:
|
||||
- serviceaccount
|
||||
@@ -8806,9 +8806,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- service-account:read
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- service-account:read
|
||||
summary: List service account keys
|
||||
tags:
|
||||
- serviceaccount
|
||||
@@ -8874,9 +8874,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- service-account:update
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- service-account:update
|
||||
summary: Create a service account key
|
||||
tags:
|
||||
- serviceaccount
|
||||
@@ -8929,9 +8929,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- service-account:update
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- service-account:update
|
||||
summary: Revoke a service account key
|
||||
tags:
|
||||
- serviceaccount
|
||||
@@ -8994,9 +8994,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- service-account:update
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- service-account:update
|
||||
summary: Updates a service account key
|
||||
tags:
|
||||
- serviceaccount
|
||||
@@ -9055,9 +9055,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- service-account:read
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- service-account:read
|
||||
summary: Gets service account roles
|
||||
tags:
|
||||
- serviceaccount
|
||||
@@ -9117,9 +9117,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- service-account:update
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- service-account:update
|
||||
summary: Create service account role
|
||||
tags:
|
||||
- serviceaccount
|
||||
@@ -9166,9 +9166,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- service-account:update
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
- service-account:update
|
||||
summary: Delete service account role
|
||||
tags:
|
||||
- serviceaccount
|
||||
|
||||
200
docs/contributing/go/authz.md
Normal file
200
docs/contributing/go/authz.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# Authorization (FGA)
|
||||
|
||||
SigNoz uses OpenFGA for fine-grained authorization. Resources are modeled as FGA objects — the authz system checks whether a principal (user or service account) has a specific relation (read, update, delete, etc.) on a specific resource.
|
||||
|
||||
This guide explains how to enable FGA for a new entity.
|
||||
|
||||
## Overview
|
||||
|
||||
Enabling FGA for an entity involves four steps:
|
||||
|
||||
1. **Define typeables** — Declare the resource type identities in `authtypes`
|
||||
2. **Register in the authz registry** — Define which managed roles get which permissions
|
||||
3. **Switch routes to the Check middleware** — Replace role-based middleware with resource-level FGA checks
|
||||
4. **Add a migration** — Backfill FGA tuples for existing organizations
|
||||
|
||||
## Step 1: Define typeables in `authtypes`
|
||||
|
||||
Add the typeable vars to the var block in `pkg/types/authtypes/typeable.go`, alongside the existing typeables. Every FGA-managed entity needs two typeables:
|
||||
|
||||
- A **collection typeable** (`metaresources`) — for `create` and `list` operations
|
||||
- An **instance typeable** (`metaresource`) — for `read`, `update`, and `delete` operations
|
||||
|
||||
```go
|
||||
// pkg/types/authtypes/typeable.go — add to the existing var block
|
||||
var (
|
||||
// ... existing typeables ...
|
||||
|
||||
TypeableMetaResourceMyEntity = MustNewTypeableMetaResource(MustNewName("my-entity"))
|
||||
TypeableMetaResourcesMyEntities = MustNewTypeableMetaResources(MustNewName("my-entities"))
|
||||
)
|
||||
```
|
||||
|
||||
These produce FGA objects like:
|
||||
- `metaresource:organization/{orgID}/my-entity/{entityID}` — individual instance
|
||||
- `metaresources:organization/{orgID}/my-entities/*` — collection
|
||||
|
||||
Use kebab-case for names. The collection name is typically the plural form.
|
||||
|
||||
## Step 2: Register in the authz registry
|
||||
|
||||
Create a new file `pkg/authz/authzregistry/myentity.go`. Each registry file exports two functions:
|
||||
|
||||
- `myEntityTypeables()` — returns the typeables for this entity
|
||||
- `myEntityTransactions()` — returns the managed role → transaction mapping
|
||||
|
||||
Use `serviceaccount.go` or `dashboard.go` as a reference. Here is the pattern:
|
||||
|
||||
```go
|
||||
package authzregistry
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
|
||||
func myEntityTypeables() []authtypes.Typeable {
|
||||
return []authtypes.Typeable{
|
||||
authtypes.TypeableMetaResourceMyEntity,
|
||||
authtypes.TypeableMetaResourcesMyEntities,
|
||||
}
|
||||
}
|
||||
|
||||
func myEntityTransactions() map[string][]*authtypes.Transaction {
|
||||
return map[string][]*authtypes.Transaction{
|
||||
authtypes.SigNozAdminRoleName: {
|
||||
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourcesMyEntities, authtypes.RelationCreate),
|
||||
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourcesMyEntities, authtypes.RelationList),
|
||||
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourceMyEntity, authtypes.RelationRead),
|
||||
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourceMyEntity, authtypes.RelationUpdate),
|
||||
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourceMyEntity, authtypes.RelationDelete),
|
||||
},
|
||||
authtypes.SigNozEditorRoleName: {
|
||||
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourcesMyEntities, authtypes.RelationList),
|
||||
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourceMyEntity, authtypes.RelationRead),
|
||||
},
|
||||
authtypes.SigNozViewerRoleName: {
|
||||
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourcesMyEntities, authtypes.RelationList),
|
||||
authtypes.MustNewWildcardTransaction(authtypes.TypeableMetaResourceMyEntity, authtypes.RelationRead),
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`MustNewWildcardTransaction(typeable, relation)` creates a transaction granting the relation on all instances (`*`) of that resource type. It validates that the relation is valid for the type and generates a unique ID.
|
||||
|
||||
Then wire it into `pkg/authz/authzregistry/registry.go`:
|
||||
|
||||
```go
|
||||
func collectTypeables() []authtypes.Typeable {
|
||||
typeables := make([]authtypes.Typeable, 0)
|
||||
typeables = append(typeables, roleTypeables()...)
|
||||
typeables = append(typeables, dashboardTypeables()...)
|
||||
typeables = append(typeables, serviceAccountTypeables()...)
|
||||
typeables = append(typeables, myEntityTypeables()...) // <-- add this
|
||||
return typeables
|
||||
}
|
||||
|
||||
func collectTransactions() map[string][]*authtypes.Transaction {
|
||||
transactions := make(map[string][]*authtypes.Transaction)
|
||||
|
||||
sources := []map[string][]*authtypes.Transaction{
|
||||
dashboardTransactions(),
|
||||
serviceAccountTransactions(),
|
||||
myEntityTransactions(), // <-- add this
|
||||
}
|
||||
|
||||
for _, source := range sources {
|
||||
for roleName, txns := range source {
|
||||
transactions[roleName] = append(transactions[roleName], txns...)
|
||||
}
|
||||
}
|
||||
|
||||
return transactions
|
||||
}
|
||||
```
|
||||
|
||||
## Step 3: Switch routes to the Check middleware
|
||||
|
||||
In your route file (e.g., `pkg/apiserver/signozapiserver/myentity.go`), replace `AdminAccess` / `EditAccess` / `ViewAccess` with the `Check` middleware:
|
||||
|
||||
```go
|
||||
provider.authZ.Check(
|
||||
handler, // the HTTP handler func
|
||||
authtypes.RelationRead, // the relation to check
|
||||
authtypes.TypeableMetaResourceMyEntity, // the typeable
|
||||
selectorCallback, // extracts resource ID from the request
|
||||
roles, // role names for community edition fallback
|
||||
)
|
||||
```
|
||||
|
||||
### Selector callbacks
|
||||
|
||||
You need two callbacks — one for collection operations, one for instance operations:
|
||||
|
||||
```go
|
||||
// For create/list — wildcard selector on the collection.
|
||||
func myEntityCollectionSelector(_ *http.Request, _ authtypes.Claims) ([]authtypes.Selector, error) {
|
||||
return []authtypes.Selector{
|
||||
authtypes.MustNewSelector(authtypes.TypeMetaResources, authtypes.WildCardSelectorString),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// For read/update/delete — specific instance ID + wildcard.
|
||||
func myEntityInstanceSelector(req *http.Request, _ authtypes.Claims) ([]authtypes.Selector, error) {
|
||||
id := mux.Vars(req)["id"]
|
||||
idSelector, err := authtypes.NewSelector(authtypes.TypeMetaResource, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []authtypes.Selector{
|
||||
idSelector,
|
||||
authtypes.MustNewSelector(authtypes.TypeMetaResource, authtypes.WildCardSelectorString),
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
The instance callback includes a wildcard selector so that roles with wildcard permission (`*`) also match. Use `NewSelector` (not `MustNewSelector`) for user-supplied path parameters to avoid panics on malformed input.
|
||||
|
||||
### Role fallback
|
||||
|
||||
The `roles` parameter is used by the **community edition**, where `CheckWithTupleCreation` only checks role membership (ignoring resource selectors). Pass the role names that should have access:
|
||||
|
||||
```go
|
||||
var myEntityAdminRoles = []string{authtypes.SigNozAdminRoleName}
|
||||
var myEntityReadRoles = []string{authtypes.SigNozAdminRoleName, authtypes.SigNozEditorRoleName, authtypes.SigNozViewerRoleName}
|
||||
```
|
||||
|
||||
### OpenAPI security schemes
|
||||
|
||||
Use `newScopedSecuritySchemes` with the exact FGA scope, generated via `Typeable.Scope(relation)`:
|
||||
|
||||
```go
|
||||
SecuritySchemes: newScopedSecuritySchemes([]string{
|
||||
authtypes.TypeableMetaResourceMyEntity.Scope(authtypes.RelationRead),
|
||||
}),
|
||||
// produces: ["my-entity:read"]
|
||||
```
|
||||
|
||||
## Step 4: Add a migration for existing organizations
|
||||
|
||||
New organizations get FGA tuples automatically during bootstrap (via `CreateManagedUserRoleTransactions`). Existing organizations need a SQL migration to backfill the tuples.
|
||||
|
||||
Create a migration file in `pkg/sqlmigration/` (use the next available number). Follow the pattern in `078_add_sa_managed_role_txn.go`:
|
||||
|
||||
1. Select the OpenFGA store ID
|
||||
2. Iterate all organizations
|
||||
3. For each org × tuple, insert into the `tuple` and `changelog` tables
|
||||
4. Use `ON CONFLICT DO NOTHING` for idempotency
|
||||
5. Handle both PostgreSQL and SQLite dialects
|
||||
|
||||
Register the migration in `pkg/signoz/provider.go`.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Typeable vars added to `pkg/types/authtypes/typeable.go`
|
||||
- [ ] Registry file created in `pkg/authz/authzregistry/` with `*Typeables()` and `*Transactions()` functions
|
||||
- [ ] Functions wired into `collectTypeables()` and `collectTransactions()` in `registry.go`
|
||||
- [ ] Routes switched from `AdminAccess`/`EditAccess`/`ViewAccess` to `Check` middleware
|
||||
- [ ] Selector callbacks use `NewSelector` (not `MustNewSelector`) for user-supplied IDs
|
||||
- [ ] OpenAPI `SecuritySchemes` use `newScopedSecuritySchemes` with exact scope strings
|
||||
- [ ] Migration backfills FGA tuples for existing organizations
|
||||
- [ ] `make go-build-community` and `make go-build-enterprise` compile
|
||||
- [ ] `make go-test` passes
|
||||
@@ -11,6 +11,7 @@ We adhere to three primary style guides as our foundation:
|
||||
We **recommend** (almost enforce) reviewing these guides before contributing to the codebase. They provide valuable insights into writing idiomatic Go code and will help you understand our approach to backend development. In addition, we have a few additional rules that make certain areas stricter than the above which can be found in area-specific files in this package:
|
||||
|
||||
- [Abstractions](abstractions.md) - When to introduce new types and intermediate representations
|
||||
- [Authorization](authz.md) - Enabling FGA for new entities
|
||||
- [Errors](errors.md) - Structured error handling
|
||||
- [Endpoint](endpoint.md) - HTTP endpoint patterns
|
||||
- [Flagger](flagger.md) - Feature flag patterns
|
||||
|
||||
@@ -25,19 +25,19 @@ type provider struct {
|
||||
openfgaServer *openfgaserver.Server
|
||||
licensing licensing.Licensing
|
||||
store authtypes.RoleStore
|
||||
registry []authz.RegisterTypeable
|
||||
registry authz.Registry
|
||||
settings factory.ScopedProviderSettings
|
||||
onBeforeRoleDelete []authz.OnBeforeRoleDelete
|
||||
}
|
||||
|
||||
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, registry ...authz.RegisterTypeable) factory.ProviderFactory[authz.AuthZ, authz.Config] {
|
||||
func NewProviderFactory(sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, registry authz.Registry) factory.ProviderFactory[authz.AuthZ, authz.Config] {
|
||||
return factory.NewProviderFactory(factory.MustNewName("openfga"), func(ctx context.Context, ps factory.ProviderSettings, config authz.Config) (authz.AuthZ, error) {
|
||||
return newOpenfgaProvider(ctx, ps, config, sqlstore, openfgaSchema, openfgaDataStore, licensing, onBeforeRoleDelete, registry)
|
||||
})
|
||||
}
|
||||
|
||||
func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, registry []authz.RegisterTypeable) (authz.AuthZ, error) {
|
||||
pkgOpenfgaAuthzProvider := pkgopenfgaauthz.NewProviderFactory(sqlstore, openfgaSchema, openfgaDataStore)
|
||||
func newOpenfgaProvider(ctx context.Context, settings factory.ProviderSettings, config authz.Config, sqlstore sqlstore.SQLStore, openfgaSchema []openfgapkgtransformer.ModuleFile, openfgaDataStore storage.OpenFGADatastore, licensing licensing.Licensing, onBeforeRoleDelete []authz.OnBeforeRoleDelete, registry authz.Registry) (authz.AuthZ, error) {
|
||||
pkgOpenfgaAuthzProvider := pkgopenfgaauthz.NewProviderFactory(sqlstore, openfgaSchema, openfgaDataStore, registry)
|
||||
pkgAuthzService, err := pkgOpenfgaAuthzProvider.New(ctx, settings, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -210,12 +210,7 @@ func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, ro
|
||||
|
||||
func (provider *provider) GetResources(_ context.Context) []*authtypes.Resource {
|
||||
resources := make([]*authtypes.Resource, 0)
|
||||
for _, register := range provider.registry {
|
||||
for _, typeable := range register.MustGetTypeables() {
|
||||
resources = append(resources, &authtypes.Resource{Name: typeable.Name(), Type: typeable.Type()})
|
||||
}
|
||||
}
|
||||
for _, typeable := range provider.MustGetTypeables() {
|
||||
for _, typeable := range provider.registry.GetTypeables() {
|
||||
resources = append(resources, &authtypes.Resource{Name: typeable.Name(), Type: typeable.Type()})
|
||||
}
|
||||
|
||||
@@ -234,7 +229,7 @@ func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id
|
||||
}
|
||||
|
||||
objects := make([]*authtypes.Object, 0)
|
||||
for _, objectType := range provider.getUniqueTypes() {
|
||||
for _, objectType := range provider.registry.GetUniqueTypes() {
|
||||
if !slices.Contains(authtypes.TypeableRelations[objectType], relation) {
|
||||
continue
|
||||
}
|
||||
@@ -318,9 +313,6 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
|
||||
return provider.store.Delete(ctx, orgID, id)
|
||||
}
|
||||
|
||||
func (provider *provider) MustGetTypeables() []authtypes.Typeable {
|
||||
return []authtypes.Typeable{authtypes.TypeableRole, authtypes.TypeableResourcesRoles}
|
||||
}
|
||||
|
||||
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) ([]*openfgav1.TupleKey, error) {
|
||||
tuples := []*openfgav1.TupleKey{}
|
||||
@@ -359,15 +351,8 @@ func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID va
|
||||
}
|
||||
|
||||
func (provider *provider) getManagedRoleTransactionTuples(orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
|
||||
transactionsByRole := make(map[string][]*authtypes.Transaction)
|
||||
for _, register := range provider.registry {
|
||||
for roleName, txns := range register.MustGetManagedRoleTransactions() {
|
||||
transactionsByRole[roleName] = append(transactionsByRole[roleName], txns...)
|
||||
}
|
||||
}
|
||||
|
||||
tuples := make([]*openfgav1.TupleKey, 0)
|
||||
for roleName, transactions := range transactionsByRole {
|
||||
for roleName, transactions := range provider.registry.GetManagedRoleTransactions() {
|
||||
for _, txn := range transactions {
|
||||
typeable := authtypes.MustNewTypeableFromType(txn.Object.Resource.Type, txn.Object.Resource.Name)
|
||||
txnTuples, err := typeable.Tuples(
|
||||
@@ -395,7 +380,7 @@ func (provider *provider) deleteTuples(ctx context.Context, roleName string, org
|
||||
subject := authtypes.MustNewSubject(authtypes.TypeableRole, roleName, orgID, &authtypes.RelationAssignee)
|
||||
|
||||
tuples := make([]*openfgav1.TupleKey, 0)
|
||||
for _, objectType := range provider.getUniqueTypes() {
|
||||
for _, objectType := range provider.registry.GetUniqueTypes() {
|
||||
typeTuples, err := provider.ReadTuples(ctx, &openfgav1.ReadRequestTupleKey{
|
||||
User: subject,
|
||||
Object: objectType.StringValue() + ":",
|
||||
@@ -425,27 +410,3 @@ func (provider *provider) deleteTuples(ctx context.Context, roleName string, org
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) getUniqueTypes() []authtypes.Type {
|
||||
seen := make(map[string]struct{})
|
||||
uniqueTypes := make([]authtypes.Type, 0)
|
||||
for _, register := range provider.registry {
|
||||
for _, typeable := range register.MustGetTypeables() {
|
||||
typeKey := typeable.Type().StringValue()
|
||||
if _, ok := seen[typeKey]; ok {
|
||||
continue
|
||||
}
|
||||
seen[typeKey] = struct{}{}
|
||||
uniqueTypes = append(uniqueTypes, typeable.Type())
|
||||
}
|
||||
}
|
||||
for _, typeable := range provider.MustGetTypeables() {
|
||||
typeKey := typeable.Type().StringValue()
|
||||
if _, ok := seen[typeKey]; ok {
|
||||
continue
|
||||
}
|
||||
seen[typeKey] = struct{}{}
|
||||
uniqueTypes = append(uniqueTypes, typeable.Type())
|
||||
}
|
||||
|
||||
return uniqueTypes
|
||||
}
|
||||
|
||||
@@ -472,7 +472,7 @@ func (module *module) getOrCreateAPIKey(ctx context.Context, orgID valuer.UUID,
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = module.serviceAccount.SetRoleByName(ctx, orgID, serviceAccount.ID, authtypes.SigNozViewerRoleName)
|
||||
err = module.serviceAccount.SetRoleByName(ctx, orgID, serviceAccount.ID, authtypes.SigNozViewerRoleName, serviceaccount.WithSkipEscalationGuard())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -217,28 +217,6 @@ func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valu
|
||||
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, isAdmin, lock)
|
||||
}
|
||||
|
||||
func (module *module) MustGetTypeables() []authtypes.Typeable {
|
||||
return module.pkgDashboardModule.MustGetTypeables()
|
||||
}
|
||||
|
||||
func (module *module) MustGetManagedRoleTransactions() map[string][]*authtypes.Transaction {
|
||||
return map[string][]*authtypes.Transaction{
|
||||
authtypes.SigNozAnonymousRoleName: {
|
||||
{
|
||||
ID: valuer.GenerateUUID(),
|
||||
Relation: authtypes.RelationRead,
|
||||
Object: *authtypes.MustNewObject(
|
||||
authtypes.Resource{
|
||||
Type: authtypes.TypeMetaResource,
|
||||
Name: dashboardtypes.TypeableMetaResourcePublicDashboard.Name(),
|
||||
},
|
||||
authtypes.MustNewSelector(authtypes.TypeMetaResource, "*"),
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (module *module) deletePublic(ctx context.Context, _ valuer.UUID, dashboardID valuer.UUID) error {
|
||||
return module.store.DeletePublic(ctx, dashboardID.StringValue())
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/serviceaccounttypes"
|
||||
@@ -141,7 +142,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationServiceAccount(ctx context.Cont
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration service account: %w", err))
|
||||
}
|
||||
err = ah.Signoz.Modules.ServiceAccount.SetRoleByName(ctx, orgId, cloudIntegrationServiceAccount.ID, authtypes.SigNozViewerRoleName)
|
||||
err = ah.Signoz.Modules.ServiceAccount.SetRoleByName(ctx, orgId, cloudIntegrationServiceAccount.ID, authtypes.SigNozViewerRoleName, serviceaccount.WithSkipEscalationGuard())
|
||||
if err != nil {
|
||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration service account: %w", err))
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"react",
|
||||
"react-perf",
|
||||
"typescript",
|
||||
"unicorn",
|
||||
"jsx-a11y",
|
||||
"import",
|
||||
"jest",
|
||||
@@ -205,8 +206,6 @@
|
||||
"@typescript-eslint/explicit-function-return-type": "error",
|
||||
// Requires explicit return types on functions
|
||||
"@typescript-eslint/no-var-requires": "error",
|
||||
"@typescript-eslint/no-useless-default-assignment": "off", // provide unsafe fixes in our codebase due to bad typing
|
||||
"@typescript-eslint/no-duplicate-type-constituents": "off", // provide fixes that breaks some assumptions, eg: type A = L, B = L, C = A | B (removes B)
|
||||
// Disallows require() in TypeScript (use import instead)
|
||||
// Disabled - using TypeScript instead
|
||||
"react/jsx-props-no-spreading": "off",
|
||||
@@ -313,6 +312,14 @@
|
||||
"name": "react-redux",
|
||||
"message": "[State mgmt] react-redux is deprecated. Migrate to Zustand, nuqs, or react-query."
|
||||
},
|
||||
{
|
||||
"name": "xstate",
|
||||
"message": "[State mgmt] xstate is deprecated. Migrate to Zustand or react-query."
|
||||
},
|
||||
{
|
||||
"name": "@xstate/react",
|
||||
"message": "[State mgmt] @xstate/react is deprecated. Migrate to Zustand or react-query."
|
||||
},
|
||||
{
|
||||
"name": "react",
|
||||
"importNames": [
|
||||
@@ -331,6 +338,82 @@
|
||||
"react/no-array-index-key": "warn",
|
||||
// TODO: Changed to warn during oxlint migration, should be changed to error,
|
||||
|
||||
"unicorn/error-message": "warn",
|
||||
"unicorn/escape-case": "warn",
|
||||
"unicorn/new-for-builtins": "warn",
|
||||
"unicorn/no-abusive-eslint-disable": "warn",
|
||||
"unicorn/no-console-spaces": "warn",
|
||||
"unicorn/no-instanceof-array": "warn",
|
||||
"unicorn/no-invalid-remove-event-listener": "warn",
|
||||
"unicorn/no-new-array": "warn",
|
||||
"unicorn/no-new-buffer": "warn",
|
||||
"unicorn/no-thenable": "warn",
|
||||
"unicorn/no-unreadable-array-destructuring": "warn",
|
||||
"unicorn/no-useless-fallback-in-spread": "warn",
|
||||
"unicorn/no-useless-length-check": "warn",
|
||||
"unicorn/no-useless-promise-resolve-reject": "warn",
|
||||
"unicorn/no-useless-spread": "warn",
|
||||
"unicorn/no-zero-fractions": "warn",
|
||||
"unicorn/number-literal-case": "warn",
|
||||
"unicorn/prefer-array-find": "warn",
|
||||
"unicorn/prefer-array-flat": "warn",
|
||||
"unicorn/prefer-array-flat-map": "warn",
|
||||
"unicorn/prefer-array-index-of": "warn",
|
||||
"unicorn/prefer-array-some": "warn",
|
||||
"unicorn/prefer-at": "warn",
|
||||
"unicorn/prefer-code-point": "warn",
|
||||
"unicorn/prefer-date-now": "warn",
|
||||
"unicorn/prefer-default-parameters": "warn",
|
||||
"unicorn/prefer-includes": "warn",
|
||||
"unicorn/prefer-modern-math-apis": "warn",
|
||||
"unicorn/prefer-native-coercion-functions": "warn",
|
||||
"unicorn/prefer-node-protocol": "off",
|
||||
"unicorn/prefer-number-properties": "warn",
|
||||
"unicorn/prefer-optional-catch-binding": "warn",
|
||||
"unicorn/prefer-regexp-test": "warn",
|
||||
"unicorn/prefer-set-has": "warn",
|
||||
"unicorn/prefer-string-replace-all": "warn",
|
||||
"unicorn/prefer-string-slice": "warn",
|
||||
"unicorn/prefer-string-starts-ends-with": "warn",
|
||||
"unicorn/prefer-string-trim-start-end": "warn",
|
||||
"unicorn/prefer-type-error": "warn",
|
||||
"unicorn/require-array-join-separator": "warn",
|
||||
"unicorn/require-number-to-fixed-digits-argument": "warn",
|
||||
"unicorn/throw-new-error": "warn",
|
||||
"unicorn/consistent-function-scoping": "warn",
|
||||
"unicorn/explicit-length-check": "warn",
|
||||
"unicorn/filename-case": [
|
||||
"warn",
|
||||
{
|
||||
"case": "kebabCase"
|
||||
}
|
||||
],
|
||||
"unicorn/no-array-for-each": "warn",
|
||||
"unicorn/no-lonely-if": "warn",
|
||||
"unicorn/no-negated-condition": "warn",
|
||||
"unicorn/no-null": "warn",
|
||||
"unicorn/no-object-as-default-parameter": "warn",
|
||||
"unicorn/no-static-only-class": "warn",
|
||||
"unicorn/no-this-assignment": "warn",
|
||||
"unicorn/no-unreadable-iife": "warn",
|
||||
"unicorn/no-useless-switch-case": "warn",
|
||||
"unicorn/no-useless-undefined": "warn",
|
||||
"unicorn/prefer-add-event-listener": "warn",
|
||||
"unicorn/prefer-dom-node-append": "warn",
|
||||
"unicorn/prefer-dom-node-dataset": "warn",
|
||||
"unicorn/prefer-dom-node-remove": "warn",
|
||||
"unicorn/prefer-dom-node-text-content": "warn",
|
||||
"unicorn/prefer-keyboard-event-key": "warn",
|
||||
"unicorn/prefer-math-trunc": "warn",
|
||||
"unicorn/prefer-modern-dom-apis": "warn",
|
||||
"unicorn/prefer-negative-index": "warn",
|
||||
"unicorn/prefer-prototype-methods": "warn",
|
||||
"unicorn/prefer-query-selector": "warn",
|
||||
"unicorn/prefer-reflect-apply": "warn",
|
||||
"unicorn/prefer-set-size": "warn",
|
||||
"unicorn/prefer-spread": "warn",
|
||||
"unicorn/prefer-ternary": "warn",
|
||||
"unicorn/require-post-message-target-origin": "warn",
|
||||
"oxc/bad-array-method-on-arguments": "error",
|
||||
"oxc/bad-bitwise-operator": "error",
|
||||
"oxc/bad-comparison-sequence": "error",
|
||||
@@ -455,8 +538,8 @@
|
||||
"sonarjs/no-nested-template-literals": "error",
|
||||
// Avoids nested template literals
|
||||
"sonarjs/no-redundant-boolean": "warn", // TODO: Change to error after migration
|
||||
// Removes redundant boolean literals - turned off because it clashes with unicorn rules
|
||||
"sonarjs/no-redundant-jump": "off",
|
||||
// Removes redundant boolean literals
|
||||
"sonarjs/no-redundant-jump": "error",
|
||||
// Removes unnecessary returns/continues
|
||||
"sonarjs/no-same-line-conditional": "error",
|
||||
// Prevents same-line conditionals
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"@visx/shape": "3.5.0",
|
||||
"@visx/tooltip": "3.3.0",
|
||||
"@vitejs/plugin-react": "5.1.4",
|
||||
"@xstate/react": "^3.0.0",
|
||||
"ansi-to-html": "0.7.2",
|
||||
"antd": "5.11.0",
|
||||
"antd-table-saveas-excel": "2.2.1",
|
||||
@@ -145,6 +146,7 @@
|
||||
"vite": "npm:rolldown-vite@7.3.1",
|
||||
"vite-plugin-html": "3.2.2",
|
||||
"web-vitals": "^0.2.4",
|
||||
"xstate": "^4.31.0",
|
||||
"zod": "4.3.6",
|
||||
"zustand": "5.0.11"
|
||||
},
|
||||
@@ -212,9 +214,9 @@
|
||||
"msw": "1.3.2",
|
||||
"npm-run-all": "latest",
|
||||
"orval": "7.18.0",
|
||||
"oxfmt": "0.46.0",
|
||||
"oxlint": "1.61.0",
|
||||
"oxlint-tsgolint": "0.21.1",
|
||||
"oxfmt": "0.41.0",
|
||||
"oxlint": "1.59.0",
|
||||
"oxlint-tsgolint": "0.20.0",
|
||||
"portfinder-sync": "^0.0.2",
|
||||
"postcss": "8.5.6",
|
||||
"postcss-scss": "4.0.9",
|
||||
@@ -239,7 +241,7 @@
|
||||
"lint-staged": {
|
||||
"*.(js|jsx|ts|tsx)": [
|
||||
"oxfmt --check",
|
||||
"oxlint --quiet",
|
||||
"oxlint --fix",
|
||||
"sh scripts/typecheck-staged.sh"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -16,6 +16,5 @@
|
||||
"roles": "Roles",
|
||||
"role_details": "Role Details",
|
||||
"members": "Members",
|
||||
"service_accounts": "Service Accounts",
|
||||
"mcp_server": "MCP Server"
|
||||
"service_accounts": "Service Accounts"
|
||||
}
|
||||
|
||||
@@ -53,6 +53,5 @@
|
||||
"METER": "SigNoz | Meter",
|
||||
"ROLES_SETTINGS": "SigNoz | Roles",
|
||||
"MEMBERS_SETTINGS": "SigNoz | Members",
|
||||
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
|
||||
"MCP_SERVER": "SigNoz | MCP Server"
|
||||
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts"
|
||||
}
|
||||
|
||||
@@ -16,6 +16,5 @@
|
||||
"roles": "Roles",
|
||||
"role_details": "Role Details",
|
||||
"members": "Members",
|
||||
"service_accounts": "Service Accounts",
|
||||
"mcp_server": "MCP Server"
|
||||
"service_accounts": "Service Accounts"
|
||||
}
|
||||
|
||||
@@ -76,6 +76,5 @@
|
||||
"METER": "SigNoz | Meter",
|
||||
"ROLES_SETTINGS": "SigNoz | Roles",
|
||||
"MEMBERS_SETTINGS": "SigNoz | Members",
|
||||
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts",
|
||||
"MCP_SERVER": "SigNoz | MCP Server"
|
||||
"SERVICE_ACCOUNTS_SETTINGS": "SigNoz | Service Accounts"
|
||||
}
|
||||
|
||||
25
frontend/src/api/globalConfig/getGlobalConfig.ts
Normal file
25
frontend/src/api/globalConfig/getGlobalConfig.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 {
|
||||
GlobalConfigData,
|
||||
GlobalConfigDataProps,
|
||||
} from 'types/api/globalConfig/types';
|
||||
|
||||
const getGlobalConfig = async (): Promise<
|
||||
SuccessResponseV2<GlobalConfigData>
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.get<GlobalConfigDataProps>(`/global/config`);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default getGlobalConfig;
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" height="34" viewBox="0 0 131 34">
|
||||
<path fill="currentColor" d="M.36 8.6h16.7v5.6H6.04c-.2 0-.35.16-.35.35v4.9c0 .2.16.35.35.35h11.02v5.6h-5.33c-.2 0-.35.16-.35.35v4.9c0 .2.16.35.35.35h4.98c.2 0 .35-.15.35-.35V25.4h5.34c.2 0 .35-.16.35-.35v-4.9c0-.2-.16-.35-.35-.35h-5.34v-5.6h5.34c.2 0 .35-.16.35-.35v-4.9c0-.2-.16-.35-.35-.35h-5.34V3.35c0-.2-.16-.35-.35-.35H.36c-.2 0-.36.16-.36.35v4.9c0 .2.16.35.36.35ZM44.41 14.7c-.5-.5-1.1-.9-1.76-1.18a5.62 5.62 0 0 0-4.6.17c-.73.37-1.32.91-1.75 1.62h-.17V8.59H34.1v16.83h2.04v-1.81h.17c.21.36.47.67.77.94.31.25.65.48 1.01.67.37.18.77.31 1.18.39a6.2 6.2 0 0 0 3.39-.24 5.36 5.36 0 0 0 3.02-3.1c.29-.75.44-1.62.44-2.6v-.47c0-.96-.16-1.83-.47-2.58-.3-.75-.7-1.4-1.23-1.9v-.01Zm-5.87.66a3.9 3.9 0 0 1 4.34.84c.36.35.64.8.83 1.3.2.5.3 1.07.3 1.7v.47c0 .64-.1 1.23-.3 1.74a3.75 3.75 0 0 1-2.06 2.15 4.27 4.27 0 0 1-3.12-.03 3.86 3.86 0 0 1-2.09-2.2c-.2-.52-.3-1.11-.3-1.75v-.29c0-.62.1-1.2.3-1.7v-.01c.21-.53.5-.99.84-1.36.36-.37.78-.66 1.26-.86ZM97.04 8.59H95v4.86h-2.94v1.86H95v8.17c0 .56.17 1.03.53 1.4.37.35.84.54 1.4.54h4.18v-1.87h-3.5c-.2 0-.33-.05-.43-.15-.1-.1-.14-.27-.14-.49v-7.6h4.65v-1.86h-4.65V8.59ZM114.61 15a5.48 5.48 0 0 0-1.8-1.33 5.6 5.6 0 0 0-2.57-.56 6.17 6.17 0 0 0-4.26 1.7 5.6 5.6 0 0 0-1.72 4.2v.57c0 .9.15 1.75.44 2.5a5.58 5.58 0 0 0 5.5 3.67c1.55 0 2.8-.35 3.72-1.04a5.35 5.35 0 0 0 1.91-2.73l.03-.07-1.94-.52-.02.07c-.11.33-.27.64-.46.94-.17.27-.4.52-.7.74-.28.22-.63.39-1.04.51-.41.13-.9.19-1.46.19a3.8 3.8 0 0 1-2.84-1.05 4.07 4.07 0 0 1-1.1-2.7h9.68v-1.6c0-.54-.11-1.12-.34-1.75a5.04 5.04 0 0 0-1.03-1.74Zm-8.25 3.21a3.8 3.8 0 0 1 1.22-2.25 4.19 4.19 0 0 1 3.99-.7c.44.16.83.38 1.17.66.34.27.62.62.82 1.02.21.38.34.8.38 1.27h-7.58ZM129.09 14.42a4.47 4.47 0 0 0-3.37-1.3c-.93 0-1.73.2-2.4.59-.64.39-1.15.97-1.52 1.74h-.17v-2h-2.04v11.97h2.04v-6.23c0-1.26.32-2.28.95-3.02a3.31 3.31 0 0 1 2.65-1.14c.94 0 1.7.3 2.24.9.56.6.83 1.52.83 2.74v6.75h2.04v-7.13c0-1.71-.42-3.02-1.25-3.87ZM88.1 15a5.48 5.48 0 0 0-1.78-1.33 5.6 5.6 0 0 0-2.58-.56 6.17 6.17 0 0 0-4.27 1.7 5.59 5.59 0 0 0-1.71 4.2v.56c0 .92.14 1.76.44 2.51a5.6 5.6 0 0 0 5.5 3.67c1.55 0 2.8-.35 3.72-1.04a5.36 5.36 0 0 0 1.91-2.73l.03-.07-1.94-.52-.03.07c-.1.32-.26.64-.45.94-.17.27-.4.52-.7.74-.29.21-.64.39-1.05.51-.4.12-.9.19-1.45.19a3.8 3.8 0 0 1-2.85-1.05 4.07 4.07 0 0 1-1.09-2.7h9.68v-1.61c0-.53-.12-1.12-.34-1.74A5.03 5.03 0 0 0 88.1 15Zm-8.24 3.21a3.83 3.83 0 0 1 1.22-2.25 4.2 4.2 0 0 1 3.99-.7c.44.16.83.38 1.16.66.35.27.62.62.83 1.02.2.38.33.8.37 1.27h-7.57ZM73.65 19.42a6.11 6.11 0 0 0-3.23-1.02 6.63 6.63 0 0 1-2.68-.58c-.47-.3-.7-.7-.7-1.25 0-.27.08-.5.21-.7.14-.2.33-.38.56-.52a4.05 4.05 0 0 1 1.78-.42c.85 0 1.54.21 2.06.63.53.41.83 1 .91 1.73l.01.1 1.95-.47-.01-.07c-.07-.45-.22-.91-.45-1.36a3.46 3.46 0 0 0-.94-1.2 4.6 4.6 0 0 0-1.52-.84 6.05 6.05 0 0 0-2.1-.34c-.58 0-1.13.07-1.65.22-.52.14-1 .36-1.43.65-.41.3-.74.66-.99 1.1a2.9 2.9 0 0 0-.37 1.49v.14c0 1.06.38 1.87 1.14 2.42a6.2 6.2 0 0 0 3.24.97c1.18.08 2.04.26 2.56.54.5.28.75.72.75 1.36 0 .6-.25 1.06-.78 1.4-.52.32-1.22.48-2.1.48a3.68 3.68 0 0 1-2.46-.79 3.13 3.13 0 0 1-1.04-2.14v-.09l-1.93.46h-.02v.07a4.4 4.4 0 0 0 3.1 4c.7.24 1.52.36 2.46.36.7 0 1.35-.09 1.93-.27.6-.16 1.12-.4 1.53-.72A3.38 3.38 0 0 0 74.8 22v-.14c0-1.07-.39-1.9-1.14-2.44ZM60.25 23.4c-.1-.1-.14-.27-.14-.49v-9.46h-2.05v1.85h-.16a3.78 3.78 0 0 0-1.61-1.63 4.62 4.62 0 0 0-2.26-.56c-.77 0-1.5.14-2.19.41a5.27 5.27 0 0 0-3.02 3.12c-.29.75-.44 1.63-.44 2.6v.38c0 .99.15 1.87.44 2.63.3.75.7 1.4 1.2 1.93a5.48 5.48 0 0 0 4.05 1.57c.8 0 1.51-.2 2.2-.58.69-.38 1.24-.97 1.63-1.75h.16v.06c0 .56.18 1.03.53 1.4.37.35.85.54 1.41.54h1.36v-1.87h-.68c-.2 0-.34-.05-.43-.15Zm-4.46.13c-.46.2-.97.3-1.52.3a3.68 3.68 0 0 1-2.75-1.09 4.42 4.42 0 0 1-1.05-3.12v-.38c0-.62.1-1.2.29-1.71a3.65 3.65 0 0 1 5-2.17c.47.2.88.49 1.21.86.35.37.62.83.8 1.36.2.5.3 1.08.3 1.7v.3c0 .63-.1 1.23-.3 1.76-.18.5-.45.96-.78 1.33-.33.37-.73.66-1.2.86Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,13 +0,0 @@
|
||||
<svg width="109" height="24" viewBox="0 0 109 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_125_22125)">
|
||||
<path d="M0 -2.08616e-07V24H17.9352C22.9911 24 26.0999 21.0858 26.0999 17.04V6.96C26.0999 2.91432 22.9911 -2.08616e-07 17.9352 -2.08616e-07H0ZM6.76413 5.82864H19.1992V18.1714H6.76413V5.82864Z" fill="currentColor"/>
|
||||
<path d="M46.7659 18.6172H35.0824V14.16H46.7659V18.6172ZM46.595 5.38296V9.5658H35.0824V5.38296H46.595ZM50.2846 12.1373V11.5886C52.5734 10.5258 53.7008 8.8458 53.7008 6.13728C53.7008 2.64012 50.9337 0.000116183 45.5361 0.000116183H28.3184V24H45.7752C51.1728 24 53.9399 21.8401 53.9399 18.0685C53.9399 15.0172 52.6418 13.2001 50.2846 12.1373Z" fill="currentColor"/>
|
||||
<path d="M62.397 18.1714H74.8319V5.82864H62.397V18.1714ZM63.6609 24C58.6049 24 55.4961 21.0858 55.4961 17.04V6.96012C55.4961 2.91432 58.6049 0.000116183 63.6609 0.000116183H73.568C78.6238 0.000116183 81.7326 2.91432 81.7326 6.96012V17.04C81.7326 21.0858 78.6238 24 73.568 24H63.6609Z" fill="currentColor"/>
|
||||
<path d="M101.66 15.12L90.8995 14.3658C85.5361 13.9886 83.418 11.1772 83.418 7.47432V6.96012C83.418 2.91432 86.5266 0.000116183 91.5827 0.000116183H100.157C105.214 0.000116183 108.323 2.91432 108.323 6.96012V7.98864H101.968V5.14284H90.2504V8.43432L100.601 9.18864C105.999 9.56568 108.493 12.8572 108.493 16.5257V17.04C108.493 20.7428 105.384 24 100.328 24H91.5827C86.5266 24 83.418 20.7428 83.418 17.04V16.0115H89.7722V18.8572H101.66V15.12Z" fill="currentColor"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_125_22125">
|
||||
<rect width="108.494" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1 +0,0 @@
|
||||
<svg width='234' height='42' viewBox='0 0 234 42' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M18 30H6V18H18V30Z' fill='#CFCECD'/><path d='M18 12H6V30H18V12ZM24 36H0V6H24V36Z' fill='#656363'/><path d='M48 30H36V18H48V30Z' fill='#CFCECD'/><path d='M36 30H48V12H36V30ZM54 36H36V42H30V6H54V36Z' fill='#656363'/><path d='M84 24V30H66V24H84Z' fill='#CFCECD'/><path d='M84 24H66V30H84V36H60V6H84V24ZM66 18H78V12H66V18Z' fill='#656363'/><path d='M108 36H96V18H108V36Z' fill='#CFCECD'/><path d='M108 12H96V36H90V6H108V12ZM114 36H108V12H114V36Z' fill='#656363'/><path d='M144 30H126V18H144V30Z' fill='#CFCECD'/><path d='M144 12H126V30H144V36H120V6H144V12Z' fill='#211E1E'/><path d='M168 30H156V18H168V30Z' fill='#CFCECD'/><path d='M168 12H156V30H168V12ZM174 36H150V6H174V36Z' fill='#211E1E'/><path d='M198 30H186V18H198V30Z' fill='#CFCECD'/><path d='M198 12H186V30H198V12ZM204 36H180V6H198V0H204V36Z' fill='#211E1E'/><path d='M234 24V30H216V24H234Z' fill='#CFCECD'/><path d='M216 12V18H228V12H216ZM234 24H216V30H234V36H210V6H234V24Z' fill='#211E1E'/></svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -74,7 +74,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background: radial-gradient(circle, var(--l1-foreground) 10%, transparent 0);
|
||||
background: radial-gradient(circle, #fff 10%, transparent 0);
|
||||
background-size: 12px 12px;
|
||||
opacity: 1;
|
||||
|
||||
|
||||
@@ -99,6 +99,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.auth-error-container {
|
||||
.error-content {
|
||||
&__error-code {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
&__error-message {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__message-badge-label-text {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
&__message-item {
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&::before {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
&__scroll-hint-text {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes horizontal-shaking {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
|
||||
@@ -87,3 +87,23 @@
|
||||
background: var(--l3-background);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.auth-footer-content {
|
||||
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.auth-footer-icon {
|
||||
filter: brightness(0) saturate(100%) invert(25%) sepia(8%) saturate(518%)
|
||||
hue-rotate(192deg) brightness(80%) contrast(95%);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.auth-footer-text {
|
||||
color: var(--text-neutral-light-200);
|
||||
}
|
||||
|
||||
.auth-footer-link-icon {
|
||||
color: var(--text-neutral-light-100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,3 +143,28 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.bg-dot-pattern {
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
var(--l3-background) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 12px 12px;
|
||||
}
|
||||
|
||||
.auth-page-gradient {
|
||||
background: radial-gradient(
|
||||
ellipse at center top,
|
||||
color-mix(in srgb, var(--primary-background) 12%, transparent) 0%,
|
||||
transparent 60%
|
||||
);
|
||||
opacity: 0.8;
|
||||
filter: blur(200px);
|
||||
|
||||
@media (min-width: 768px) {
|
||||
filter: blur(300px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: linear-gradient(0deg, transparent 0%, transparent 100%), #0b0c0e;
|
||||
|
||||
.ant-card-body {
|
||||
height: 100%;
|
||||
@@ -237,7 +238,18 @@
|
||||
height: 2px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
background-color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.celery-task-graph-grid-container {
|
||||
.celery-task-graph-worker-count {
|
||||
background: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.configure-option-Info {
|
||||
border: 1px dashed var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ function CreateServiceAccountModal(): JSX.Element {
|
||||
SA_QUERY_PARAMS.CREATE_SA,
|
||||
parseAsBoolean.withDefault(false),
|
||||
);
|
||||
const [, setSelectedAccountId] = useQueryState(SA_QUERY_PARAMS.ACCOUNT);
|
||||
|
||||
const { showErrorModal, isErrorModalVisible } = useErrorModal();
|
||||
|
||||
@@ -51,12 +50,11 @@ function CreateServiceAccountModal(): JSX.Element {
|
||||
const { mutate: createServiceAccount, isLoading: isSubmitting } =
|
||||
useCreateServiceAccount({
|
||||
mutation: {
|
||||
onSuccess: async (response) => {
|
||||
onSuccess: async () => {
|
||||
toast.success('Service account created successfully');
|
||||
reset();
|
||||
await setIsOpen(null);
|
||||
await invalidateListServiceAccounts(queryClient);
|
||||
await setSelectedAccountId(response.data.id);
|
||||
},
|
||||
onError: (err) => {
|
||||
const errMessage = convertToApiError(
|
||||
@@ -69,7 +67,7 @@ function CreateServiceAccountModal(): JSX.Element {
|
||||
|
||||
function handleClose(): void {
|
||||
reset();
|
||||
void setIsOpen(null);
|
||||
setIsOpen(null);
|
||||
}
|
||||
|
||||
function handleCreate(values: FormValues): void {
|
||||
|
||||
@@ -77,11 +77,11 @@
|
||||
width: 280px;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l1-foreground);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:focus::placeholder {
|
||||
color: rgba($color: var(--l1-foreground), $alpha: 0.4);
|
||||
color: rgba($color: #ffffff, $alpha: 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.time-options-container {
|
||||
.time-options-item {
|
||||
&.active {
|
||||
background-color: rgba($color: #ffffff, $alpha: 0.2);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba($color: #ffffff, $alpha: 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba($color: #ffffff, $alpha: 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.timeSelection-input {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
padding-left: 0px !important;
|
||||
|
||||
input::placeholder {
|
||||
color: var(---bg-ink-300);
|
||||
}
|
||||
|
||||
input:focus::placeholder {
|
||||
color: rgba($color: #000000, $alpha: 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.date-time-popover__footer {
|
||||
border-top: 1px solid var(--l1-border);
|
||||
padding: 8px 14px;
|
||||
@@ -264,3 +300,34 @@
|
||||
background: color-mix(in srgb, var(--bg-robin-200) 8%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.timezone-container {
|
||||
.timezone {
|
||||
background: rgb(179 179 179 / 15%);
|
||||
&__icon {
|
||||
stroke: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-time-picker {
|
||||
.timeSelection-input {
|
||||
&:hover {
|
||||
border-color: var(--l1-border) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.timezone-badge {
|
||||
background: rgb(179 179 179 / 15%);
|
||||
}
|
||||
|
||||
.time-input-suffix-icon-badge {
|
||||
background: rgb(179 179 179 / 15%);
|
||||
|
||||
&:hover {
|
||||
background: rgb(179 179 179 / 20%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,3 +129,20 @@ $item-spacing: 8px;
|
||||
width: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.timezone-picker {
|
||||
&__search {
|
||||
.search-icon {
|
||||
stroke: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
.timezone-name-wrapper {
|
||||
&__selected-icon {
|
||||
.check-icon {
|
||||
stroke: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
.dropdown-button {
|
||||
color: var(--l1-foreground);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dropdown-button--dark {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { EllipsisOutlined } from '@ant-design/icons';
|
||||
import { Button, Dropdown, MenuProps } from 'antd';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
import './DropDown.styles.scss';
|
||||
|
||||
@@ -11,6 +12,8 @@ function DropDown({
|
||||
element: JSX.Element[];
|
||||
onDropDownItemClick?: MenuProps['onClick'];
|
||||
}): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const items: MenuProps['items'] = element.map(
|
||||
(e: JSX.Element, index: number) => ({
|
||||
label: e,
|
||||
@@ -32,7 +35,7 @@ function DropDown({
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
className={`dropdown-button`}
|
||||
className={!isDarkMode ? 'dropdown-button--dark' : 'dropdown-button'}
|
||||
onClick={(e): void => {
|
||||
e.preventDefault();
|
||||
setDdOpen(true);
|
||||
|
||||
@@ -10,7 +10,6 @@ import cx from 'classnames';
|
||||
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
||||
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
|
||||
import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -47,7 +46,6 @@ import {
|
||||
TextSelect,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
@@ -81,10 +79,6 @@ function LogDetailInner({
|
||||
const [selectedView, setSelectedView] = useState<VIEWS>(selectedTab);
|
||||
|
||||
const [isFilterVisible, setIsFilterVisible] = useState<boolean>(false);
|
||||
const { featureFlags } = useAppContext();
|
||||
const isBodyJsonQueryEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.BODY_JSON_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
const [filters, setFilters] = useState<TagFilter | null>(null);
|
||||
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||
@@ -214,29 +208,11 @@ function LogDetailInner({
|
||||
}
|
||||
};
|
||||
|
||||
const logBody = useMemo(() => {
|
||||
if (!isBodyJsonQueryEnabled) {
|
||||
return log?.body || '';
|
||||
}
|
||||
|
||||
try {
|
||||
const json = JSON.parse(log?.body || '');
|
||||
|
||||
if (typeof json?.message === 'string' && json.message !== '') {
|
||||
return json.message;
|
||||
}
|
||||
|
||||
return log?.body || '';
|
||||
} catch (error) {
|
||||
return log?.body || '';
|
||||
}
|
||||
}, [isBodyJsonQueryEnabled, log?.body]);
|
||||
|
||||
const htmlBody = useMemo(
|
||||
() => ({
|
||||
__html: getSanitizedLogBody(logBody || '', { shouldEscapeHtml: true }),
|
||||
__html: getSanitizedLogBody(log?.body || '', { shouldEscapeHtml: true }),
|
||||
}),
|
||||
[logBody],
|
||||
[log?.body],
|
||||
);
|
||||
|
||||
const handleJSONCopy = (): void => {
|
||||
@@ -442,7 +418,7 @@ function LogDetailInner({
|
||||
<div className="log-detail-drawer__log">
|
||||
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
|
||||
<Tooltip
|
||||
title={removeEscapeCharacters(logBody)}
|
||||
title={removeEscapeCharacters(log?.body)}
|
||||
placement="left"
|
||||
mouseLeaveDelay={0}
|
||||
>
|
||||
|
||||
@@ -196,3 +196,17 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.members-table {
|
||||
.ant-table-tbody {
|
||||
> tr.members-table-row--tinted > td {
|
||||
background: rgba(0, 0, 0, 0.015);
|
||||
}
|
||||
|
||||
> tr:hover > td {
|
||||
background: rgba(0, 0, 0, 0.03) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,3 +167,22 @@
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.config-btn {
|
||||
&.missing-config-btn {
|
||||
background: var(--bg-amber-100);
|
||||
color: var(--bg-amber-500);
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-amber-600) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.missing-config-btn {
|
||||
.config-btn-content {
|
||||
border-right: 1px solid var(--bg-amber-600);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ $custom-border-color: #2c3044;
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
color: color-mix(in srgb, var(--l2-foreground) 45%, transparent);
|
||||
color: color-mix(in srgb, var(--border) 45%, transparent);
|
||||
}
|
||||
|
||||
// Base styles are for dark mode
|
||||
@@ -48,6 +48,10 @@ $custom-border-color: #2c3044;
|
||||
visibility: visible !important;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
|
||||
.lightMode & {
|
||||
color: rgba(0, 0, 0, 0.85) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.ant-select-focused .ant-select-selection-placeholder {
|
||||
@@ -63,6 +67,10 @@ $custom-border-color: #2c3044;
|
||||
color: var(--l2-foreground);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
|
||||
.lightMode & {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-selector {
|
||||
@@ -106,7 +114,7 @@ $custom-border-color: #2c3044;
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
color: color-mix(in srgb, var(--l2-foreground) 45%, transparent);
|
||||
color: color-mix(in srgb, var(--border) 45%, transparent);
|
||||
}
|
||||
|
||||
// Customize tags in multiselect (dark mode by default)
|
||||
@@ -209,7 +217,7 @@ $custom-border-color: #2c3044;
|
||||
.empty-message {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
color: color-mix(in srgb, var(--l1-foreground) 45%, transparent);
|
||||
color: color-mix(in srgb, var(--border) 45%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -567,7 +575,7 @@ $custom-border-color: #2c3044;
|
||||
.empty-message {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
color: color-mix(in srgb, var(--l1-foreground) 45%, transparent);
|
||||
color: color-mix(in srgb, var(--border) 45%, transparent);
|
||||
}
|
||||
|
||||
.status-message {
|
||||
@@ -975,6 +983,10 @@ $custom-border-color: #2c3044;
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
visibility 0.2s ease;
|
||||
|
||||
.lightMode & {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-within .all-text {
|
||||
|
||||
@@ -249,6 +249,57 @@
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.query-aggregation-container {
|
||||
.aggregation-container {
|
||||
.query-aggregation-select-container {
|
||||
.query-aggregation-select-editor {
|
||||
.cm-editor {
|
||||
.cm-tooltip-autocomplete {
|
||||
background: var(--l1-background) !important;
|
||||
border: 1px solid var(--l1-border) !important;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
|
||||
backdrop-filter: none;
|
||||
|
||||
ul {
|
||||
li {
|
||||
&:hover,
|
||||
&[aria-selected='true'] {
|
||||
background: var(--l3-background) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cm-line {
|
||||
::-moz-selection {
|
||||
background: var(--l2-background) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--l1-background) !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
.cm-function {
|
||||
color: var(--primary-background) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.query-aggregation-error-popover {
|
||||
.ant-popover-inner {
|
||||
background-color: var(--l1-background);
|
||||
border: none;
|
||||
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.query-aggregation-error-popover {
|
||||
.ant-popover-inner {
|
||||
background-color: var(--l1-border);
|
||||
|
||||
@@ -121,3 +121,44 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.qb-trace-operator {
|
||||
&-arrow {
|
||||
&::before {
|
||||
background: repeating-linear-gradient(
|
||||
to right,
|
||||
var(--l3-background),
|
||||
var(--l3-background) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
&::after {
|
||||
background-color: var(--l3-background);
|
||||
}
|
||||
}
|
||||
&.non-list-view {
|
||||
&::before {
|
||||
background: repeating-linear-gradient(
|
||||
to bottom,
|
||||
var(--l3-background),
|
||||
var(--l3-background) 4px,
|
||||
transparent 4px,
|
||||
transparent 8px
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&-label-with-input {
|
||||
border: 1px solid var(--l1-border) !important;
|
||||
background: var(--l1-background) !important;
|
||||
|
||||
.label {
|
||||
color: var(--l1-foreground) !important;
|
||||
border-right: 1px solid var(--l1-border) !important;
|
||||
background: var(--l1-background) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background: radial-gradient(circle, var(--l1-foreground) 10%, transparent 0);
|
||||
background: radial-gradient(circle, #fff 10%, transparent 0);
|
||||
background-size: 12px 12px;
|
||||
opacity: 1;
|
||||
|
||||
|
||||
@@ -12,12 +12,6 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__tab-group {
|
||||
[data-slot='toggle-group'] {
|
||||
border-radius: 2px;
|
||||
|
||||
@@ -197,3 +197,17 @@
|
||||
background-color: var(--l1-border);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.sa-table {
|
||||
.ant-table-tbody {
|
||||
> tr.sa-table-row--tinted > td {
|
||||
background: rgba(0, 0, 0, 0.015);
|
||||
}
|
||||
|
||||
> tr:hover > td {
|
||||
background: rgba(0, 0, 0, 0.03) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,12 +147,12 @@
|
||||
left: 50%;
|
||||
width: 4px;
|
||||
transform: translateX(-50%);
|
||||
background: var(--l2-background);
|
||||
opacity: 1;
|
||||
pointer-events: none;
|
||||
transition:
|
||||
background 120ms ease,
|
||||
width 120ms ease;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.cursorColResize:hover .tanstackResizeHandleLine {
|
||||
|
||||
@@ -186,3 +186,29 @@
|
||||
letter-spacing: -0.06px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.warning-content {
|
||||
&__warning-code {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
&__warning-message {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
&__message-item {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
&__message-badge {
|
||||
&-label-text {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
.key-value-label__value {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
&__docs-button {
|
||||
background: var(--l1-background);
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@ export enum Events {
|
||||
UPDATE_GRAPH_MANAGER_TABLE = 'UPDATE_GRAPH_MANAGER_TABLE',
|
||||
TABLE_COLUMNS_DATA = 'TABLE_COLUMNS_DATA',
|
||||
SLOW_API_WARNING = 'SLOW_API_WARNING',
|
||||
TOOLTIP_PINNED = 'TOOLTIP_PINNED',
|
||||
TOOLTIP_UNPINNED = 'TOOLTIP_UNPINNED',
|
||||
TOOLTIP_CONTENT_SCROLLED = 'TOOLTIP_CONTENT_SCROLLED',
|
||||
}
|
||||
|
||||
export enum InfraMonitoringEvents {
|
||||
|
||||
@@ -9,5 +9,4 @@ export enum FeatureKeys {
|
||||
ANOMALY_DETECTION = 'anomaly_detection',
|
||||
ONBOARDING_V3 = 'onboarding_v3',
|
||||
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
|
||||
BODY_JSON_ENABLED = 'body_json_enabled',
|
||||
}
|
||||
|
||||
@@ -87,7 +87,6 @@ const ROUTES = {
|
||||
HOME_PAGE: '/',
|
||||
PUBLIC_DASHBOARD: '/public/dashboard/:dashboardId',
|
||||
SERVICE_ACCOUNTS_SETTINGS: '/settings/service-accounts',
|
||||
MCP_SERVER: '/settings/mcp-server',
|
||||
} as const;
|
||||
|
||||
export default ROUTES;
|
||||
|
||||
@@ -157,3 +157,9 @@
|
||||
.view-all-drawer {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.ant-table {
|
||||
background: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,3 +226,54 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.api-quick-filter-left-section {
|
||||
.api-quick-filters-header {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
.api-module-right-section {
|
||||
.toolbar {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
.no-filtered-domains-message-container {
|
||||
.no-filtered-domains-message-content {
|
||||
.no-filtered-domains-message {
|
||||
.no-domain-title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.no-domain-subtitle {
|
||||
color: var(--l2-foreground);
|
||||
|
||||
.attribute {
|
||||
font-family: 'Space Mono';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.api-monitoring-domain-list-table {
|
||||
.ant-table {
|
||||
.ant-table-cell {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.table-row-light {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.table-row-dark {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.round-metric-tag {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
color: var(--l1-foreground);
|
||||
color: #fff;
|
||||
font-family: Inter;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
@@ -463,3 +463,135 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.dashboard-description-container {
|
||||
color: var(--l1-foreground);
|
||||
|
||||
.dashboard-details {
|
||||
.left-section {
|
||||
.dashboard-title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.right-section {
|
||||
.icons {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.configure-button {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-description-section {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
.dashboard-settings {
|
||||
.ant-popover-inner {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background) !important;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.section-1 {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.ant-btn {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
.section-2 {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.ant-btn {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rename-dashboard {
|
||||
.ant-modal-content {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
|
||||
.ant-modal-header {
|
||||
background: var(--l1-background);
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.ant-modal-title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
.dashboard-content {
|
||||
.name-text {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.dashboard-name-input {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
.dashboard-rename {
|
||||
.cancel-btn {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section-naming {
|
||||
.ant-modal-content {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
|
||||
.ant-modal-header {
|
||||
background: var(--l1-background);
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.ant-modal-title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
.section-naming-content {
|
||||
.name-text {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.section-name-input {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
.dashboard-rename {
|
||||
.cancel-btn {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,3 +141,58 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.overview-content {
|
||||
.overview-settings {
|
||||
border: 1px solid var(--l1-border);
|
||||
|
||||
.name-icon-input {
|
||||
.dashboard-image-input {
|
||||
.ant-select-selector {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-name-input {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-name {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.description-text-area {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
.overview-settings-footer {
|
||||
border-top: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
|
||||
.unsaved {
|
||||
.unsaved-dot {
|
||||
background: var(--primary-background);
|
||||
}
|
||||
.unsaved-changes {
|
||||
color: var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
.footer-action-btns {
|
||||
.discard-btn {
|
||||
color: var(--l1-foreground);
|
||||
background-color: var(--l3-background);
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
color: var(--l3-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,27 @@
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.variable-item {
|
||||
.variable-name {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
color: var(--bg-robin-300);
|
||||
}
|
||||
|
||||
.variable-value {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
outline: 1px solid var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cycle-error-alert {
|
||||
margin-bottom: 12px;
|
||||
padding: 4px 12px;
|
||||
|
||||
@@ -116,3 +116,50 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.panel-type-selection-modal {
|
||||
.ant-modal-content {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
|
||||
.ant-modal-header {
|
||||
background: var(--l1-background);
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.ant-modal-title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
.panel-selection {
|
||||
.selected {
|
||||
background: var(--l2-background);
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
border: 1px solid var(--l1-border);
|
||||
|
||||
.ant-card-body {
|
||||
.ant-typography {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
border-top: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
|
||||
.ant-btn {
|
||||
color: var(--l1-foreground);
|
||||
|
||||
background: var(--primary-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,3 +61,11 @@
|
||||
width: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.dashboard-breadcrumbs {
|
||||
.dashboard-btn {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import uPlot from 'uplot';
|
||||
import { ChartProps } from '../types';
|
||||
|
||||
const TOOLTIP_WIDTH_PADDING = 120;
|
||||
const TOOLTIP_MIN_WIDTH = 300;
|
||||
const TOOLTIP_MIN_WIDTH = 200;
|
||||
|
||||
export default function ChartWrapper({
|
||||
legendConfig = { position: LegendPosition.BOTTOM },
|
||||
|
||||
@@ -104,12 +104,7 @@ export const usePanelContextMenu = ({
|
||||
}
|
||||
|
||||
if (data && data?.record?.queryName) {
|
||||
onClick(data.coord, {
|
||||
...data.record,
|
||||
label: data.label,
|
||||
seriesColor: data.seriesColor,
|
||||
timeRange,
|
||||
});
|
||||
onClick(data.coord, { ...data.record, label: data.label, timeRange });
|
||||
}
|
||||
},
|
||||
[onClick, queryResponse],
|
||||
|
||||
@@ -57,3 +57,32 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.download-logs-popover {
|
||||
.ant-popover-inner {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: linear-gradient(
|
||||
139deg,
|
||||
color-mix(in srgb, var(--card) 80%, transparent) 0%,
|
||||
color-mix(in srgb, var(--card) 90%, transparent) 98.68%
|
||||
);
|
||||
|
||||
box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2);
|
||||
|
||||
.download-logs-content {
|
||||
.action-btns {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
.action-btns:hover {
|
||||
&.ant-btn-text {
|
||||
background-color: var(--l3-background) !important;
|
||||
}
|
||||
}
|
||||
.export-heading {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,41 @@
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.main-container {
|
||||
.plot-tag {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
.ant-modal-content {
|
||||
background-color: var(--l1-foreground);
|
||||
.ant-modal-confirm-title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.ant-modal-confirm-content {
|
||||
.ant-typography {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-confirm-btns {
|
||||
button:nth-of-type(1) {
|
||||
background-color: var(--l3-background);
|
||||
border: none;
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-help-btns {
|
||||
.doc-redirection-btn {
|
||||
color: var(--bg-aqua-600) !important;
|
||||
border-color: var(--bg-aqua-600) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.create-notification-btn {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { createMachine } from 'xstate';
|
||||
|
||||
export const ResourceAttributesFilterMachine =
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QBECGsAWAjA9qgThAAQDKYBAxhkQIIB2xAYgJYA2ALmPgHQAqqUANJgAngGIAcgFEAGr0SgADjljN2zHHQUgAHogAcAFgAM3AOz6ATAEYAzJdsA2Y4cOWAnABoQIxAFpDR2tuQ319AFYTcKdbFycAX3jvNExcAmIySmp6JjZOHn4hUTFNACFWAFd8bWVVdU1tPQQzY1MXY2tDdzNHM3dHd0NvXwR7biMTa313S0i+63DE5PRsPEJScnwqWgYiFg4uPgFhcQAlKRIpeSQQWrUNLRumx3Czbg8TR0sbS31jfUcw38fW47gBHmm4XCVms3SWIBSq3SGyyO1yBx4AHlFFxUOwcPhJLJrkoVPcGk9ENYFuF3i5YR0wtEHECEAEgiEmV8zH1DLYzHZ4Yi0utMltsrt9vluNjcfjCWVKtUbnd6o9QE1rMYBtxbGFvsZ3NrZj1WdYOfotUZLX0XEFHEKViKMpttjk9nlDrL8HiCWJzpcSbcyWrGoh3NCQj0zK53P1ph1WeFLLqnJZ2s5vmZLA6kginWsXaj3VLDoUAGqoSpgEp0cpVGohh5hhDWDy0sz8zruakzamWVm-Qyg362V5-AZOayO1KFlHitEejFHKCV6v+i5XRt1ZuU1s52zjNOOaZfdOWIY+RDZ0Hc6ZmKEXqyLPPCudit2Sz08ACSEFYNbSHI27kuquiIOEjiONwjJgrM3RWJYZisgEIJgnYPTmuEdi2OaiR5nQOAQHA2hvsiH4Sui0qFCcIGhnuLSmP0YJuJ2xjJsmKELG8XZTK0tjdHG06vgW5GupRS7St6vrKqSO4UhqVL8TBWp8o4eqdl0A5Xmy3G6gK56-B4uERDOSKiuJi6lgUAhrhUYB0buimtrEKZBDYrxaS0OZca8+ltheybOI4hivGZzrzp+VGHH+AGOQp4EIHy+ghNYnawtG4TsbYvk8QKfHGAJfQ9uF76WSW37xWBTSGJ0qXpd0vRZdEKGPqC2YeO2-zfO4+HxEAA */
|
||||
createMachine({
|
||||
tsTypes: {} as import('./Labels.machine.typegen').Typegen0,
|
||||
initial: 'Idle',
|
||||
states: {
|
||||
LabelKey: {
|
||||
on: {
|
||||
NEXT: {
|
||||
actions: 'onSelectLabelValue',
|
||||
target: 'LabelValue',
|
||||
},
|
||||
onBlur: {
|
||||
actions: 'onSelectLabelValue',
|
||||
target: 'LabelValue',
|
||||
},
|
||||
RESET: {
|
||||
target: 'Idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
LabelValue: {
|
||||
on: {
|
||||
NEXT: {
|
||||
actions: ['onValidateQuery'],
|
||||
},
|
||||
onBlur: {
|
||||
actions: ['onValidateQuery'],
|
||||
// target: 'Idle',
|
||||
},
|
||||
RESET: {
|
||||
target: 'Idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
Idle: {
|
||||
on: {
|
||||
NEXT: {
|
||||
actions: 'onSelectLabelKey',
|
||||
description: 'Enter a label key',
|
||||
target: 'LabelKey',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
id: 'Label Key Values',
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
// This file was automatically generated. Edits will be overwritten
|
||||
|
||||
export interface Typegen0 {
|
||||
'@@xstate/typegen': true;
|
||||
eventsCausingActions: {
|
||||
onSelectLabelValue: 'NEXT' | 'onBlur';
|
||||
onValidateQuery: 'NEXT' | 'onBlur';
|
||||
onSelectLabelKey: 'NEXT';
|
||||
};
|
||||
internalEvents: {
|
||||
'xstate.init': { type: 'xstate.init' };
|
||||
};
|
||||
invokeSrcNameMap: {};
|
||||
missingImplementations: {
|
||||
actions: 'onSelectLabelValue' | 'onValidateQuery' | 'onSelectLabelKey';
|
||||
services: never;
|
||||
guards: never;
|
||||
delays: never;
|
||||
};
|
||||
eventsCausingServices: {};
|
||||
eventsCausingGuards: {};
|
||||
eventsCausingDelays: {};
|
||||
matchesStates: 'LabelKey' | 'LabelValue' | 'Idle';
|
||||
tags: never;
|
||||
}
|
||||
@@ -4,20 +4,20 @@ import {
|
||||
CloseCircleFilled,
|
||||
ExclamationCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useMachine } from '@xstate/react';
|
||||
import { Button, Input, message, Modal } from 'antd';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { map } from 'lodash-es';
|
||||
import { Labels } from 'types/api/alerts/def';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { ResourceAttributesFilterMachine } from './Labels.machine';
|
||||
import QueryChip from './QueryChip';
|
||||
import { QueryChipItem, SearchContainer } from './styles';
|
||||
import { ILabelRecord } from './types';
|
||||
import { createQuery, flattenLabels, prepareLabels } from './utils';
|
||||
|
||||
type LabelStep = 'Idle' | 'LabelKey' | 'LabelValue';
|
||||
type LabelEvent = 'NEXT' | 'onBlur' | 'RESET';
|
||||
|
||||
interface LabelSelectProps {
|
||||
onSetLabels: (q: Labels) => void;
|
||||
initialValues: Labels | undefined;
|
||||
@@ -35,65 +35,42 @@ function LabelSelect({
|
||||
const [queries, setQueries] = useState<ILabelRecord[]>(
|
||||
initialValues ? flattenLabels(initialValues) : [],
|
||||
);
|
||||
const [step, setStep] = useState<LabelStep>('Idle');
|
||||
|
||||
const dispatchChanges = (updatedRecs: ILabelRecord[]): void => {
|
||||
onSetLabels(prepareLabels(updatedRecs, initialValues));
|
||||
setQueries(updatedRecs);
|
||||
};
|
||||
|
||||
const onSelectLabelValue = (): void => {
|
||||
if (currentVal !== '') {
|
||||
setStaging((prevState) => [...prevState, currentVal]);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
setCurrentVal('');
|
||||
};
|
||||
const [state, send] = useMachine(ResourceAttributesFilterMachine, {
|
||||
actions: {
|
||||
onSelectLabelKey: () => {},
|
||||
onSelectLabelValue: () => {
|
||||
if (currentVal !== '') {
|
||||
setStaging((prevState) => [...prevState, currentVal]);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
setCurrentVal('');
|
||||
},
|
||||
onValidateQuery: (): void => {
|
||||
if (currentVal === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const onValidateQuery = (): void => {
|
||||
if (currentVal === '') {
|
||||
return;
|
||||
}
|
||||
const generatedQuery = createQuery([...staging, currentVal]);
|
||||
|
||||
const generatedQuery = createQuery([...staging, currentVal]);
|
||||
|
||||
if (generatedQuery) {
|
||||
dispatchChanges([...queries, generatedQuery]);
|
||||
setStaging([]);
|
||||
setCurrentVal('');
|
||||
setStep('Idle');
|
||||
}
|
||||
};
|
||||
|
||||
const send = (event: LabelEvent): void => {
|
||||
if (event === 'RESET') {
|
||||
setStep('Idle');
|
||||
return;
|
||||
}
|
||||
if (event === 'NEXT') {
|
||||
if (step === 'Idle') {
|
||||
setStep('LabelKey');
|
||||
} else if (step === 'LabelKey') {
|
||||
onSelectLabelValue();
|
||||
setStep('LabelValue');
|
||||
} else if (step === 'LabelValue') {
|
||||
onValidateQuery();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event === 'onBlur') {
|
||||
if (step === 'LabelKey') {
|
||||
onSelectLabelValue();
|
||||
setStep('LabelValue');
|
||||
} else if (step === 'LabelValue') {
|
||||
onValidateQuery();
|
||||
}
|
||||
}
|
||||
};
|
||||
if (generatedQuery) {
|
||||
dispatchChanges([...queries, generatedQuery]);
|
||||
setStaging([]);
|
||||
setCurrentVal('');
|
||||
send('RESET');
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const handleFocus = (): void => {
|
||||
if (step === 'Idle') {
|
||||
if (state.value === 'Idle') {
|
||||
send('NEXT');
|
||||
}
|
||||
};
|
||||
@@ -102,7 +79,7 @@ function LabelSelect({
|
||||
if (staging.length === 1 && staging[0] !== undefined) {
|
||||
send('onBlur');
|
||||
}
|
||||
}, [staging]);
|
||||
}, [send, staging]);
|
||||
|
||||
useEffect(() => {
|
||||
handleBlur();
|
||||
@@ -138,14 +115,14 @@ function LabelSelect({
|
||||
});
|
||||
};
|
||||
const renderPlaceholder = useCallback((): string => {
|
||||
if (step === 'LabelKey') {
|
||||
if (state.value === 'LabelKey') {
|
||||
return 'Enter a label key then press ENTER.';
|
||||
}
|
||||
if (step === 'LabelValue') {
|
||||
if (state.value === 'LabelValue') {
|
||||
return `Enter a value for label key(${staging[0]}) then press ENTER.`;
|
||||
}
|
||||
return t('placeholder_label_key_pair');
|
||||
}, [t, step, staging]);
|
||||
}, [t, state, staging]);
|
||||
return (
|
||||
<SearchContainer isDarkMode={isDarkMode} disabled={false}>
|
||||
<div style={{ display: 'inline-flex', flexWrap: 'wrap' }}>
|
||||
@@ -171,7 +148,7 @@ function LabelSelect({
|
||||
if (e.key === 'Enter' || e.code === 'Enter' || e.key === ':') {
|
||||
send('NEXT');
|
||||
}
|
||||
if (step === 'Idle') {
|
||||
if (state.value === 'Idle') {
|
||||
send('NEXT');
|
||||
}
|
||||
}}
|
||||
@@ -182,7 +159,7 @@ function LabelSelect({
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
|
||||
{queries.length > 0 || staging.length > 0 || currentVal ? (
|
||||
{queries.length || staging.length || currentVal ? (
|
||||
<Button
|
||||
onClick={handleClearAll}
|
||||
icon={<CloseCircleFilled />}
|
||||
|
||||
@@ -174,3 +174,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.dashboard-empty-state {
|
||||
.dashboard-content {
|
||||
.heading {
|
||||
.icons {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.welcome {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.welcome-info {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,3 +333,100 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.fullscreen-grid-container {
|
||||
.react-grid-layout {
|
||||
.row-panel {
|
||||
background: var(--l2-background);
|
||||
|
||||
.settings-icon {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.row-icon {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.widget-full-view {
|
||||
.ant-modal-content {
|
||||
background-color: var(--l1-foreground);
|
||||
|
||||
.ant-modal-header {
|
||||
background-color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row-settings {
|
||||
.ant-popover-inner {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
|
||||
.menu-content {
|
||||
.section-1 {
|
||||
.rename-btn {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.section-2 {
|
||||
border-top: 1px solid var(--l1-border);
|
||||
.remove-section {
|
||||
color: var(--bg-cherry-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rename-section {
|
||||
.ant-modal-content {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
|
||||
.ant-modal-header {
|
||||
background: var(--l1-background);
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.ant-modal-title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
.typography {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.ant-form-item {
|
||||
.action-btns {
|
||||
.cancel-btn {
|
||||
color: var(--l1-foreground);
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.view-onclick-show-button {
|
||||
background: var(--l2-background);
|
||||
border-color: var(--l1-foreground);
|
||||
color: var(--l1-foreground);
|
||||
|
||||
.menu-item {
|
||||
&:hover {
|
||||
background-color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,14 @@
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.widget-header-container {
|
||||
.ant-input-group-addon {
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.long-tooltip {
|
||||
.ant-tooltip-content {
|
||||
max-height: 500px;
|
||||
|
||||
@@ -482,10 +482,10 @@
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&.selected {
|
||||
background: var(--l3-background);
|
||||
color: var(--primary);
|
||||
background: var(--l2-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import floppyDiscUrl from '@/assets/Icons/floppy-disc.svg';
|
||||
import logsUrl from '@/assets/Icons/logs.svg';
|
||||
|
||||
import { getItemIcon } from '../constants';
|
||||
import { ScrollText } from '@signozhq/icons';
|
||||
|
||||
export default function SavedViews({
|
||||
onUpdateChecklistDoneItem,
|
||||
@@ -351,7 +351,7 @@ export default function SavedViews({
|
||||
className={selectedEntity === 'logs' ? 'selected tab' : 'tab'}
|
||||
onClick={(): void => handleTabChange('logs')}
|
||||
>
|
||||
<ScrollText size={14} />
|
||||
<img src={logsUrl} alt="logs-icon" className="logs-icon" />
|
||||
Logs
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -148,3 +148,50 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.entity-metric-traces-header {
|
||||
.filter-section {
|
||||
border-top: 1px solid var(--l1-border);
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.ant-select-selector {
|
||||
border-color: var(--l1-border) !important;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.entity-metric-traces-table {
|
||||
.ant-table {
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--l1-border);
|
||||
|
||||
.ant-table-thead > tr > th {
|
||||
background: var(--l1-foreground);
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th:has(.entityname-column-header) {
|
||||
background: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
background: var(--l1-foreground);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.ant-table-cell:has(.entityname-column-value) {
|
||||
background: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.entityname-column-value {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +192,63 @@
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.ant-drawer-header {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
background: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.entity-detail-drawer {
|
||||
.title {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.entity-detail-drawer__entity {
|
||||
.ant-typography {
|
||||
color: var(--l2-foreground);
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-foreground);
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.views-tabs {
|
||||
.tab {
|
||||
background: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.selected_view {
|
||||
background: var(--l3-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.selected_view::before {
|
||||
background: var(--l3-background);
|
||||
border-left: 1px solid var(--l1-border);
|
||||
}
|
||||
}
|
||||
|
||||
.compass-button {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-foreground);
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tabs-and-search {
|
||||
.action-btn {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-foreground);
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.entity-metric-traces {
|
||||
margin-top: 1rem;
|
||||
|
||||
@@ -342,6 +399,53 @@
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.entity-metric-traces-header {
|
||||
.filter-section {
|
||||
border-top: 1px solid var(--l1-border);
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
|
||||
.ant-select-selector {
|
||||
border-color: var(--l1-border) !important;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.entity-metric-traces-table {
|
||||
.ant-table {
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--l1-border);
|
||||
|
||||
.ant-table-thead > tr > th {
|
||||
background: var(--l1-foreground);
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th:has(.entityname-column-header) {
|
||||
background: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.ant-table-cell {
|
||||
background: var(--l1-foreground);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.ant-table-cell:has(.entityname-column-value) {
|
||||
background: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.entityname-column-value {
|
||||
color: var(--l3-background);
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.entity-metrics-logs-container {
|
||||
margin-top: 1rem;
|
||||
|
||||
|
||||
@@ -773,6 +773,206 @@
|
||||
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.ingestion-key-container {
|
||||
.ingestion-key-content {
|
||||
.title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.ant-table-row {
|
||||
.ant-table-cell {
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.ant-table-cell {
|
||||
background: var(--l1-background) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.column-render {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
|
||||
.ant-collapse {
|
||||
border: none;
|
||||
|
||||
.ant-collapse-header {
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
border-top: 1px solid var(--l1-border);
|
||||
}
|
||||
}
|
||||
|
||||
.title-with-action {
|
||||
.ingestion-key-title {
|
||||
.ant-typography {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.ingestion-key-value {
|
||||
background: var(--l3-background);
|
||||
|
||||
.ant-typography {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.copy-key-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
.ant-typography {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ingestion-key-details {
|
||||
border-top: 1px solid var(--l1-border);
|
||||
|
||||
.ingestion-key-tag {
|
||||
background: var(--l3-background);
|
||||
|
||||
.tag-text {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.ingestion-key-created-by {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.ingestion-key-last-used-at {
|
||||
.ant-typography {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete-ingestion-key-modal {
|
||||
.ant-modal-content {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
|
||||
.ant-modal-header {
|
||||
background: var(--l1-background);
|
||||
|
||||
.title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
.ant-typography {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.ingestion-key-input {
|
||||
.ant-input {
|
||||
background: var(--l3-background);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
.cancel-btn {
|
||||
background: var(--l3-background);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ingestion-key-info-container {
|
||||
.user-email {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
.limits-data {
|
||||
border: 1px solid var(--l1-border);
|
||||
}
|
||||
}
|
||||
|
||||
.ingestion-key-modal {
|
||||
.ant-modal-content {
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
padding: 0;
|
||||
|
||||
.ant-modal-header {
|
||||
background: none;
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ingestion-key-access-role {
|
||||
.ant-radio-button-wrapper {
|
||||
&.ant-radio-button-wrapper-checked {
|
||||
color: var(--l1-foreground);
|
||||
background: var(--l3-background);
|
||||
border-color: var(--l1-border);
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
background: var(--l3-background);
|
||||
border-color: var(--l1-border);
|
||||
|
||||
&::before {
|
||||
background-color: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: var(--l1-foreground);
|
||||
background: var(--l3-background);
|
||||
border-color: var(--l1-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab {
|
||||
border: 1px solid var(--l1-border);
|
||||
|
||||
&::before {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copyable-text {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
.ingestion-key-expires-at {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.expires-at .ant-picker {
|
||||
border-color: var(--l1-border) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mt-8 {
|
||||
margin-top: 8px;
|
||||
}
|
||||
@@ -885,3 +1085,26 @@
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.ingestion-setup-details-links {
|
||||
background: color-mix(in srgb, var(--primary-background) 10%, transparent);
|
||||
color: var(--accent-primary);
|
||||
|
||||
.learn-more {
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.ingestion-url-error-tooltip {
|
||||
.ingestion-url-error-content {
|
||||
.ingestion-url-error-code {
|
||||
color: var(--bg-amber-500);
|
||||
}
|
||||
}
|
||||
|
||||
.ingestion-url-error-message {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,7 @@ import { initialQueryMeterWithType } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { INITIAL_ALERT_THRESHOLD_STATE } from 'container/CreateAlertV2/context/constants';
|
||||
import dayjs from 'dayjs';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { useGetGlobalConfig } from 'api/generated/services/global';
|
||||
import { useGetGlobalConfig } from 'hooks/globalConfig/useGetGlobalConfig';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { cloneDeep, isNil, isUndefined } from 'lodash-es';
|
||||
@@ -359,8 +358,6 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
error: globalConfigError,
|
||||
} = useGetGlobalConfig();
|
||||
|
||||
const globalConfigApiError = convertToApiError(globalConfigError);
|
||||
|
||||
const { mutate: createIngestionKey, isLoading: isLoadingCreateAPIKey } =
|
||||
useCreateIngestionKey<AxiosError<RenderErrorResponseDTO>>();
|
||||
|
||||
@@ -969,7 +966,7 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
|
||||
<div className="ingestion-key-value">
|
||||
<Typography.Text>
|
||||
{APIKey?.value?.slice(0, 2)}********
|
||||
{APIKey?.value?.substring(0, 2)}********
|
||||
{APIKey?.value
|
||||
?.substring(APIKey?.value?.length ? APIKey.value.length - 2 : 0)
|
||||
?.trim()}
|
||||
@@ -1607,11 +1604,11 @@ function MultiIngestionSettings(): JSX.Element {
|
||||
title={
|
||||
<div className="ingestion-url-error-content">
|
||||
<Typography.Text className="ingestion-url-error-code">
|
||||
{globalConfigApiError?.getErrorCode()}
|
||||
{globalConfigError?.getErrorCode()}
|
||||
</Typography.Text>
|
||||
|
||||
<Typography.Text className="ingestion-url-error-message">
|
||||
{globalConfigApiError?.getErrorMessage()}
|
||||
{globalConfigError?.getErrorMessage()}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
|
||||
.licenses-page-header-title {
|
||||
color: var(--l1-foreground);
|
||||
background: var(--l1-background);
|
||||
border-right: 1px solid var(--l1-border);
|
||||
text-align: center;
|
||||
font-family: Inter;
|
||||
font-size: 13px;
|
||||
@@ -56,3 +54,38 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.licenses-page {
|
||||
.licenses-page-header {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
background: var(--card);
|
||||
backdrop-filter: blur(20px);
|
||||
|
||||
.licenses-page-header-title {
|
||||
color: var(--l1-foreground);
|
||||
|
||||
background: var(--l1-background);
|
||||
border-right: 1px solid var(--l1-border);
|
||||
}
|
||||
}
|
||||
|
||||
.licenses-page-content-container {
|
||||
.licenses-page-content {
|
||||
background: var(--l1-background);
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,3 +176,15 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.info-text {
|
||||
color: var(--bg-robin-600) !important;
|
||||
}
|
||||
|
||||
.info-link-container {
|
||||
.anticon {
|
||||
color: var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1114,6 +1114,305 @@
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.dashboards-list-container {
|
||||
.dashboards-list-view-content {
|
||||
.title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
.subtitle {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.ant-table-row {
|
||||
.ant-table-cell {
|
||||
background: var(--l1-background);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.ant-table-cell {
|
||||
background: var(--l1-background) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-list-item {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--card);
|
||||
|
||||
.dashboard-title {
|
||||
color: var(--l2-foreground);
|
||||
|
||||
.title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.title-with-action {
|
||||
.dashboard-title {
|
||||
.ant-typography {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
.ant-typography {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-details {
|
||||
.dashboard-tag {
|
||||
background: var(--l3-background);
|
||||
.tag-text {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
.created-by {
|
||||
.dashboard-tag {
|
||||
background: var(--l3-background);
|
||||
|
||||
.tag-text {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
.dashboard-created-by {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.updated-by {
|
||||
.text {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.dashboard-tag {
|
||||
background: var(--l3-background);
|
||||
|
||||
.tag-text {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
.dashboard-created-by {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-created-by {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.dashboard-created-at {
|
||||
.ant-typography {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.no-search {
|
||||
.text {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.all-dashboards-header {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
|
||||
.typography {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.right-actions {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
.dashboard-empty-state {
|
||||
.text {
|
||||
.no-dashboard {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
.info {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
.dashboard-error-state {
|
||||
.error-text {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.action-btns {
|
||||
.retry-btn {
|
||||
background: var(--l3-background);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete-view-modal {
|
||||
.ant-modal-content {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--card);
|
||||
|
||||
.ant-modal-header {
|
||||
background: var(--card);
|
||||
|
||||
.title {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
.ant-typography {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.save-view-input {
|
||||
.ant-input {
|
||||
background: var(--l2-background);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
.cancel-btn {
|
||||
background: var(--l3-background);
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-actions {
|
||||
.ant-popover-inner {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--card);
|
||||
|
||||
.dashboard-action-content {
|
||||
.section-1 {
|
||||
.action-btn {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.section-2 {
|
||||
border-top: 1px solid var(--l1-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sort-dashboards {
|
||||
.ant-popover-inner {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--card);
|
||||
|
||||
.sort-content {
|
||||
.sort-heading {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.sort-btns {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.configure-group {
|
||||
.ant-popover-inner {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--card);
|
||||
.configure-content {
|
||||
.configure-btn {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.configure-metadata-root {
|
||||
.ant-modal-content {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--card);
|
||||
|
||||
.ant-modal-header {
|
||||
background: var(--card);
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
.configure-content {
|
||||
.configure-preview {
|
||||
border: 0.915px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
|
||||
.header {
|
||||
.title {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
.createdAt {
|
||||
.formatted-time {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.user {
|
||||
.user-tag {
|
||||
color: var(--l2-foreground);
|
||||
background-color: var(--l3-background);
|
||||
}
|
||||
|
||||
.dashboard-created-by {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.updatedAt {
|
||||
.formatted-time {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.user {
|
||||
.user-tag {
|
||||
color: var(--l2-foreground);
|
||||
background-color: var(--l3-background);
|
||||
}
|
||||
|
||||
.dashboard-created-by {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metadata-action {
|
||||
.connection-line {
|
||||
border: 1px dashed var(--l1-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
.save-changes {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title-toolip {
|
||||
.ant-tooltip-content {
|
||||
.ant-tooltip-inner {
|
||||
|
||||
@@ -194,3 +194,76 @@
|
||||
border-top: 1px solid var(--l1-border);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.new-dashboard-templates-modal {
|
||||
.ant-modal-content {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.new-dashboard-templates-content-header {
|
||||
border-bottom: 1px solid var(--l1-border);
|
||||
}
|
||||
|
||||
.new-dashboard-templates-content {
|
||||
.new-dashboard-templates-list {
|
||||
border-right: 1px solid var(--l1-border);
|
||||
|
||||
.templates-list {
|
||||
.template-list-item {
|
||||
.template-name {
|
||||
color: var(--l3-background);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--bg-robin-200) 8%, transparent);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: color-mix(in srgb, var(--bg-robin-200) 8%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-dashboard-template-preview {
|
||||
.template-preview-header {
|
||||
.template-preview-title {
|
||||
.template-preview-icon {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.template-info {
|
||||
.template-name {
|
||||
color: var(--l3-background);
|
||||
}
|
||||
|
||||
.template-description {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.create-dashboard-btn {
|
||||
.ant-btn {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.template-preview-image {
|
||||
img {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
border-top: 1px solid var(--l1-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { createMachine } from 'xstate';
|
||||
|
||||
export const DashboardSearchAndFilter = createMachine({
|
||||
tsTypes: {} as import('./Dashboard.machine.typegen').Typegen0,
|
||||
initial: 'Idle',
|
||||
states: {
|
||||
Category: {
|
||||
on: {
|
||||
NEXT: {
|
||||
actions: 'onSelectOperator',
|
||||
target: 'Operator',
|
||||
},
|
||||
onBlur: {
|
||||
actions: 'onBlurPurge',
|
||||
target: 'Idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
Operator: {
|
||||
on: {
|
||||
NEXT: {
|
||||
actions: 'onSelectValue',
|
||||
target: 'Value',
|
||||
},
|
||||
onBlur: {
|
||||
actions: 'onBlurPurge',
|
||||
target: 'Idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
Value: {
|
||||
on: {
|
||||
onBlur: {
|
||||
actions: ['onValidateQuery', 'onBlurPurge'],
|
||||
target: 'Idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
Idle: {
|
||||
on: {
|
||||
NEXT: {
|
||||
actions: 'onSelectCategory',
|
||||
description: 'Select Category',
|
||||
target: 'Category',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
id: 'Dashboard Search And Filter',
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
// This file was automatically generated. Edits will be overwritten
|
||||
|
||||
export interface Typegen0 {
|
||||
'@@xstate/typegen': true;
|
||||
eventsCausingActions: {
|
||||
onSelectOperator: 'NEXT';
|
||||
onBlurPurge: 'onBlur';
|
||||
onSelectValue: 'NEXT';
|
||||
onValidateQuery: 'onBlur';
|
||||
onSelectCategory: 'NEXT';
|
||||
};
|
||||
internalEvents: {
|
||||
'xstate.init': { type: 'xstate.init' };
|
||||
};
|
||||
invokeSrcNameMap: {};
|
||||
missingImplementations: {
|
||||
actions:
|
||||
| 'onSelectOperator'
|
||||
| 'onBlurPurge'
|
||||
| 'onSelectValue'
|
||||
| 'onValidateQuery'
|
||||
| 'onSelectCategory';
|
||||
services: never;
|
||||
guards: never;
|
||||
delays: never;
|
||||
};
|
||||
eventsCausingServices: {};
|
||||
eventsCausingGuards: {};
|
||||
eventsCausingDelays: {};
|
||||
matchesStates: 'Category' | 'Operator' | 'Value' | 'Idle';
|
||||
tags: never;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { QueryChipContainer, QueryChipItem } from './styles';
|
||||
import { IQueryStructure } from './types';
|
||||
|
||||
export default function QueryChip({
|
||||
queryData,
|
||||
onRemove,
|
||||
}: {
|
||||
queryData: IQueryStructure;
|
||||
onRemove: (id: string) => void;
|
||||
}): JSX.Element {
|
||||
const { category, operator, value, id } = queryData;
|
||||
return (
|
||||
<QueryChipContainer>
|
||||
<QueryChipItem>{category}</QueryChipItem>
|
||||
<QueryChipItem>{operator}</QueryChipItem>
|
||||
<QueryChipItem closable onClose={(): void => onRemove(id)}>
|
||||
{Array.isArray(value) ? value.join(', ') : null}
|
||||
</QueryChipItem>
|
||||
</QueryChipContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { TOperator } from '../types';
|
||||
import { executeSearchQueries } from '../utils';
|
||||
|
||||
describe('executeSearchQueries', () => {
|
||||
const firstDashboard: Dashboard = {
|
||||
id: uuid(),
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
createdBy: '',
|
||||
updatedBy: '',
|
||||
data: {
|
||||
title: 'first dashboard',
|
||||
variables: {},
|
||||
},
|
||||
};
|
||||
const secondDashboard: Dashboard = {
|
||||
id: uuid(),
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
createdBy: '',
|
||||
updatedBy: '',
|
||||
data: {
|
||||
title: 'second dashboard',
|
||||
variables: {},
|
||||
},
|
||||
};
|
||||
const thirdDashboard: Dashboard = {
|
||||
id: uuid(),
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
createdBy: '',
|
||||
updatedBy: '',
|
||||
data: {
|
||||
title: 'third dashboard (with special characters +?\\)',
|
||||
variables: {},
|
||||
},
|
||||
};
|
||||
const dashboards = [firstDashboard, secondDashboard, thirdDashboard];
|
||||
|
||||
it('should filter dashboards based on title', () => {
|
||||
const query = {
|
||||
category: 'title',
|
||||
id: 'someid',
|
||||
operator: '=' as TOperator,
|
||||
value: 'first dashboard',
|
||||
};
|
||||
|
||||
expect(executeSearchQueries([query], dashboards)).toEqual([firstDashboard]);
|
||||
});
|
||||
|
||||
it('should filter dashboards with special characters', () => {
|
||||
const query = {
|
||||
category: 'title',
|
||||
id: 'someid',
|
||||
operator: '=' as TOperator,
|
||||
value: 'third dashboard (with special characters +?\\)',
|
||||
};
|
||||
|
||||
expect(executeSearchQueries([query], dashboards)).toEqual([thirdDashboard]);
|
||||
});
|
||||
});
|
||||
212
frontend/src/container/ListOfDashboard/SearchFilter/index.tsx
Normal file
212
frontend/src/container/ListOfDashboard/SearchFilter/index.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import {
|
||||
MutableRefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { CloseCircleFilled } from '@ant-design/icons';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useMachine } from '@xstate/react';
|
||||
import { Button, RefSelectProps, Select } from 'antd';
|
||||
import history from 'lib/history';
|
||||
import { filter, map } from 'lodash-es';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { DashboardSearchAndFilter } from './Dashboard.machine';
|
||||
import QueryChip from './QueryChip';
|
||||
import { QueryChipItem, SearchContainer } from './styles';
|
||||
import { IOptionsData, IQueryStructure, TCategory, TOperator } from './types';
|
||||
import {
|
||||
convertQueriesToURLQuery,
|
||||
convertURLQueryStringToQuery,
|
||||
executeSearchQueries,
|
||||
OptionsSchemas,
|
||||
OptionsValueResolution,
|
||||
} from './utils';
|
||||
|
||||
function SearchFilter({
|
||||
searchData,
|
||||
filterDashboards,
|
||||
}: {
|
||||
searchData: Dashboard[];
|
||||
filterDashboards: (filteredDashboards: Dashboard[]) => void;
|
||||
}): JSX.Element {
|
||||
const [category, setCategory] = useState<TCategory>();
|
||||
const [optionsData, setOptionsData] = useState<IOptionsData>(
|
||||
OptionsSchemas.attribute,
|
||||
);
|
||||
const selectRef = useRef() as MutableRefObject<RefSelectProps>;
|
||||
const [selectedValues, setSelectedValues] = useState<string[]>([]);
|
||||
const [staging, setStaging] = useState<string[] | string[][] | unknown[]>([]);
|
||||
const [queries, setQueries] = useState<IQueryStructure[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const searchQueryString = new URLSearchParams(history.location.search).get(
|
||||
'search',
|
||||
);
|
||||
if (searchQueryString) {
|
||||
setQueries(convertURLQueryStringToQuery(searchQueryString) || []);
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
filterDashboards(executeSearchQueries(queries, searchData));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [queries, searchData]);
|
||||
|
||||
const updateURLWithQuery = useCallback(
|
||||
(inputQueries?: IQueryStructure[]): void => {
|
||||
history.push({
|
||||
pathname: history.location.pathname,
|
||||
search:
|
||||
inputQueries || queries
|
||||
? `?search=${convertQueriesToURLQuery(inputQueries || queries)}`
|
||||
: '',
|
||||
});
|
||||
},
|
||||
[queries],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (Array.isArray(queries) && queries.length > 0) {
|
||||
updateURLWithQuery();
|
||||
}
|
||||
}, [queries, updateURLWithQuery]);
|
||||
|
||||
const [state, send] = useMachine(DashboardSearchAndFilter, {
|
||||
actions: {
|
||||
onSelectCategory: () => {
|
||||
setOptionsData(OptionsSchemas.attribute);
|
||||
},
|
||||
onSelectOperator: () => {
|
||||
setOptionsData(OptionsSchemas.operator);
|
||||
},
|
||||
onSelectValue: () => {
|
||||
setOptionsData(
|
||||
OptionsValueResolution(category as TCategory, searchData) as IOptionsData,
|
||||
);
|
||||
},
|
||||
onBlurPurge: () => {
|
||||
setSelectedValues([]);
|
||||
setStaging([]);
|
||||
},
|
||||
onValidateQuery: () => {
|
||||
if (staging.length <= 2 && selectedValues.length === 0) {
|
||||
return;
|
||||
}
|
||||
setQueries([
|
||||
...queries,
|
||||
{
|
||||
id: uuidv4(),
|
||||
category: staging[0] as string,
|
||||
operator: staging[1] as TOperator,
|
||||
value: selectedValues,
|
||||
},
|
||||
]);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const nextState = (): void => {
|
||||
send('NEXT');
|
||||
};
|
||||
|
||||
const removeQueryById = (queryId: string): void => {
|
||||
setQueries((queries) => {
|
||||
const updatedQueries = filter(queries, ({ id }) => id !== queryId);
|
||||
updateURLWithQuery(updatedQueries);
|
||||
return updatedQueries;
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (value: never | string[]): void => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
if (optionsData.mode) {
|
||||
setSelectedValues(value.filter(Boolean));
|
||||
return;
|
||||
}
|
||||
setStaging([...staging, value]);
|
||||
|
||||
if (state.value === 'Category') {
|
||||
setCategory(`${value}`.toLowerCase() as TCategory);
|
||||
}
|
||||
nextState();
|
||||
setSelectedValues([]);
|
||||
};
|
||||
const handleFocus = (): void => {
|
||||
if (state.value === 'Idle') {
|
||||
send('NEXT');
|
||||
selectRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = (): void => {
|
||||
send('onBlur');
|
||||
selectRef?.current?.blur();
|
||||
};
|
||||
|
||||
const clearQueries = (): void => {
|
||||
setQueries([]);
|
||||
history.push({
|
||||
pathname: history.location.pathname,
|
||||
search: ``,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<SearchContainer>
|
||||
<div>
|
||||
{map(queries, (query) => (
|
||||
<QueryChip key={query.id} queryData={query} onRemove={removeQueryById} />
|
||||
))}
|
||||
{map(staging, (value) => (
|
||||
<QueryChipItem key={JSON.stringify(value)}>
|
||||
{value as string}
|
||||
</QueryChipItem>
|
||||
))}
|
||||
</div>
|
||||
{optionsData && (
|
||||
<Select
|
||||
placeholder={
|
||||
!queries.length &&
|
||||
!staging.length &&
|
||||
!selectedValues.length &&
|
||||
'Search or Filter results'
|
||||
}
|
||||
size="small"
|
||||
ref={selectRef}
|
||||
mode={optionsData.mode as 'tags' | 'multiple'}
|
||||
style={{ flex: 1 }}
|
||||
onChange={handleChange}
|
||||
bordered={false}
|
||||
suffixIcon={null}
|
||||
value={selectedValues}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
showSearch
|
||||
>
|
||||
{optionsData.options &&
|
||||
Array.isArray(optionsData.options) &&
|
||||
optionsData.options.map(
|
||||
(optionItem): JSX.Element => (
|
||||
<Select.Option
|
||||
key={(optionItem.value as string) || (optionItem.name as string)}
|
||||
value={optionItem.value || optionItem.name}
|
||||
>
|
||||
{optionItem.name}
|
||||
</Select.Option>
|
||||
),
|
||||
)}
|
||||
</Select>
|
||||
)}
|
||||
{queries && queries.length > 0 && (
|
||||
<Button icon={<CloseCircleFilled />} type="text" onClick={clearQueries} />
|
||||
)}
|
||||
</SearchContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchFilter;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { grey } from '@ant-design/colors';
|
||||
import { Tag } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const SearchContainer = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
padding: 0.2rem 0;
|
||||
margin: 1rem 0;
|
||||
border: 1px solid #ccc5;
|
||||
`;
|
||||
export const QueryChipContainer = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 0.5rem;
|
||||
&:hover {
|
||||
& > * {
|
||||
background: ${grey.primary}44;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QueryChipItem = styled(Tag)`
|
||||
margin-right: 0.1rem;
|
||||
`;
|
||||
18
frontend/src/container/ListOfDashboard/SearchFilter/types.ts
Normal file
18
frontend/src/container/ListOfDashboard/SearchFilter/types.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export type TOperator = '=' | '!=';
|
||||
|
||||
export type TCategory = 'title' | 'description' | 'tags';
|
||||
export interface IQueryStructure {
|
||||
category: string;
|
||||
id: string;
|
||||
operator: TOperator;
|
||||
value: string | string[];
|
||||
}
|
||||
|
||||
interface IOptions {
|
||||
name: string;
|
||||
value?: string;
|
||||
}
|
||||
export interface IOptionsData {
|
||||
mode: undefined | 'tags' | 'multiple';
|
||||
options: IOptions[] | [];
|
||||
}
|
||||
153
frontend/src/container/ListOfDashboard/SearchFilter/utils.ts
Normal file
153
frontend/src/container/ListOfDashboard/SearchFilter/utils.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { decode, encode } from 'js-base64';
|
||||
import { flattenDeep, map, uniqWith } from 'lodash-es';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { IOptionsData, IQueryStructure, TCategory, TOperator } from './types';
|
||||
|
||||
export const convertQueriesToURLQuery = (
|
||||
queries: IQueryStructure[],
|
||||
): string => {
|
||||
if (!queries || !queries.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return encode(JSON.stringify(queries));
|
||||
};
|
||||
|
||||
export const convertURLQueryStringToQuery = (
|
||||
queryString: string,
|
||||
): IQueryStructure[] => JSON.parse(decode(queryString));
|
||||
|
||||
export const resolveOperator = (
|
||||
result: unknown,
|
||||
operator: TOperator,
|
||||
): boolean => {
|
||||
if (operator === '!=') {
|
||||
return !result;
|
||||
}
|
||||
if (operator === '=') {
|
||||
return !!result;
|
||||
}
|
||||
return !!result;
|
||||
};
|
||||
export const executeSearchQueries = (
|
||||
queries: IQueryStructure[] = [],
|
||||
searchData: Dashboard[] = [],
|
||||
): Dashboard[] => {
|
||||
if (!searchData.length || !queries.length) {
|
||||
return searchData;
|
||||
}
|
||||
const escapeRegExp = (regExp: string): string =>
|
||||
regExp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
queries.forEach((query: IQueryStructure) => {
|
||||
const { operator } = query;
|
||||
let { value } = query;
|
||||
const categoryLowercase: TCategory = `${query.category}`.toLowerCase() as
|
||||
| 'title'
|
||||
| 'description';
|
||||
value = flattenDeep([value]);
|
||||
|
||||
searchData = searchData.filter(({ data: searchPayload }: Dashboard) => {
|
||||
try {
|
||||
const searchSpace =
|
||||
flattenDeep([searchPayload[categoryLowercase]]).filter(Boolean) || null;
|
||||
if (!searchSpace || !searchSpace.length) {
|
||||
return resolveOperator(false, operator);
|
||||
}
|
||||
|
||||
for (const searchSpaceItem of searchSpace) {
|
||||
if (searchSpaceItem) {
|
||||
for (const queryValue of value) {
|
||||
if (searchSpaceItem.match(escapeRegExp(queryValue))) {
|
||||
return resolveOperator(true, operator);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
return resolveOperator(false, operator);
|
||||
});
|
||||
});
|
||||
return searchData;
|
||||
};
|
||||
|
||||
export const OptionsSchemas = {
|
||||
attribute: {
|
||||
mode: undefined,
|
||||
options: [
|
||||
{
|
||||
name: 'Title',
|
||||
},
|
||||
{
|
||||
name: 'Description',
|
||||
},
|
||||
{
|
||||
name: 'Tags',
|
||||
},
|
||||
],
|
||||
},
|
||||
operator: {
|
||||
mode: undefined,
|
||||
options: [
|
||||
{
|
||||
value: '=',
|
||||
name: 'Equal',
|
||||
},
|
||||
{
|
||||
name: 'Not Equal',
|
||||
value: '!=',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export function OptionsValueResolution(
|
||||
category: TCategory,
|
||||
searchData: Dashboard[],
|
||||
): Record<string, unknown> | IOptionsData {
|
||||
const OptionsValueSchema = {
|
||||
title: {
|
||||
mode: 'tags',
|
||||
options: uniqWith(
|
||||
map(searchData, (searchItem) => ({ name: searchItem.data.title })),
|
||||
(prev, next) => prev.name === next.name,
|
||||
),
|
||||
},
|
||||
description: {
|
||||
mode: 'tags',
|
||||
options: uniqWith(
|
||||
map(searchData, (searchItem) =>
|
||||
searchItem.data.description
|
||||
? {
|
||||
name: searchItem.data.description,
|
||||
value: searchItem.data.description,
|
||||
}
|
||||
: null,
|
||||
).filter(Boolean),
|
||||
(prev, next) => prev?.name === next?.name,
|
||||
),
|
||||
},
|
||||
tags: {
|
||||
mode: 'tags',
|
||||
options: uniqWith(
|
||||
map(
|
||||
flattenDeep(
|
||||
// @ts-ignore
|
||||
map(searchData, (searchItem) => searchItem.data.tags).filter(Boolean),
|
||||
),
|
||||
(tag) => ({ name: tag }),
|
||||
),
|
||||
(prev, next) => prev.name === next.name,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
OptionsValueSchema[category] ||
|
||||
({ mode: undefined, options: [] } as IOptionsData)
|
||||
);
|
||||
}
|
||||
@@ -7,3 +7,9 @@
|
||||
.delete-btn:hover {
|
||||
background-color: color-mix(in srgb, var(--l1-foreground) 12%, transparent);
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.delete-btn:hover {
|
||||
background-color: rgba(0, 0, 0, 0.06) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,19 +8,8 @@ import {
|
||||
OPERATORS,
|
||||
QUERY_BUILDER_FUNCTIONS,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { TitleWrapper } from './BodyTitleRenderer.styles';
|
||||
import { DROPDOWN_KEY } from './constant';
|
||||
@@ -36,32 +25,17 @@ function BodyTitleRenderer({
|
||||
parentIsArray = false,
|
||||
nodeKey,
|
||||
value,
|
||||
handleChangeSelectedView,
|
||||
}: BodyTitleRendererProps): JSX.Element {
|
||||
const { onAddToQuery } = useActiveLog();
|
||||
const { stagedQuery, updateQueriesData } = useQueryBuilder();
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
const { notifications } = useNotifications();
|
||||
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
||||
|
||||
const cleanedNodeKey = removeObjectFromString(nodeKey);
|
||||
const isBodyJsonQueryEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.BODY_JSON_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
// Group by is supported only for body json query enabled and not for array elements
|
||||
const isGroupBySupported =
|
||||
isBodyJsonQueryEnabled && !cleanedNodeKey.includes('[]');
|
||||
|
||||
const filterHandler = (isFilterIn: boolean) => (): void => {
|
||||
if (parentIsArray) {
|
||||
onAddToQuery(
|
||||
generateFieldKeyForArray(
|
||||
cleanedNodeKey,
|
||||
removeObjectFromString(nodeKey),
|
||||
getDataTypes(value),
|
||||
isBodyJsonQueryEnabled,
|
||||
),
|
||||
`${value}`,
|
||||
isFilterIn
|
||||
@@ -71,7 +45,7 @@ function BodyTitleRenderer({
|
||||
);
|
||||
} else {
|
||||
onAddToQuery(
|
||||
`body.${cleanedNodeKey}`,
|
||||
`body.${removeObjectFromString(nodeKey)}`,
|
||||
`${value}`,
|
||||
isFilterIn ? OPERATORS['='] : OPERATORS['!='],
|
||||
getDataTypes(value),
|
||||
@@ -79,67 +53,10 @@ function BodyTitleRenderer({
|
||||
}
|
||||
};
|
||||
|
||||
const groupByHandler = useCallback((): void => {
|
||||
if (!stagedQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groupByKey = parentIsArray
|
||||
? generateFieldKeyForArray(
|
||||
cleanedNodeKey,
|
||||
getDataTypes(value),
|
||||
isBodyJsonQueryEnabled,
|
||||
)
|
||||
: `body.${cleanedNodeKey}`;
|
||||
|
||||
const fieldDataType = getDataTypes(value);
|
||||
const normalizedDataType: DataTypes | undefined = Object.values(
|
||||
DataTypes,
|
||||
).includes(fieldDataType as DataTypes)
|
||||
? (fieldDataType as DataTypes)
|
||||
: undefined;
|
||||
|
||||
const updatedQuery = updateQueriesData(
|
||||
stagedQuery,
|
||||
'queryData',
|
||||
(item, index) => {
|
||||
if (index === 0) {
|
||||
const newGroupByItem: BaseAutocompleteData = {
|
||||
key: groupByKey,
|
||||
type: '',
|
||||
dataType: normalizedDataType,
|
||||
};
|
||||
|
||||
return { ...item, groupBy: [...(item.groupBy || []), newGroupByItem] };
|
||||
}
|
||||
|
||||
return item;
|
||||
},
|
||||
);
|
||||
|
||||
const queryData: ICurrentQueryData = {
|
||||
name: viewName,
|
||||
id: updatedQuery.id,
|
||||
query: updatedQuery,
|
||||
};
|
||||
|
||||
handleChangeSelectedView?.(ExplorerViews.TIMESERIES, queryData);
|
||||
}, [
|
||||
cleanedNodeKey,
|
||||
handleChangeSelectedView,
|
||||
isBodyJsonQueryEnabled,
|
||||
parentIsArray,
|
||||
stagedQuery,
|
||||
updateQueriesData,
|
||||
value,
|
||||
viewName,
|
||||
]);
|
||||
|
||||
const onClickHandler: MenuProps['onClick'] = (props): void => {
|
||||
const mapper = {
|
||||
[DROPDOWN_KEY.FILTER_IN]: filterHandler(true),
|
||||
[DROPDOWN_KEY.FILTER_OUT]: filterHandler(false),
|
||||
[DROPDOWN_KEY.GROUP_BY]: groupByHandler,
|
||||
};
|
||||
|
||||
const handler = mapper[props.key];
|
||||
@@ -159,14 +76,6 @@ function BodyTitleRenderer({
|
||||
key: DROPDOWN_KEY.FILTER_OUT,
|
||||
label: `Filter out ${value}`,
|
||||
},
|
||||
...(isGroupBySupported
|
||||
? [
|
||||
{
|
||||
key: DROPDOWN_KEY.GROUP_BY,
|
||||
label: `Group by ${nodeKey}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
onClick: onClickHandler,
|
||||
};
|
||||
@@ -175,6 +84,7 @@ function BodyTitleRenderer({
|
||||
(e: React.MouseEvent): void => {
|
||||
// Prevent tree node expansion/collapse
|
||||
e.stopPropagation();
|
||||
const cleanedKey = removeObjectFromString(nodeKey);
|
||||
let copyText: string;
|
||||
|
||||
// Check if value is an object or array
|
||||
@@ -196,8 +106,8 @@ function BodyTitleRenderer({
|
||||
|
||||
if (copyText) {
|
||||
const notificationMessage = isObject
|
||||
? `${cleanedNodeKey} object copied to clipboard`
|
||||
: `${cleanedNodeKey} copied to clipboard`;
|
||||
? `${cleanedKey} object copied to clipboard`
|
||||
: `${cleanedKey} copied to clipboard`;
|
||||
|
||||
notifications.success({
|
||||
message: notificationMessage,
|
||||
@@ -205,7 +115,7 @@ function BodyTitleRenderer({
|
||||
});
|
||||
}
|
||||
},
|
||||
[cleanedNodeKey, parentIsArray, setCopy, value, notifications],
|
||||
[nodeKey, parentIsArray, setCopy, value, notifications],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
@@ -7,7 +6,6 @@ export interface BodyTitleRendererProps {
|
||||
nodeKey: string;
|
||||
value: unknown;
|
||||
parentIsArray?: boolean;
|
||||
handleChangeSelectedView?: ChangeViewFunctionType;
|
||||
}
|
||||
|
||||
export type AnyObject = { [key: string]: any };
|
||||
|
||||
@@ -58,3 +58,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.table-view-actions-content {
|
||||
.ant-popover-inner {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l1-background) !important;
|
||||
backdrop-filter: none;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Popover, Spin, Tooltip, Tree } from 'antd';
|
||||
import type { DataNode } from 'antd/es/tree';
|
||||
import GroupByIcon from 'assets/CustomIcons/GroupByIcon';
|
||||
import cx from 'classnames';
|
||||
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
|
||||
@@ -58,7 +57,7 @@ interface ITableViewActionsProps {
|
||||
}
|
||||
|
||||
// Memoized Tree Component
|
||||
const MemoizedTree = React.memo<{ treeData: DataNode[] }>(({ treeData }) => (
|
||||
const MemoizedTree = React.memo<{ treeData: any[] }>(({ treeData }) => (
|
||||
<Tree
|
||||
defaultExpandAll
|
||||
showLine
|
||||
@@ -75,54 +74,50 @@ const BodyContent: React.FC<{
|
||||
record: DataType;
|
||||
bodyHtml: { __html: string };
|
||||
textToCopy: string;
|
||||
handleChangeSelectedView?: ChangeViewFunctionType;
|
||||
}> = React.memo(
|
||||
({ fieldData, record, bodyHtml, textToCopy, handleChangeSelectedView }) => {
|
||||
const { isLoading, treeData, error } = useAsyncJSONProcessing(
|
||||
fieldData.value,
|
||||
record.field === 'body',
|
||||
handleChangeSelectedView,
|
||||
}> = React.memo(({ fieldData, record, bodyHtml, textToCopy }) => {
|
||||
const { isLoading, treeData, error } = useAsyncJSONProcessing(
|
||||
fieldData.value,
|
||||
record.field === 'body',
|
||||
);
|
||||
|
||||
// Show JSON tree if available, otherwise show HTML content
|
||||
if (record.field === 'body' && treeData) {
|
||||
return <MemoizedTree treeData={treeData} />;
|
||||
}
|
||||
|
||||
if (record.field === 'body' && isLoading) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Spin size="small" />
|
||||
<span style={{ color: Color.BG_SIENNA_400 }}>Processing JSON...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show JSON tree if available, otherwise show HTML content
|
||||
if (record.field === 'body' && treeData) {
|
||||
return <MemoizedTree treeData={treeData} />;
|
||||
}
|
||||
if (record.field === 'body' && error) {
|
||||
return (
|
||||
<span
|
||||
style={{ color: Color.BG_SIENNA_400, whiteSpace: 'pre-wrap', tabSize: 4 }}
|
||||
>
|
||||
Error parsing Body JSON
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (record.field === 'body' && isLoading) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Spin size="small" />
|
||||
<span style={{ color: Color.BG_SIENNA_400 }}>Processing JSON...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (record.field === 'body' && error) {
|
||||
return (
|
||||
if (record.field === 'body') {
|
||||
return (
|
||||
<CopyClipboardHOC entityKey="body" textToCopy={textToCopy}>
|
||||
<span
|
||||
style={{ color: Color.BG_SIENNA_400, whiteSpace: 'pre-wrap', tabSize: 4 }}
|
||||
>
|
||||
Error parsing Body JSON
|
||||
<span dangerouslySetInnerHTML={bodyHtml} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
</CopyClipboardHOC>
|
||||
);
|
||||
}
|
||||
|
||||
if (record.field === 'body') {
|
||||
return (
|
||||
<CopyClipboardHOC entityKey="body" textToCopy={textToCopy}>
|
||||
<span
|
||||
style={{ color: Color.BG_SIENNA_400, whiteSpace: 'pre-wrap', tabSize: 4 }}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={bodyHtml} />
|
||||
</span>
|
||||
</CopyClipboardHOC>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
);
|
||||
return null;
|
||||
});
|
||||
|
||||
BodyContent.displayName = 'BodyContent';
|
||||
|
||||
@@ -324,7 +319,6 @@ export default function TableViewActions(
|
||||
record={record}
|
||||
bodyHtml={bodyHtml}
|
||||
textToCopy={textToCopy}
|
||||
handleChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -348,7 +342,6 @@ export default function TableViewActions(
|
||||
fieldData,
|
||||
bodyHtml,
|
||||
textToCopy,
|
||||
handleChangeSelectedView,
|
||||
formatTimezoneAdjustedTimestamp,
|
||||
cleanTimestamp,
|
||||
]);
|
||||
@@ -362,7 +355,6 @@ export default function TableViewActions(
|
||||
record={record}
|
||||
bodyHtml={bodyHtml}
|
||||
textToCopy={textToCopy}
|
||||
handleChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
{!isListViewPanel &&
|
||||
!RESTRICTED_SELECTED_FIELDS.includes(fieldFilterKey) && (
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import { jsonToDataNodes, recursiveParseJSON } from '../utils';
|
||||
|
||||
@@ -12,7 +9,6 @@ const MAX_BODY_BYTES = 100 * 1024; // 100 KB
|
||||
const useAsyncJSONProcessing = (
|
||||
value: string,
|
||||
shouldProcess: boolean,
|
||||
handleChangeSelectedView?: ChangeViewFunctionType,
|
||||
): {
|
||||
isLoading: boolean;
|
||||
treeData: any[] | null;
|
||||
@@ -29,10 +25,6 @@ const useAsyncJSONProcessing = (
|
||||
});
|
||||
|
||||
const processingRef = useRef<boolean>(false);
|
||||
const { featureFlags } = useAppContext();
|
||||
const isBodyJsonQueryEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.BODY_JSON_ENABLED)
|
||||
?.active || false;
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
useEffect((): (() => void) => {
|
||||
@@ -55,10 +47,7 @@ const useAsyncJSONProcessing = (
|
||||
try {
|
||||
const parsedBody = recursiveParseJSON(value);
|
||||
if (!isEmpty(parsedBody)) {
|
||||
const treeData = jsonToDataNodes(parsedBody, {
|
||||
isBodyJsonQueryEnabled,
|
||||
handleChangeSelectedView,
|
||||
});
|
||||
const treeData = jsonToDataNodes(parsedBody);
|
||||
setJsonState({ isLoading: false, treeData, error: null });
|
||||
} else {
|
||||
setJsonState({ isLoading: false, treeData: null, error: null });
|
||||
@@ -84,10 +73,7 @@ const useAsyncJSONProcessing = (
|
||||
try {
|
||||
const parsedBody = recursiveParseJSON(value);
|
||||
if (!isEmpty(parsedBody)) {
|
||||
const treeData = jsonToDataNodes(parsedBody, {
|
||||
isBodyJsonQueryEnabled,
|
||||
handleChangeSelectedView,
|
||||
});
|
||||
const treeData = jsonToDataNodes(parsedBody);
|
||||
setJsonState({ isLoading: false, treeData, error: null });
|
||||
} else {
|
||||
setJsonState({ isLoading: false, treeData: null, error: null });
|
||||
@@ -115,7 +101,7 @@ const useAsyncJSONProcessing = (
|
||||
return (): void => {
|
||||
processingRef.current = false;
|
||||
};
|
||||
}, [value, shouldProcess, isBodyJsonQueryEnabled, handleChangeSelectedView]);
|
||||
}, [value, shouldProcess]);
|
||||
|
||||
return jsonState;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export const DROPDOWN_KEY = {
|
||||
FILTER_IN: 'filterIn',
|
||||
FILTER_OUT: 'filterOut',
|
||||
GROUP_BY: 'groupBy',
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Convert from 'ansi-to-html';
|
||||
import type { DataNode } from 'antd/es/tree';
|
||||
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import dompurify from 'dompurify';
|
||||
import { uniqueId } from 'lodash-es';
|
||||
@@ -35,32 +34,13 @@ export const recursiveParseJSON = (obj: string): Record<string, unknown> => {
|
||||
}
|
||||
};
|
||||
|
||||
type JsonToDataNodesOptions = {
|
||||
parentKey?: string;
|
||||
parentIsArray?: boolean;
|
||||
isBodyJsonQueryEnabled?: boolean;
|
||||
handleChangeSelectedView?: ChangeViewFunctionType;
|
||||
};
|
||||
|
||||
type ComputeDataNodeOptions = {
|
||||
key: string;
|
||||
valueIsArray: boolean;
|
||||
value: unknown;
|
||||
nodeKey: string;
|
||||
parentIsArray: boolean;
|
||||
isBodyJsonQueryEnabled?: boolean;
|
||||
handleChangeSelectedView?: ChangeViewFunctionType;
|
||||
};
|
||||
|
||||
export const computeDataNode = ({
|
||||
key,
|
||||
valueIsArray,
|
||||
value,
|
||||
nodeKey,
|
||||
parentIsArray,
|
||||
isBodyJsonQueryEnabled = false,
|
||||
handleChangeSelectedView,
|
||||
}: ComputeDataNodeOptions): DataNode => ({
|
||||
export const computeDataNode = (
|
||||
key: string,
|
||||
valueIsArray: boolean,
|
||||
value: unknown,
|
||||
nodeKey: string,
|
||||
parentIsArray: boolean,
|
||||
): DataNode => ({
|
||||
key: uniqueId(),
|
||||
title: (
|
||||
<BodyTitleRenderer
|
||||
@@ -68,30 +48,20 @@ export const computeDataNode = ({
|
||||
nodeKey={nodeKey}
|
||||
value={value}
|
||||
parentIsArray={parentIsArray}
|
||||
handleChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
),
|
||||
children: jsonToDataNodes(value as Record<string, unknown>, {
|
||||
parentKey: valueIsArray
|
||||
? `${nodeKey}${isBodyJsonQueryEnabled ? '[]' : '[*]'}`
|
||||
: nodeKey,
|
||||
parentIsArray: valueIsArray,
|
||||
isBodyJsonQueryEnabled,
|
||||
handleChangeSelectedView,
|
||||
}),
|
||||
children: jsonToDataNodes(
|
||||
value as Record<string, unknown>,
|
||||
valueIsArray ? `${nodeKey}[*]` : nodeKey,
|
||||
valueIsArray,
|
||||
),
|
||||
});
|
||||
|
||||
export function jsonToDataNodes(
|
||||
json: Record<string, unknown>,
|
||||
options: JsonToDataNodesOptions = {},
|
||||
parentKey = '',
|
||||
parentIsArray = false,
|
||||
): DataNode[] {
|
||||
const {
|
||||
parentKey = '',
|
||||
parentIsArray = false,
|
||||
isBodyJsonQueryEnabled = false,
|
||||
handleChangeSelectedView,
|
||||
} = options;
|
||||
|
||||
return Object.entries(json).map(([key, value]) => {
|
||||
let nodeKey = parentKey || key;
|
||||
if (parentIsArray) {
|
||||
@@ -104,15 +74,7 @@ export function jsonToDataNodes(
|
||||
|
||||
if (parentIsArray) {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return computeDataNode({
|
||||
key,
|
||||
valueIsArray,
|
||||
value,
|
||||
nodeKey,
|
||||
parentIsArray,
|
||||
isBodyJsonQueryEnabled,
|
||||
handleChangeSelectedView,
|
||||
});
|
||||
return computeDataNode(key, valueIsArray, value, nodeKey, parentIsArray);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -123,31 +85,14 @@ export function jsonToDataNodes(
|
||||
nodeKey={nodeKey}
|
||||
value={value}
|
||||
parentIsArray={parentIsArray}
|
||||
handleChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
),
|
||||
children: jsonToDataNodes(
|
||||
{},
|
||||
{
|
||||
parentKey: nodeKey,
|
||||
parentIsArray: valueIsArray,
|
||||
isBodyJsonQueryEnabled,
|
||||
handleChangeSelectedView,
|
||||
},
|
||||
),
|
||||
children: jsonToDataNodes({}, nodeKey, valueIsArray),
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return computeDataNode({
|
||||
key,
|
||||
valueIsArray,
|
||||
value,
|
||||
nodeKey,
|
||||
parentIsArray,
|
||||
isBodyJsonQueryEnabled,
|
||||
handleChangeSelectedView,
|
||||
});
|
||||
return computeDataNode(key, valueIsArray, value, nodeKey, parentIsArray);
|
||||
}
|
||||
return {
|
||||
key: uniqueId(),
|
||||
@@ -157,7 +102,6 @@ export function jsonToDataNodes(
|
||||
nodeKey={nodeKey}
|
||||
value={value}
|
||||
parentIsArray={parentIsArray}
|
||||
handleChangeSelectedView={handleChangeSelectedView}
|
||||
/>
|
||||
),
|
||||
};
|
||||
@@ -179,7 +123,6 @@ export function flattenObject(obj: AnyObject, prefix = ''): AnyObject {
|
||||
export const generateFieldKeyForArray = (
|
||||
fieldKey: string,
|
||||
dataType: DataTypes,
|
||||
isBodyJsonQueryEnabled = false,
|
||||
): string => {
|
||||
let lastDotIndex = fieldKey.lastIndexOf('.');
|
||||
let resultNodeKey = fieldKey;
|
||||
@@ -195,16 +138,6 @@ export const generateFieldKeyForArray = (
|
||||
newResultNodeKey = resultNodeKey.substring(0, lastDotIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// When filtering for a value inside an array, the query builder expects the
|
||||
// last array segment to be referenced without the trailing `[]`.
|
||||
// Examples:
|
||||
// - has(body.config.features, 'fast_checkout')
|
||||
// - has(body.config.features[].items, 'pen')
|
||||
// - has(body.config.features[].items[].variants, 'ballpen')
|
||||
if (isBodyJsonQueryEnabled && newResultNodeKey.endsWith('[]')) {
|
||||
newResultNodeKey = newResultNodeKey.slice(0, -2);
|
||||
}
|
||||
return `body.${newResultNodeKey}`;
|
||||
};
|
||||
|
||||
|
||||
@@ -160,6 +160,55 @@
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.login-form-card {
|
||||
background: var(--l2-background);
|
||||
}
|
||||
|
||||
.login-error-container {
|
||||
.error-content {
|
||||
background: color-mix(in srgb, var(--danger-background) 10%, transparent);
|
||||
border-color: color-mix(in srgb, var(--danger-background) 20%, transparent);
|
||||
|
||||
&__error-code {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
&__error-message {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__docs-button {
|
||||
color: var(--l1-foreground);
|
||||
border-color: var(--l1-border);
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
color: var(--l2-foreground);
|
||||
border-color: var(--l1-border);
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&__message-badge-label-text {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
&__message-item {
|
||||
color: var(--l1-foreground);
|
||||
|
||||
&::before {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
|
||||
&__scroll-hint-text {
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.password-label-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -10,6 +10,10 @@ export const Label = styled.label`
|
||||
color: var(--l1-foreground);
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
|
||||
.lightMode & {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
`;
|
||||
|
||||
export const FormContainer = styled(Form)`
|
||||
@@ -46,10 +50,20 @@ export const FormContainer = styled(Form)`
|
||||
background: var(--l3-background) !important;
|
||||
border-color: var(--l1-border) !important;
|
||||
color: var(--l1-foreground) !important;
|
||||
|
||||
.lightMode & {
|
||||
background: var(--bg-vanilla-200) !important;
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
color: var(--text-ink-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
& .ant-input::placeholder {
|
||||
color: var(--l3-foreground) !important;
|
||||
|
||||
.lightMode & {
|
||||
color: var(--text-neutral-light-200) !important;
|
||||
}
|
||||
}
|
||||
|
||||
& .ant-input:focus,
|
||||
|
||||
@@ -323,3 +323,50 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.logs-list-view-container {
|
||||
.logs-list-table-view-container {
|
||||
table {
|
||||
thead {
|
||||
tr {
|
||||
th {
|
||||
color: var(--l1-foreground) !important;
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--l1-border) !important;
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--l1-border) !important;
|
||||
background-color: var(--l1-background) !important;
|
||||
|
||||
tr {
|
||||
&:hover {
|
||||
background-color: var(--l3-background) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
&:hover {
|
||||
background-color: var(--l3-background) !important;
|
||||
}
|
||||
|
||||
border-bottom: 1px solid var(--l1-border) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sticky-header-table-container {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--l1-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,6 +263,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.logs-explorer-views-container {
|
||||
.views-tabs-container {
|
||||
.views-tabs {
|
||||
.selected_view {
|
||||
background: white;
|
||||
color: var(--text-robin-400);
|
||||
border: 1px solid var(--bg-robin-400);
|
||||
}
|
||||
|
||||
.selected_view::before {
|
||||
background: var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.order-by-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
.mcp-auth-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--padding-4) var(--padding-5);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 8px;
|
||||
background: var(--l2-background);
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-5);
|
||||
font-size: var(--label-medium-500-font-size);
|
||||
font-weight: var(--label-medium-500-font-weight);
|
||||
color: var(--l1-foreground);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--foreground);
|
||||
line-height: var(--line-height-18);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
&__field-label {
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
color: var(--foreground);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
&__endpoint-value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
padding: var(--padding-2) var(--padding-3);
|
||||
border: 1px solid var(--l3-border);
|
||||
border-radius: 6px;
|
||||
background: var(--l3-background);
|
||||
font-family: var(--font-family-sf-mono, monospace);
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
color: var(--l1-foreground);
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__cta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-10);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__helper-text {
|
||||
font-size: var(--paragraph-small-400-font-size);
|
||||
color: var(--foreground);
|
||||
line-height: var(--paragraph-small-400-line-height);
|
||||
}
|
||||
|
||||
&__info-banner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-2);
|
||||
padding: var(--padding-2) var(--padding-3);
|
||||
border-left: 3px solid var(--bg-robin-400);
|
||||
background: var(--l3-background);
|
||||
border-radius: 4px;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
color: var(--bg-robin-400);
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import AuthCard from './AuthCard';
|
||||
|
||||
const mockOnCopyInstanceUrl = jest.fn();
|
||||
const mockOnCreateServiceAccount = jest.fn();
|
||||
|
||||
const defaultProps = {
|
||||
instanceUrl: 'http://localhost',
|
||||
onCopyInstanceUrl: mockOnCopyInstanceUrl,
|
||||
onCreateServiceAccount: mockOnCreateServiceAccount,
|
||||
};
|
||||
|
||||
describe('AuthCard', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders the instance URL', () => {
|
||||
render(<AuthCard {...defaultProps} isAdmin />);
|
||||
|
||||
expect(screen.getByTestId('mcp-instance-url')).toHaveTextContent(
|
||||
'http://localhost',
|
||||
);
|
||||
});
|
||||
|
||||
it('shows Create Service Account button for admin', () => {
|
||||
render(<AuthCard {...defaultProps} isAdmin />);
|
||||
|
||||
expect(screen.getByText('Create service account')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(
|
||||
'Only admins can create API keys. Ask your workspace admin for a key with read access, then paste it into the API Key field.',
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows info banner for non-admin', () => {
|
||||
render(<AuthCard {...defaultProps} isAdmin={false} />);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Only admins can create API keys. Ask your workspace admin for a key with read access, then paste it into the API Key field.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText('Create service account')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onCopyInstanceUrl when copy button is clicked', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<AuthCard {...defaultProps} isAdmin />);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'Copy SigNoz instance URL' }),
|
||||
);
|
||||
|
||||
expect(mockOnCopyInstanceUrl).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls onCreateServiceAccount when admin clicks the CTA', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<AuthCard {...defaultProps} isAdmin />);
|
||||
|
||||
await user.click(screen.getByText('Create service account'));
|
||||
|
||||
expect(mockOnCreateServiceAccount).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,75 +0,0 @@
|
||||
import { Badge, Button } from '@signozhq/ui';
|
||||
import { Info, KeyRound } from '@signozhq/icons';
|
||||
import CopyIconButton from '../CopyIconButton';
|
||||
|
||||
import './AuthCard.styles.scss';
|
||||
|
||||
interface AuthCardProps {
|
||||
isAdmin: boolean;
|
||||
instanceUrl: string;
|
||||
onCopyInstanceUrl: () => void;
|
||||
onCreateServiceAccount: () => void;
|
||||
}
|
||||
|
||||
function AuthCard({
|
||||
isAdmin,
|
||||
instanceUrl,
|
||||
onCopyInstanceUrl,
|
||||
onCreateServiceAccount,
|
||||
}: AuthCardProps): JSX.Element {
|
||||
return (
|
||||
<section className="mcp-auth-card">
|
||||
<h3 className="mcp-auth-card__title">
|
||||
<Badge color="secondary" variant="default">
|
||||
2
|
||||
</Badge>
|
||||
Authenticate from your client
|
||||
</h3>
|
||||
<p className="mcp-auth-card__description">
|
||||
On first connect, your client opens a SigNoz authorization page asking for
|
||||
two values:
|
||||
</p>
|
||||
|
||||
<div className="mcp-auth-card__field">
|
||||
<span className="mcp-auth-card__field-label">SigNoz Instance URL</span>
|
||||
<div className="mcp-auth-card__endpoint-value">
|
||||
<span data-testid="mcp-instance-url">{instanceUrl}</span>
|
||||
<CopyIconButton
|
||||
ariaLabel="Copy SigNoz instance URL"
|
||||
onCopy={onCopyInstanceUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mcp-auth-card__field">
|
||||
<span className="mcp-auth-card__field-label">API Key</span>
|
||||
{isAdmin ? (
|
||||
<div className="mcp-auth-card__cta-row">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
prefix={<KeyRound size={14} />}
|
||||
onClick={onCreateServiceAccount}
|
||||
>
|
||||
Create service account
|
||||
</Button>
|
||||
<span className="mcp-auth-card__helper-text">
|
||||
Create a service account, then add a new key inside it - paste that key
|
||||
into the API Key field.
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mcp-auth-card__info-banner">
|
||||
<Info size={14} />
|
||||
<span className="mcp-auth-card__helper-text">
|
||||
Only admins can create API keys. Ask your workspace admin for a key with
|
||||
read access, then paste it into the API Key field.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default AuthCard;
|
||||
@@ -1,66 +0,0 @@
|
||||
.mcp-client-tabs-root {
|
||||
button[data-variant='primary'] {
|
||||
border: none;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
// Remove default tab content padding/margin — the card provides spacing.
|
||||
--tab-content-padding: 0;
|
||||
--tab-content-margin: var(--spacing-4) 0 0;
|
||||
}
|
||||
|
||||
.mcp-client-tabs {
|
||||
&__snippet-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-6);
|
||||
margin-top: var(--spacing-2);
|
||||
|
||||
.learn-more {
|
||||
width: fit-content;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
}
|
||||
|
||||
&__install-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-10);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__helper-text {
|
||||
font-size: var(--paragraph-small-400-font-size);
|
||||
color: var(--foreground);
|
||||
line-height: var(--paragraph-small-400-line-height);
|
||||
}
|
||||
|
||||
&__endpoint-value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
padding: var(--padding-2) var(--padding-3);
|
||||
border: 1px solid var(--l3-border);
|
||||
border-radius: 6px;
|
||||
background: var(--l3-background);
|
||||
font-family: var(--font-family-sf-mono, monospace);
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
color: var(--l1-foreground);
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__snippet-pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&__instructions {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--foreground);
|
||||
line-height: var(--line-height-20);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import ClientTabs from './ClientTabs';
|
||||
import { MCP_CLIENTS } from '../clients';
|
||||
|
||||
jest.mock('utils/navigation', () => ({
|
||||
openInNewTab: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockOnTabChange = jest.fn();
|
||||
const mockOnCopySnippet = jest.fn();
|
||||
const mockOnInstallClick = jest.fn();
|
||||
const mockOnDocsLinkClick = jest.fn();
|
||||
|
||||
const MCP_ENDPOINT = 'https://mcp.us.signoz.cloud/mcp';
|
||||
|
||||
const defaultProps = {
|
||||
endpoint: MCP_ENDPOINT,
|
||||
activeTab: MCP_CLIENTS[0].key,
|
||||
onTabChange: mockOnTabChange,
|
||||
onCopySnippet: mockOnCopySnippet,
|
||||
onInstallClick: mockOnInstallClick,
|
||||
onDocsLinkClick: mockOnDocsLinkClick,
|
||||
};
|
||||
|
||||
describe('ClientTabs', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders a tab for each MCP client', () => {
|
||||
render(<ClientTabs {...defaultProps} />);
|
||||
|
||||
MCP_CLIENTS.forEach((client) => {
|
||||
expect(screen.getByText(client.label)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the snippet for clients that provide one (Cursor)', () => {
|
||||
render(<ClientTabs {...defaultProps} activeTab="cursor" />);
|
||||
|
||||
// The snippet is rendered inside a <pre> element; check its content
|
||||
const snippetPre = document.querySelector('.mcp-client-tabs__snippet-pre');
|
||||
expect(snippetPre).toBeInTheDocument();
|
||||
expect(snippetPre?.textContent).toContain(MCP_ENDPOINT);
|
||||
expect(snippetPre?.textContent).toContain('mcpServers');
|
||||
});
|
||||
|
||||
it('renders endpoint block and instructions text for clients without a snippet (Claude Desktop)', () => {
|
||||
render(<ClientTabs {...defaultProps} activeTab="claude-desktop" />);
|
||||
|
||||
const snippetPre = document.querySelector('.mcp-client-tabs__snippet-pre');
|
||||
expect(snippetPre?.textContent).toBe(MCP_ENDPOINT);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Open Claude Desktop, go to Settings → Connectors → Add custom connector, and paste the endpoint URL above. Claude Desktop does not read remote MCP servers from claude_desktop_config.json - the connector UI is the only supported path.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows enabled install button when endpoint is set (Cursor)', () => {
|
||||
render(<ClientTabs {...defaultProps} activeTab="cursor" />);
|
||||
|
||||
const installBtn = screen.getByRole('button', {
|
||||
name: 'Add to Cursor',
|
||||
});
|
||||
expect(installBtn).toBeEnabled();
|
||||
});
|
||||
|
||||
it('shows disabled install button when endpoint is missing (Cursor)', () => {
|
||||
render(<ClientTabs {...defaultProps} endpoint="" activeTab="cursor" />);
|
||||
|
||||
const installBtn = screen.getByRole('button', {
|
||||
name: 'Add to Cursor',
|
||||
});
|
||||
expect(installBtn).toBeDisabled();
|
||||
});
|
||||
|
||||
it('calls onCopySnippet with client key and snippet on copy', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const cursorClient = MCP_CLIENTS.find((c) => c.key === 'cursor')!;
|
||||
|
||||
render(<ClientTabs {...defaultProps} activeTab="cursor" />);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: `Copy ${cursorClient.label} config`,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockOnCopySnippet).toHaveBeenCalledWith(
|
||||
'cursor',
|
||||
cursorClient.snippet!(MCP_ENDPOINT),
|
||||
);
|
||||
});
|
||||
|
||||
it('copy button is disabled when no endpoint', () => {
|
||||
render(<ClientTabs {...defaultProps} endpoint="" activeTab="cursor" />);
|
||||
|
||||
const cursorClient = MCP_CLIENTS.find((c) => c.key === 'cursor')!;
|
||||
const copyBtn = screen.getByRole('button', {
|
||||
name: `Copy ${cursorClient.label} config`,
|
||||
});
|
||||
|
||||
expect(copyBtn).toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -1,126 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Button, Tabs } from '@signozhq/ui';
|
||||
import LearnMore from 'components/LearnMore/LearnMore';
|
||||
import { Download } from '@signozhq/icons';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import CopyIconButton from '../CopyIconButton';
|
||||
import { docsUrl, MCP_CLIENTS, McpClient } from '../clients';
|
||||
|
||||
import './ClientTabs.styles.scss';
|
||||
|
||||
const ENDPOINT_PLACEHOLDER = 'https://mcp.<region>.signoz.cloud/mcp';
|
||||
|
||||
interface ClientTabsProps {
|
||||
endpoint: string;
|
||||
activeTab: string;
|
||||
onTabChange: (key: string) => void;
|
||||
onCopySnippet: (clientKey: string, snippet: string) => void;
|
||||
onInstallClick: (clientKey: string) => void;
|
||||
onDocsLinkClick: (target: string) => void;
|
||||
}
|
||||
|
||||
function ClientTabs({
|
||||
endpoint,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
onCopySnippet,
|
||||
onInstallClick,
|
||||
onDocsLinkClick,
|
||||
}: ClientTabsProps): JSX.Element {
|
||||
const items = useMemo(
|
||||
() =>
|
||||
MCP_CLIENTS.map((client: McpClient) => {
|
||||
const snippet = client.snippet
|
||||
? client.snippet(endpoint || ENDPOINT_PLACEHOLDER)
|
||||
: null;
|
||||
const installHref =
|
||||
client.installUrl && endpoint ? client.installUrl(endpoint) : null;
|
||||
|
||||
const installLabel = client.installLabel ?? `Add to ${client.label}`;
|
||||
|
||||
return {
|
||||
key: client.key,
|
||||
label: client.label,
|
||||
children: (
|
||||
<div className="mcp-client-tabs__snippet-wrapper">
|
||||
{snippet !== null ? (
|
||||
<div className="mcp-client-tabs__endpoint-value mcp-client-tabs__snippet">
|
||||
<pre className="mcp-client-tabs__snippet-pre">{snippet}</pre>
|
||||
<CopyIconButton
|
||||
ariaLabel={`Copy ${client.label} config`}
|
||||
disabled={!endpoint}
|
||||
onCopy={(): void => onCopySnippet(client.key, snippet)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mcp-client-tabs__endpoint-value mcp-client-tabs__snippet">
|
||||
<pre className="mcp-client-tabs__snippet-pre">
|
||||
{endpoint || ENDPOINT_PLACEHOLDER}
|
||||
</pre>
|
||||
<CopyIconButton
|
||||
ariaLabel="Copy MCP endpoint"
|
||||
disabled={!endpoint}
|
||||
onCopy={(): void => onCopySnippet(client.key, endpoint)}
|
||||
/>
|
||||
</div>
|
||||
<p className="mcp-client-tabs__instructions">
|
||||
{client.instructions ?? ''}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{client.installUrl && (
|
||||
<div className="mcp-client-tabs__install-row">
|
||||
{installHref ? (
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
prefix={<Download size={14} />}
|
||||
onClick={(): void => {
|
||||
onInstallClick(client.key);
|
||||
openInNewTab(installHref);
|
||||
}}
|
||||
>
|
||||
{installLabel}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
disabled
|
||||
prefix={<Download size={14} />}
|
||||
>
|
||||
{installLabel}
|
||||
</Button>
|
||||
)}
|
||||
<span className="mcp-client-tabs__helper-text">
|
||||
Or copy the config below for manual setup.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<LearnMore
|
||||
text={`${client.label} setup docs`}
|
||||
url={docsUrl(client.docsPath)}
|
||||
onClick={(): void => onDocsLinkClick(`client-${client.key}`)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[endpoint, onCopySnippet, onInstallClick, onDocsLinkClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
className="mcp-client-tabs-root"
|
||||
value={activeTab}
|
||||
onChange={onTabChange}
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClientTabs;
|
||||
@@ -1,5 +0,0 @@
|
||||
.mcp-copy-btn {
|
||||
&:hover {
|
||||
background-color: var(--l3-background-hover) !important;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Button, Tooltip, TooltipProvider } from '@signozhq/ui';
|
||||
import { Copy } from '@signozhq/icons';
|
||||
import './CopyIconButton.styles.scss';
|
||||
|
||||
interface CopyIconButtonProps {
|
||||
ariaLabel: string;
|
||||
onCopy: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function CopyIconButton({
|
||||
ariaLabel,
|
||||
onCopy,
|
||||
disabled = false,
|
||||
}: CopyIconButtonProps): JSX.Element {
|
||||
const tooltipTitle = disabled
|
||||
? 'Enter your Cloud region first'
|
||||
: 'Copy to clipboard';
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip title={tooltipTitle}>
|
||||
<span>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={ariaLabel}
|
||||
disabled={disabled}
|
||||
className="mcp-copy-btn"
|
||||
prefix={<Copy size={14} />}
|
||||
onClick={onCopy}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default CopyIconButton;
|
||||
@@ -1,60 +0,0 @@
|
||||
.mcp-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-12);
|
||||
padding: var(--padding-8) var(--padding-2) var(--padding-6) var(--padding-4);
|
||||
max-width: 880px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
&__header-title {
|
||||
font-size: var(--label-large-500-font-size);
|
||||
font-weight: var(--label-large-500-font-weight);
|
||||
color: var(--l1-foreground);
|
||||
letter-spacing: -0.09px;
|
||||
line-height: var(--line-height-normal);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__header-subtitle {
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
font-weight: var(--paragraph-base-400-font-weight);
|
||||
color: var(--foreground);
|
||||
letter-spacing: -0.07px;
|
||||
line-height: var(--paragraph-base-400-line-height);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mcp-settings__card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--padding-4) var(--padding-5);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: 8px;
|
||||
background: var(--l2-background);
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-5);
|
||||
font-size: var(--label-medium-500-font-size);
|
||||
font-weight: var(--label-medium-500-font-weight);
|
||||
color: var(--l1-foreground);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&-description {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--foreground);
|
||||
line-height: var(--line-height-18);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user