mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-16 18:02:09 +00:00
Compare commits
1 Commits
feat/cloud
...
debug_time
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5320138eb9 |
5
.github/CODEOWNERS
vendored
5
.github/CODEOWNERS
vendored
@@ -127,15 +127,12 @@
|
|||||||
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
|
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
|
||||||
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
|
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
|
||||||
|
|
||||||
# Dashboard Widget Page
|
|
||||||
/frontend/src/pages/DashboardWidget/ @SigNoz/pulse-frontend
|
|
||||||
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
|
|
||||||
|
|
||||||
## Dashboard Page
|
## Dashboard Page
|
||||||
|
|
||||||
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
|
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
|
||||||
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
|
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
|
||||||
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
|
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
|
||||||
|
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
|
||||||
|
|
||||||
## Public Dashboard Page
|
## Public Dashboard Page
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,8 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/gateway"
|
"github.com/SigNoz/signoz/pkg/gateway"
|
||||||
"github.com/SigNoz/signoz/pkg/gateway/noopgateway"
|
"github.com/SigNoz/signoz/pkg/gateway/noopgateway"
|
||||||
"github.com/SigNoz/signoz/pkg/global"
|
|
||||||
"github.com/SigNoz/signoz/pkg/licensing"
|
"github.com/SigNoz/signoz/pkg/licensing"
|
||||||
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
|
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
|
||||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
|
|
||||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
@@ -91,9 +88,6 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
|||||||
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
|
func(ps factory.ProviderSettings, q querier.Querier, a analytics.Analytics) querier.Handler {
|
||||||
return querier.NewHandler(ps, q, a)
|
return querier.NewHandler(ps, q, a)
|
||||||
},
|
},
|
||||||
func(_ sqlstore.SQLStore, _ licensing.Licensing, _ zeus.Zeus, _ gateway.Gateway, _ global.Config) cloudintegration.Module {
|
|
||||||
return implcloudintegration.NewModule()
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorContext(ctx, "failed to create signoz", "error", err)
|
logger.ErrorContext(ctx, "failed to create signoz", "error", err)
|
||||||
|
|||||||
@@ -9,13 +9,12 @@ import (
|
|||||||
"github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn"
|
"github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn"
|
||||||
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
|
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
|
||||||
"github.com/SigNoz/signoz/ee/authz/openfgaauthz"
|
"github.com/SigNoz/signoz/ee/authz/openfgaauthz"
|
||||||
|
eequerier "github.com/SigNoz/signoz/ee/querier"
|
||||||
"github.com/SigNoz/signoz/ee/authz/openfgaschema"
|
"github.com/SigNoz/signoz/ee/authz/openfgaschema"
|
||||||
"github.com/SigNoz/signoz/ee/gateway/httpgateway"
|
"github.com/SigNoz/signoz/ee/gateway/httpgateway"
|
||||||
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
|
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
|
||||||
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
||||||
"github.com/SigNoz/signoz/ee/modules/cloudintegration/implcloudintegration"
|
|
||||||
"github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard"
|
"github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard"
|
||||||
eequerier "github.com/SigNoz/signoz/ee/querier"
|
|
||||||
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
|
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
|
||||||
"github.com/SigNoz/signoz/ee/sqlschema/postgressqlschema"
|
"github.com/SigNoz/signoz/ee/sqlschema/postgressqlschema"
|
||||||
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
|
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
|
||||||
@@ -26,10 +25,7 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/authz"
|
"github.com/SigNoz/signoz/pkg/authz"
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/gateway"
|
"github.com/SigNoz/signoz/pkg/gateway"
|
||||||
"github.com/SigNoz/signoz/pkg/global"
|
|
||||||
"github.com/SigNoz/signoz/pkg/licensing"
|
"github.com/SigNoz/signoz/pkg/licensing"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
|
||||||
pkgimplcloudintegration "github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
|
|
||||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||||
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
@@ -133,9 +129,6 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
|||||||
communityHandler := querier.NewHandler(ps, q, a)
|
communityHandler := querier.NewHandler(ps, q, a)
|
||||||
return eequerier.NewHandler(ps, q, communityHandler)
|
return eequerier.NewHandler(ps, q, communityHandler)
|
||||||
},
|
},
|
||||||
func(store sqlstore.SQLStore, lic licensing.Licensing, z zeus.Zeus, gw gateway.Gateway, gc global.Config) cloudintegration.Module {
|
|
||||||
return implcloudintegration.NewModule(pkgimplcloudintegration.NewStore(store), store, lic, z, gw, gc)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package implcloudintegration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
|
||||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
|
||||||
)
|
|
||||||
|
|
||||||
type awsProvider struct{}
|
|
||||||
|
|
||||||
func (p *awsProvider) CreateArtifact(
|
|
||||||
_ context.Context,
|
|
||||||
_ valuer.UUID,
|
|
||||||
_ *cloudintegrationtypes.ConnectionArtifactRequest,
|
|
||||||
_ cloudintegration.Credentials,
|
|
||||||
_ valuer.UUID,
|
|
||||||
) (*cloudintegrationtypes.ConnectionArtifact, error) {
|
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package implcloudintegration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
|
||||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
|
||||||
)
|
|
||||||
|
|
||||||
// keeping this for example to show how more cloud providers will be added
|
|
||||||
type azureProvider struct{}
|
|
||||||
|
|
||||||
func (p *azureProvider) CreateArtifact(
|
|
||||||
_ context.Context,
|
|
||||||
_ valuer.UUID,
|
|
||||||
_ *cloudintegrationtypes.ConnectionArtifactRequest,
|
|
||||||
_ cloudintegration.Credentials,
|
|
||||||
_ valuer.UUID,
|
|
||||||
) (*cloudintegrationtypes.ConnectionArtifact, error) {
|
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
|
|
||||||
}
|
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
package implcloudintegration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
|
||||||
"github.com/SigNoz/signoz/pkg/gateway"
|
|
||||||
"github.com/SigNoz/signoz/pkg/global"
|
|
||||||
"github.com/SigNoz/signoz/pkg/licensing"
|
|
||||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types/zeustypes"
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
|
||||||
"github.com/SigNoz/signoz/pkg/zeus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type module struct {
|
|
||||||
store cloudintegrationtypes.Store
|
|
||||||
userStore types.UserStore
|
|
||||||
licensing licensing.Licensing
|
|
||||||
zeus zeus.Zeus
|
|
||||||
gateway gateway.Gateway
|
|
||||||
globalConfig global.Config
|
|
||||||
providers map[cloudintegrationtypes.CloudProviderType]cloudintegration.CloudProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewModule(
|
|
||||||
store cloudintegrationtypes.Store,
|
|
||||||
sqlStore sqlstore.SQLStore,
|
|
||||||
lic licensing.Licensing,
|
|
||||||
z zeus.Zeus,
|
|
||||||
gw gateway.Gateway,
|
|
||||||
gc global.Config,
|
|
||||||
) cloudintegration.Module {
|
|
||||||
return &module{
|
|
||||||
store: store,
|
|
||||||
userStore: impluser.NewStore(sqlStore, factory.ProviderSettings{}),
|
|
||||||
licensing: lic,
|
|
||||||
zeus: z,
|
|
||||||
gateway: gw,
|
|
||||||
globalConfig: gc,
|
|
||||||
providers: map[cloudintegrationtypes.CloudProviderType]cloudintegration.CloudProvider{
|
|
||||||
cloudintegrationtypes.CloudProviderTypeAWS: &awsProvider{},
|
|
||||||
cloudintegrationtypes.CloudProviderTypeAzure: &azureProvider{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) CreateConnectionArtifact(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, request *cloudintegrationtypes.ConnectionArtifactRequest) (*cloudintegrationtypes.ConnectionArtifact, error) {
|
|
||||||
p, ok := m.providers[provider]
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.NewInvalidInputf(cloudintegrationtypes.ErrCodeCloudProviderInvalidInput, "unsupported cloud provider: %s", provider.StringValue())
|
|
||||||
}
|
|
||||||
|
|
||||||
creds, err := m.resolveCredentials(ctx, orgID, provider)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
newAccountID := valuer.GenerateUUID()
|
|
||||||
|
|
||||||
artifact, err := p.CreateArtifact(ctx, orgID, request, creds, newAccountID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
account := &cloudintegrationtypes.StorableCloudIntegration{
|
|
||||||
Identifiable: types.Identifiable{ID: newAccountID},
|
|
||||||
TimeAuditable: types.TimeAuditable{
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
},
|
|
||||||
Provider: provider,
|
|
||||||
OrgID: orgID,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.store.UpsertAccount(ctx, account); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return artifact, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) resolveCredentials(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (cloudintegration.Credentials, error) {
|
|
||||||
creds := cloudintegration.Credentials{}
|
|
||||||
|
|
||||||
pat, err := m.getOrCreateIntegrationPAT(ctx, orgID, provider)
|
|
||||||
if err != nil {
|
|
||||||
return creds, err
|
|
||||||
}
|
|
||||||
creds.SigNozAPIKey = pat
|
|
||||||
|
|
||||||
if m.licensing == nil {
|
|
||||||
return creds, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
license, err := m.licensing.GetActive(ctx, orgID)
|
|
||||||
if err != nil {
|
|
||||||
return creds, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if license == nil {
|
|
||||||
return creds, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
respBytes, err := m.zeus.GetDeployment(ctx, license.Key)
|
|
||||||
if err != nil {
|
|
||||||
return creds, errors.NewInternalf(errors.CodeInternal, "couldn't query deployment info: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
deployment, err := zeustypes.NewGettableDeployment(respBytes)
|
|
||||||
if err != nil {
|
|
||||||
return creds, err
|
|
||||||
}
|
|
||||||
creds.SigNozAPIUrl = deployment.SignozAPIUrl
|
|
||||||
|
|
||||||
if m.globalConfig.IngestionURL != nil {
|
|
||||||
creds.IngestionUrl = m.globalConfig.IngestionURL.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.gateway != nil {
|
|
||||||
ingestionKeyName := fmt.Sprintf("%s-integration", provider.StringValue())
|
|
||||||
ingestionKey, err := m.getOrCreateIngestionKey(ctx, orgID, ingestionKeyName)
|
|
||||||
if err != nil {
|
|
||||||
return creds, err
|
|
||||||
}
|
|
||||||
creds.IngestionKey = ingestionKey
|
|
||||||
}
|
|
||||||
|
|
||||||
return creds, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) getOrCreateIngestionKey(ctx context.Context, orgID valuer.UUID, keyName string) (string, error) {
|
|
||||||
result, err := m.gateway.SearchIngestionKeysByName(ctx, orgID, keyName, 1, 10)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.NewInternalf(errors.CodeInternal, "couldn't search ingestion keys: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, k := range result.Keys {
|
|
||||||
if k.Name == keyName {
|
|
||||||
return k.Value, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
created, err := m.gateway.CreateIngestionKey(ctx, orgID, keyName, []string{"integration"}, time.Time{})
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.NewInternalf(errors.CodeInternal, "couldn't create ingestion key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return created.Value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) getOrCreateIntegrationPAT(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (string, error) {
|
|
||||||
integrationPATName := fmt.Sprintf("%s integration", provider.StringValue())
|
|
||||||
|
|
||||||
integrationUser, err := m.getOrCreateIntegrationUser(ctx, orgID, provider)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
allPATs, err := m.userStore.ListAPIKeys(ctx, orgID)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.NewInternalf(errors.CodeInternal, "couldn't list PATs: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range allPATs {
|
|
||||||
if p.UserID == integrationUser.ID && p.Name == integrationPATName {
|
|
||||||
return p.Token, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newPAT, err := types.NewStorableAPIKey(
|
|
||||||
integrationPATName,
|
|
||||||
integrationUser.ID,
|
|
||||||
types.RoleViewer,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.NewInternalf(errors.CodeInternal, "couldn't create cloud integration PAT: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.userStore.CreateAPIKey(ctx, newPAT); err != nil {
|
|
||||||
return "", errors.NewInternalf(errors.CodeInternal, "couldn't persist cloud integration PAT: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return newPAT.Token, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) getOrCreateIntegrationUser(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (*types.User, error) {
|
|
||||||
cloudIntegrationUserName := fmt.Sprintf("%s-integration", provider.StringValue())
|
|
||||||
email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", cloudIntegrationUserName))
|
|
||||||
|
|
||||||
existingUsers, err := m.userStore.GetUsersByEmailAndOrgID(ctx, email, orgID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.NewInternalf(errors.CodeInternal, "couldn't look up integration user: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, u := range existingUsers {
|
|
||||||
if u.Status != types.UserStatusDeleted {
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, orgID, types.UserStatusActive)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.NewInternalf(errors.CodeInternal, "couldn't construct integration user: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
password := types.MustGenerateFactorPassword(cloudIntegrationUser.ID.StringValue())
|
|
||||||
|
|
||||||
if err := m.userStore.CreateUser(ctx, cloudIntegrationUser); err != nil {
|
|
||||||
return nil, errors.NewInternalf(errors.CodeInternal, "couldn't create integration user: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.userStore.CreatePassword(ctx, password); err != nil {
|
|
||||||
return nil, errors.NewInternalf(errors.CodeInternal, "couldn't create integration user password: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cloudIntegrationUser, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) GetAccountStatus(_ context.Context, _, _ valuer.UUID) (*cloudintegrationtypes.AccountStatus, error) {
|
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) ListConnectedAccounts(_ context.Context, _ valuer.UUID) (*cloudintegrationtypes.ConnectedAccounts, error) {
|
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) DisconnectAccount(_ context.Context, _, _ valuer.UUID) error {
|
|
||||||
return errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) UpdateAccountConfig(_ context.Context, _, _ valuer.UUID, _ *cloudintegrationtypes.UpdateAccountConfigRequest) (*cloudintegrationtypes.Account, error) {
|
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) ListServicesSummary(_ context.Context, _ valuer.UUID, _ *valuer.UUID) (*cloudintegrationtypes.ServicesSummary, error) {
|
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) GetService(_ context.Context, _ valuer.UUID, _ string, _ *valuer.UUID) (*cloudintegrationtypes.Service, error) {
|
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) UpdateServiceConfig(_ context.Context, _ string, _ valuer.UUID, _ *cloudintegrationtypes.UpdateServiceConfigRequest) (*cloudintegrationtypes.ServiceSummary, error) {
|
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) AgentCheckIn(_ context.Context, _ valuer.UUID, _ *cloudintegrationtypes.AgentCheckInRequest) (cloudintegrationtypes.AgentCheckInResponse, error) {
|
|
||||||
return cloudintegrationtypes.AgentCheckInResponse{}, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) GetDashboardByID(_ context.Context, _ string, _ valuer.UUID) (*dashboardtypes.Dashboard, error) {
|
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) GetAllDashboards(_ context.Context, _ valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
|
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "not implemented")
|
|
||||||
}
|
|
||||||
@@ -302,6 +302,7 @@ function CustomTimePicker({
|
|||||||
): void => {
|
): void => {
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
event?.stopPropagation();
|
event?.stopPropagation();
|
||||||
|
|
||||||
// check if the entered time is in the format of 1m, 2h, 3d, 4w
|
// check if the entered time is in the format of 1m, 2h, 3d, 4w
|
||||||
const isTimeDurationShortHandFormat = /^(\d+)([mhdw])$/.test(inputValue);
|
const isTimeDurationShortHandFormat = /^(\d+)([mhdw])$/.test(inputValue);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from 'hooks/dashboard/useDashboardVariables';
|
} from 'hooks/dashboard/useDashboardVariables';
|
||||||
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
||||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
|
||||||
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
||||||
import {
|
import {
|
||||||
enqueueDescendantsOfVariable,
|
enqueueDescendantsOfVariable,
|
||||||
@@ -29,7 +30,7 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
updateLocalStorageDashboardVariables,
|
updateLocalStorageDashboardVariables,
|
||||||
} = useDashboard();
|
} = useDashboard();
|
||||||
|
|
||||||
const { updateUrlVariable } = useVariablesFromUrl();
|
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
|
||||||
|
|
||||||
const { dashboardVariables } = useDashboardVariables();
|
const { dashboardVariables } = useDashboardVariables();
|
||||||
const dashboardId = useDashboardVariablesSelector(
|
const dashboardId = useDashboardVariablesSelector(
|
||||||
@@ -49,6 +50,15 @@ function DashboardVariableSelection(): JSX.Element | null {
|
|||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Initialize variables with default values if not in URL
|
||||||
|
initializeDefaultVariables(
|
||||||
|
dashboardVariables,
|
||||||
|
getUrlVariables,
|
||||||
|
updateUrlVariable,
|
||||||
|
);
|
||||||
|
}, [getUrlVariables, updateUrlVariable, dashboardVariables]);
|
||||||
|
|
||||||
// Memoize the order key to avoid unnecessary triggers
|
// Memoize the order key to avoid unnecessary triggers
|
||||||
const variableOrderKey = useMemo(() => {
|
const variableOrderKey = useMemo(() => {
|
||||||
const queryVariableOrderKey = dependencyData?.order?.join(',') ?? '';
|
const queryVariableOrderKey = dependencyData?.order?.join(',') ?? '';
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
.column-unit-selector {
|
.column-unit-selector {
|
||||||
display: flex;
|
margin-top: 16px;
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
@@ -32,11 +30,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
|
|||||||
@@ -72,24 +72,22 @@ export function ColumnUnitSelector(
|
|||||||
return (
|
return (
|
||||||
<section className="column-unit-selector">
|
<section className="column-unit-selector">
|
||||||
<Typography.Text className="heading">Column Units</Typography.Text>
|
<Typography.Text className="heading">Column Units</Typography.Text>
|
||||||
<div className="column-unit-selector-content">
|
{aggregationQueries.map(({ value, label }) => {
|
||||||
{aggregationQueries.map(({ value, label }) => {
|
const baseQueryName = value.split('.')[0];
|
||||||
const baseQueryName = value.split('.')[0];
|
return (
|
||||||
return (
|
<YAxisUnitSelectorV2
|
||||||
<YAxisUnitSelectorV2
|
value={columnUnits[value] || ''}
|
||||||
value={columnUnits[value] || ''}
|
onSelect={(unitValue: string): void =>
|
||||||
onSelect={(unitValue: string): void =>
|
handleColumnUnitSelect(value, unitValue)
|
||||||
handleColumnUnitSelect(value, unitValue)
|
}
|
||||||
}
|
fieldLabel={label}
|
||||||
fieldLabel={label}
|
key={value}
|
||||||
key={value}
|
selectedQueryName={baseQueryName}
|
||||||
selectedQueryName={baseQueryName}
|
// Update the column unit value automatically only in create mode
|
||||||
// Update the column unit value automatically only in create mode
|
shouldUpdateYAxisUnit={isNewDashboard}
|
||||||
shouldUpdateYAxisUnit={isNewDashboard}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ describe('ContextLinks Component', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check that the component renders
|
||||||
|
expect(screen.getByText('Context Links')).toBeInTheDocument();
|
||||||
|
|
||||||
// Check that the add button is present
|
// Check that the add button is present
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole('button', { name: /context link/i }),
|
screen.getByRole('button', { name: /context link/i }),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
verticalListSortingStrategy,
|
verticalListSortingStrategy,
|
||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { Button, Modal } from 'antd';
|
import { Button, Modal, Typography } from 'antd';
|
||||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
import { GripVertical, Pencil, Plus, Trash2 } from 'lucide-react';
|
import { GripVertical, Pencil, Plus, Trash2 } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
@@ -134,16 +134,11 @@ function ContextLinks({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="context-links-container">
|
<div className="context-links-container">
|
||||||
|
<Typography.Text className="context-links-text">
|
||||||
|
Context Links
|
||||||
|
</Typography.Text>
|
||||||
|
|
||||||
<div className="context-links-list">
|
<div className="context-links-list">
|
||||||
<Button
|
|
||||||
type="default"
|
|
||||||
className="add-context-link-button"
|
|
||||||
icon={<Plus size={12} />}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
onClick={handleAddContextLink}
|
|
||||||
>
|
|
||||||
Add Context Link
|
|
||||||
</Button>
|
|
||||||
<OverlayScrollbar>
|
<OverlayScrollbar>
|
||||||
<DndContext
|
<DndContext
|
||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
@@ -165,6 +160,16 @@ function ContextLinks({
|
|||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
</OverlayScrollbar>
|
</OverlayScrollbar>
|
||||||
|
|
||||||
|
{/* button to add context link */}
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
className="add-context-link-button"
|
||||||
|
icon={<Plus size={12} />}
|
||||||
|
onClick={handleAddContextLink}
|
||||||
|
>
|
||||||
|
Context Link
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
margin: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-links-text {
|
.context-links-text {
|
||||||
@@ -109,7 +110,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.add-context-link-button {
|
.add-context-link-button {
|
||||||
width: 100%;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: auto;
|
||||||
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
.right-container {
|
.right-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-bottom: 48px;
|
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -25,14 +24,14 @@
|
|||||||
letter-spacing: -0.07px;
|
letter-spacing: -0.07px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.control-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-description {
|
.name-description {
|
||||||
padding: 0 0 4px 0;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 12px 12px 16px 12px;
|
||||||
|
border-top: 1px solid var(--bg-slate-500);
|
||||||
|
border-bottom: 1px solid var(--bg-slate-500);
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
.typography {
|
.typography {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
@@ -89,6 +88,9 @@
|
|||||||
.panel-config {
|
.panel-config {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding: 12px 12px 16px 12px;
|
||||||
|
gap: 8px;
|
||||||
|
border-bottom: 1px solid var(--bg-slate-500);
|
||||||
|
|
||||||
.typography {
|
.typography {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
@@ -102,7 +104,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel-type-select {
|
.panel-type-select {
|
||||||
width: 100%;
|
|
||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@@ -136,6 +137,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.fill-gaps {
|
.fill-gaps {
|
||||||
|
margin-top: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -154,24 +156,31 @@
|
|||||||
letter-spacing: 0.52px;
|
letter-spacing: 0.52px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.fill-gaps-text-description {
|
|
||||||
color: var(--bg-vanilla-400);
|
|
||||||
font-family: Inter;
|
|
||||||
font-size: 12px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
opacity: 0.6;
|
|
||||||
line-height: 16px; /* 133.333% */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-scale,
|
.log-scale,
|
||||||
.decimal-precision-selector,
|
.decimal-precision-selector {
|
||||||
.legend-position {
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-position {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-colors {
|
||||||
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-time-text {
|
.panel-time-text {
|
||||||
|
margin-top: 16px;
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
font-family: 'Space Mono';
|
font-family: 'Space Mono';
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -184,6 +193,7 @@
|
|||||||
|
|
||||||
.y-axis-unit-selector,
|
.y-axis-unit-selector,
|
||||||
.y-axis-unit-selector-v2 {
|
.y-axis-unit-selector-v2 {
|
||||||
|
margin-top: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -268,8 +278,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stack-chart {
|
.stack-chart {
|
||||||
flex-direction: row;
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
font-family: 'Space Mono';
|
font-family: 'Space Mono';
|
||||||
@@ -283,6 +296,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bucket-config {
|
.bucket-config {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
font-family: 'Space Mono';
|
font-family: 'Space Mono';
|
||||||
@@ -334,13 +352,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.context-links {
|
||||||
|
border-bottom: 1px solid var(--bg-slate-500);
|
||||||
|
}
|
||||||
|
|
||||||
.alerts {
|
.alerts {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
padding: 12px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 12px;
|
border-bottom: 1px solid var(--bg-slate-500);
|
||||||
min-height: 44px;
|
|
||||||
border-top: 1px solid var(--bg-slate-500);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
.left-section {
|
.left-section {
|
||||||
@@ -366,16 +387,6 @@
|
|||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.context-links {
|
|
||||||
padding: 12px 12px 16px 12px;
|
|
||||||
border-bottom: 1px solid var(--bg-slate-500);
|
|
||||||
}
|
|
||||||
|
|
||||||
.thresholds-section {
|
|
||||||
padding: 12px 12px 16px 12px;
|
|
||||||
border-top: 1px solid var(--bg-slate-500);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-option {
|
.select-option {
|
||||||
@@ -407,6 +418,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.name-description {
|
.name-description {
|
||||||
|
border-top: 1px solid var(--bg-vanilla-300);
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
.typography {
|
.typography {
|
||||||
color: var(--bg-ink-400);
|
color: var(--bg-ink-400);
|
||||||
}
|
}
|
||||||
@@ -427,6 +441,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel-config {
|
.panel-config {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
.typography {
|
.typography {
|
||||||
color: var(--bg-ink-400);
|
color: var(--bg-ink-400);
|
||||||
}
|
}
|
||||||
@@ -462,9 +478,6 @@
|
|||||||
.fill-gaps-text {
|
.fill-gaps-text {
|
||||||
color: var(--bg-ink-400);
|
color: var(--bg-ink-400);
|
||||||
}
|
}
|
||||||
.fill-gaps-text-description {
|
|
||||||
color: var(--bg-ink-400);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bucket-config {
|
.bucket-config {
|
||||||
@@ -517,7 +530,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alerts {
|
.alerts {
|
||||||
border-top: 1px solid var(--bg-vanilla-300);
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
.left-section {
|
.left-section {
|
||||||
.bell-icon {
|
.bell-icon {
|
||||||
@@ -536,10 +549,6 @@
|
|||||||
.context-links {
|
.context-links {
|
||||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
.thresholds-section {
|
|
||||||
border-top: 1px solid var(--bg-vanilla-300);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-option {
|
.select-option {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
.threshold-selector-container {
|
.threshold-selector-container {
|
||||||
|
padding: 12px;
|
||||||
padding-bottom: 80px;
|
padding-bottom: 80px;
|
||||||
|
|
||||||
.threshold-select {
|
.threshold-select {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import { Button } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useGetQueryLabels } from 'hooks/useGetQueryLabels';
|
import { useGetQueryLabels } from 'hooks/useGetQueryLabels';
|
||||||
import { Plus } from 'lucide-react';
|
import { Antenna, Plus } from 'lucide-react';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import Threshold from './Threshold';
|
import Threshold from './Threshold';
|
||||||
@@ -68,14 +68,11 @@ function ThresholdSelector({
|
|||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<div className="threshold-selector-container">
|
<div className="threshold-selector-container">
|
||||||
<div className="threshold-select" onClick={addThresholdHandler}>
|
<div className="threshold-select" onClick={addThresholdHandler}>
|
||||||
<Button
|
<div className="left-section">
|
||||||
type="default"
|
<Antenna size={14} className="icon" />
|
||||||
icon={<Plus size={14} />}
|
<Typography.Text className="text">Thresholds</Typography.Text>
|
||||||
style={{ width: '100%' }}
|
</div>
|
||||||
onClick={addThresholdHandler}
|
<Plus size={14} onClick={addThresholdHandler} className="icon" />
|
||||||
>
|
|
||||||
Add Threshold
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
{thresholds.map((threshold, idx) => (
|
{thresholds.map((threshold, idx) => (
|
||||||
<Threshold
|
<Threshold
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
.settings-section {
|
|
||||||
border-top: 1px solid var(--bg-slate-500);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-section-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px 12px;
|
|
||||||
min-height: 44px;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.settings-section-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
color: var(--bg-vanilla-400);
|
|
||||||
font-family: 'Space Mono';
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 400;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chevron-icon {
|
|
||||||
color: var(--bg-vanilla-400);
|
|
||||||
transition: transform 0.2s ease-in-out;
|
|
||||||
|
|
||||||
&.open {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-section-content {
|
|
||||||
padding: 0 12px 0 12px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
max-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
opacity: 0;
|
|
||||||
transition: max-height 0.25s ease, opacity 0.25s ease, padding 0.25s ease;
|
|
||||||
|
|
||||||
&.open {
|
|
||||||
padding-bottom: 24px;
|
|
||||||
max-height: 1000px;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.lightMode {
|
|
||||||
.settings-section-header {
|
|
||||||
.chevron-icon {
|
|
||||||
color: var(--bg-ink-400);
|
|
||||||
}
|
|
||||||
.settings-section-title {
|
|
||||||
color: var(--bg-ink-400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-section {
|
|
||||||
border-top: 1px solid var(--bg-vanilla-300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { ReactNode, useState } from 'react';
|
|
||||||
import { ChevronDown } from 'lucide-react';
|
|
||||||
|
|
||||||
import './SettingsSection.styles.scss';
|
|
||||||
|
|
||||||
export interface SettingsSectionProps {
|
|
||||||
title: string;
|
|
||||||
defaultOpen?: boolean;
|
|
||||||
children: ReactNode;
|
|
||||||
icon?: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SettingsSection({
|
|
||||||
title,
|
|
||||||
defaultOpen = false,
|
|
||||||
children,
|
|
||||||
icon,
|
|
||||||
}: SettingsSectionProps): JSX.Element {
|
|
||||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
||||||
|
|
||||||
const toggleOpen = (): void => {
|
|
||||||
setIsOpen((prev) => !prev);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="settings-section">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="settings-section-header"
|
|
||||||
onClick={toggleOpen}
|
|
||||||
>
|
|
||||||
<span className="settings-section-title">
|
|
||||||
{icon ? icon : null} {title}
|
|
||||||
</span>
|
|
||||||
<ChevronDown
|
|
||||||
size={16}
|
|
||||||
className={isOpen ? 'chevron-icon open' : 'chevron-icon'}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
isOpen ? 'settings-section-content open' : 'settings-section-content'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SettingsSection;
|
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Select,
|
Select,
|
||||||
|
Space,
|
||||||
Switch,
|
Switch,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
@@ -27,16 +28,9 @@ import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
|||||||
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import {
|
import {
|
||||||
Antenna,
|
|
||||||
Axis3D,
|
|
||||||
ConciergeBell,
|
ConciergeBell,
|
||||||
Layers,
|
|
||||||
LayoutDashboard,
|
|
||||||
LineChart,
|
LineChart,
|
||||||
Link,
|
|
||||||
Pencil,
|
|
||||||
Plus,
|
Plus,
|
||||||
SlidersHorizontal,
|
|
||||||
Spline,
|
Spline,
|
||||||
SquareArrowOutUpRight,
|
SquareArrowOutUpRight,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
@@ -52,7 +46,6 @@ import { DataSource } from 'types/common/queryBuilder';
|
|||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
import { ColumnUnitSelector } from './ColumnUnitSelector/ColumnUnitSelector';
|
import { ColumnUnitSelector } from './ColumnUnitSelector/ColumnUnitSelector';
|
||||||
import SettingsSection from './components/SettingsSection/SettingsSection';
|
|
||||||
import {
|
import {
|
||||||
panelTypeVsBucketConfig,
|
panelTypeVsBucketConfig,
|
||||||
panelTypeVsColumnUnitPreferences,
|
panelTypeVsColumnUnitPreferences,
|
||||||
@@ -185,21 +178,6 @@ function RightContainer({
|
|||||||
}));
|
}));
|
||||||
}, [dashboardVariables]);
|
}, [dashboardVariables]);
|
||||||
|
|
||||||
const isAxisSectionVisible = useMemo(() => allowSoftMinMax || allowLogScale, [
|
|
||||||
allowSoftMinMax,
|
|
||||||
allowLogScale,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const isFormattingSectionVisible = useMemo(
|
|
||||||
() => allowYAxisUnit || allowDecimalPrecision || allowPanelColumnPreference,
|
|
||||||
[allowYAxisUnit, allowDecimalPrecision, allowPanelColumnPreference],
|
|
||||||
);
|
|
||||||
|
|
||||||
const isLegendSectionVisible = useMemo(
|
|
||||||
() => allowLegendPosition || allowLegendColors,
|
|
||||||
[allowLegendPosition, allowLegendColors],
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateCursorAndDropdown = (value: string, pos: number): void => {
|
const updateCursorAndDropdown = (value: string, pos: number): void => {
|
||||||
setCursorPos(pos);
|
setCursorPos(pos);
|
||||||
const lastDollar = value.lastIndexOf('$', pos - 1);
|
const lastDollar = value.lastIndexOf('$', pos - 1);
|
||||||
@@ -215,15 +193,6 @@ function RightContainer({
|
|||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const decimapPrecisionOptions = useMemo(() => {
|
|
||||||
return [
|
|
||||||
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
|
|
||||||
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
|
|
||||||
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
|
|
||||||
{ label: '3 decimals', value: PrecisionOptionsEnum.THREE },
|
|
||||||
];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleInputCursor = (): void => {
|
const handleInputCursor = (): void => {
|
||||||
const pos = inputRef.current?.input?.selectionStart ?? 0;
|
const pos = inputRef.current?.input?.selectionStart ?? 0;
|
||||||
updateCursorAndDropdown(inputValue, pos);
|
updateCursorAndDropdown(inputValue, pos);
|
||||||
@@ -294,297 +263,269 @@ function RightContainer({
|
|||||||
<div className="right-container">
|
<div className="right-container">
|
||||||
<section className="header">
|
<section className="header">
|
||||||
<div className="purple-dot" />
|
<div className="purple-dot" />
|
||||||
<Typography.Text className="header-text">Panel Settings</Typography.Text>
|
<Typography.Text className="header-text">Panel details</Typography.Text>
|
||||||
</section>
|
</section>
|
||||||
|
<section className="name-description">
|
||||||
<SettingsSection title="General" defaultOpen icon={<Pencil size={14} />}>
|
<Typography.Text className="typography">Name</Typography.Text>
|
||||||
<section className="name-description control-container">
|
<AutoComplete
|
||||||
<Typography.Text className="typography">Name</Typography.Text>
|
options={dashboardVariableOptions}
|
||||||
<AutoComplete
|
value={inputValue}
|
||||||
options={dashboardVariableOptions}
|
onChange={onInputChange}
|
||||||
value={inputValue}
|
onSelect={onSelect}
|
||||||
onChange={onInputChange}
|
filterOption={filterOption}
|
||||||
onSelect={onSelect}
|
style={{ width: '100%' }}
|
||||||
filterOption={filterOption}
|
getPopupContainer={popupContainer}
|
||||||
style={{ width: '100%' }}
|
placeholder="Enter the panel name here..."
|
||||||
getPopupContainer={popupContainer}
|
open={autoCompleteOpen}
|
||||||
placeholder="Enter the panel name here..."
|
|
||||||
open={autoCompleteOpen}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
rootClassName="name-input"
|
|
||||||
ref={inputRef}
|
|
||||||
onSelect={handleInputCursor}
|
|
||||||
onClick={handleInputCursor}
|
|
||||||
onBlur={(): void => setAutoCompleteOpen(false)}
|
|
||||||
/>
|
|
||||||
</AutoComplete>
|
|
||||||
<Typography.Text className="typography">Description</Typography.Text>
|
|
||||||
<TextArea
|
|
||||||
placeholder="Enter the panel description here..."
|
|
||||||
bordered
|
|
||||||
allowClear
|
|
||||||
value={description}
|
|
||||||
onChange={(event): void =>
|
|
||||||
onChangeHandler(setDescription, event.target.value)
|
|
||||||
}
|
|
||||||
rootClassName="description-input"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</SettingsSection>
|
|
||||||
|
|
||||||
<section className="panel-config">
|
|
||||||
<SettingsSection
|
|
||||||
title="Visualization"
|
|
||||||
defaultOpen
|
|
||||||
icon={<LayoutDashboard size={14} />}
|
|
||||||
>
|
>
|
||||||
<section className="panel-type control-container">
|
<Input
|
||||||
<Typography.Text className="typography">Panel Type</Typography.Text>
|
rootClassName="name-input"
|
||||||
<Select
|
ref={inputRef}
|
||||||
onChange={setGraphHandler}
|
onSelect={handleInputCursor}
|
||||||
value={selectedGraph}
|
onClick={handleInputCursor}
|
||||||
className="panel-type-select"
|
onBlur={(): void => setAutoCompleteOpen(false)}
|
||||||
data-testid="panel-change-select"
|
/>
|
||||||
data-stacking-state={stackedBarChart ? 'true' : 'false'}
|
</AutoComplete>
|
||||||
>
|
<Typography.Text className="typography">Description</Typography.Text>
|
||||||
{graphTypes.map((item) => (
|
<TextArea
|
||||||
<Option key={item.name} value={item.name}>
|
placeholder="Enter the panel description here..."
|
||||||
<div className="select-option">
|
bordered
|
||||||
<div className="icon">{item.icon}</div>
|
allowClear
|
||||||
<Typography.Text className="display">{item.display}</Typography.Text>
|
value={description}
|
||||||
</div>
|
onChange={(event): void =>
|
||||||
</Option>
|
onChangeHandler(setDescription, event.target.value)
|
||||||
))}
|
}
|
||||||
</Select>
|
rootClassName="description-input"
|
||||||
</section>
|
/>
|
||||||
|
</section>
|
||||||
{allowPanelTimePreference && (
|
<section className="panel-config">
|
||||||
<section className="panel-time-preference control-container">
|
<Typography.Text className="typography">Panel Type</Typography.Text>
|
||||||
<Typography.Text className="panel-time-text">
|
<Select
|
||||||
Panel Time Preference
|
onChange={setGraphHandler}
|
||||||
</Typography.Text>
|
value={selectedGraph}
|
||||||
<TimePreference
|
style={{ width: '100%' }}
|
||||||
{...{
|
className="panel-type-select"
|
||||||
selectedTime,
|
data-testid="panel-change-select"
|
||||||
setSelectedTime,
|
data-stacking-state={stackedBarChart ? 'true' : 'false'}
|
||||||
}}
|
>
|
||||||
/>
|
{graphTypes.map((item) => (
|
||||||
</section>
|
<Option key={item.name} value={item.name}>
|
||||||
)}
|
<div className="select-option">
|
||||||
|
<div className="icon">{item.icon}</div>
|
||||||
{allowStackingBarChart && (
|
<Typography.Text className="display">{item.display}</Typography.Text>
|
||||||
<section className="stack-chart control-container">
|
|
||||||
<Typography.Text className="label">Stack series</Typography.Text>
|
|
||||||
<Switch
|
|
||||||
checked={stackedBarChart}
|
|
||||||
size="small"
|
|
||||||
onChange={(checked): void => setStackedBarChart(checked)}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{allowFillSpans && (
|
|
||||||
<section className="fill-gaps">
|
|
||||||
<div className="fill-gaps-text-container">
|
|
||||||
<Typography className="fill-gaps-text">Fill gaps</Typography>
|
|
||||||
<Typography.Text className="fill-gaps-text-description">
|
|
||||||
Fill gaps in data with 0 for continuity
|
|
||||||
</Typography.Text>
|
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
</Option>
|
||||||
checked={isFillSpans}
|
))}
|
||||||
size="small"
|
</Select>
|
||||||
onChange={(checked): void => setIsFillSpans(checked)}
|
|
||||||
|
{allowFillSpans && (
|
||||||
|
<Space className="fill-gaps">
|
||||||
|
<Typography className="fill-gaps-text">Fill gaps</Typography>
|
||||||
|
<Switch
|
||||||
|
checked={isFillSpans}
|
||||||
|
size="small"
|
||||||
|
onChange={(checked): void => setIsFillSpans(checked)}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{allowPanelTimePreference && (
|
||||||
|
<>
|
||||||
|
<Typography.Text className="panel-time-text">
|
||||||
|
Panel Time Preference
|
||||||
|
</Typography.Text>
|
||||||
|
<TimePreference
|
||||||
|
{...{
|
||||||
|
selectedTime,
|
||||||
|
setSelectedTime,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{allowPanelColumnPreference && (
|
||||||
|
<ColumnUnitSelector
|
||||||
|
columnUnits={columnUnits}
|
||||||
|
setColumnUnits={setColumnUnits}
|
||||||
|
isNewDashboard={isNewDashboard}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{allowYAxisUnit && (
|
||||||
|
<DashboardYAxisUnitSelectorWrapper
|
||||||
|
onSelect={setYAxisUnit}
|
||||||
|
value={yAxisUnit || ''}
|
||||||
|
fieldLabel={
|
||||||
|
selectedGraphType === PanelDisplay.VALUE ||
|
||||||
|
selectedGraphType === PanelDisplay.PIE
|
||||||
|
? 'Unit'
|
||||||
|
: 'Y Axis Unit'
|
||||||
|
}
|
||||||
|
// Only update the y-axis unit value automatically in create mode
|
||||||
|
shouldUpdateYAxisUnit={isNewDashboard}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{allowDecimalPrecision && (
|
||||||
|
<section className="decimal-precision-selector">
|
||||||
|
<Typography.Text className="typography">
|
||||||
|
Decimal Precision
|
||||||
|
</Typography.Text>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
|
||||||
|
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
|
||||||
|
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
|
||||||
|
{ label: '3 decimals', value: PrecisionOptionsEnum.THREE },
|
||||||
|
{ label: '4 decimals', value: PrecisionOptionsEnum.FOUR },
|
||||||
|
{ label: 'Full Precision', value: PrecisionOptionsEnum.FULL },
|
||||||
|
]}
|
||||||
|
value={decimalPrecision}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
className="panel-type-select"
|
||||||
|
defaultValue={PrecisionOptionsEnum.TWO}
|
||||||
|
onChange={(val: PrecisionOption): void => setDecimalPrecision(val)}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{allowSoftMinMax && (
|
||||||
|
<section className="soft-min-max">
|
||||||
|
<section className="container">
|
||||||
|
<Typography.Text className="text">Soft Min</Typography.Text>
|
||||||
|
<InputNumber
|
||||||
|
type="number"
|
||||||
|
value={softMin}
|
||||||
|
onChange={softMinHandler}
|
||||||
|
rootClassName="input"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
)}
|
<section className="container">
|
||||||
</SettingsSection>
|
<Typography.Text className="text">Soft Max</Typography.Text>
|
||||||
|
<InputNumber
|
||||||
{isFormattingSectionVisible && (
|
value={softMax}
|
||||||
<SettingsSection
|
type="number"
|
||||||
title="Formatting & Units"
|
rootClassName="input"
|
||||||
icon={<SlidersHorizontal size={14} />}
|
onChange={softMaxHandler}
|
||||||
>
|
|
||||||
{allowYAxisUnit && (
|
|
||||||
<DashboardYAxisUnitSelectorWrapper
|
|
||||||
onSelect={setYAxisUnit}
|
|
||||||
value={yAxisUnit || ''}
|
|
||||||
fieldLabel={
|
|
||||||
selectedGraphType === PanelDisplay.VALUE ||
|
|
||||||
selectedGraphType === PanelDisplay.PIE
|
|
||||||
? 'Unit'
|
|
||||||
: 'Y Axis Unit'
|
|
||||||
}
|
|
||||||
// Only update the y-axis unit value automatically in create mode
|
|
||||||
shouldUpdateYAxisUnit={isNewDashboard}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</section>
|
||||||
|
</section>
|
||||||
{allowDecimalPrecision && (
|
|
||||||
<section className="decimal-precision-selector control-container">
|
|
||||||
<Typography.Text className="typography">
|
|
||||||
Decimal Precision
|
|
||||||
</Typography.Text>
|
|
||||||
<Select
|
|
||||||
options={decimapPrecisionOptions}
|
|
||||||
value={decimalPrecision}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
className="panel-type-select"
|
|
||||||
defaultValue={PrecisionOptionsEnum.TWO}
|
|
||||||
onChange={(val: PrecisionOption): void => setDecimalPrecision(val)}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{allowPanelColumnPreference && (
|
|
||||||
<ColumnUnitSelector
|
|
||||||
columnUnits={columnUnits}
|
|
||||||
setColumnUnits={setColumnUnits}
|
|
||||||
isNewDashboard={isNewDashboard}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</SettingsSection>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isAxisSectionVisible && (
|
{allowStackingBarChart && (
|
||||||
<SettingsSection title="Axes" icon={<Axis3D size={14} />}>
|
<section className="stack-chart">
|
||||||
{allowSoftMinMax && (
|
<Typography.Text className="label">Stack series</Typography.Text>
|
||||||
<section className="soft-min-max">
|
<Switch
|
||||||
<section className="container">
|
checked={stackedBarChart}
|
||||||
<Typography.Text className="text">Soft Min</Typography.Text>
|
size="small"
|
||||||
<InputNumber
|
onChange={(checked): void => setStackedBarChart(checked)}
|
||||||
type="number"
|
/>
|
||||||
value={softMin}
|
</section>
|
||||||
onChange={softMinHandler}
|
|
||||||
rootClassName="input"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
<section className="container">
|
|
||||||
<Typography.Text className="text">Soft Max</Typography.Text>
|
|
||||||
<InputNumber
|
|
||||||
value={softMax}
|
|
||||||
type="number"
|
|
||||||
rootClassName="input"
|
|
||||||
onChange={softMaxHandler}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{allowLogScale && (
|
|
||||||
<section className="log-scale control-container">
|
|
||||||
<Typography.Text className="typography">Y Axis Scale</Typography.Text>
|
|
||||||
<Select
|
|
||||||
onChange={(value): void =>
|
|
||||||
setIsLogScale(value === LogScale.LOGARITHMIC)
|
|
||||||
}
|
|
||||||
value={isLogScale ? LogScale.LOGARITHMIC : LogScale.LINEAR}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
className="panel-type-select"
|
|
||||||
defaultValue={LogScale.LINEAR}
|
|
||||||
>
|
|
||||||
<Option value={LogScale.LINEAR}>
|
|
||||||
<div className="select-option">
|
|
||||||
<div className="icon">
|
|
||||||
<LineChart size={16} />
|
|
||||||
</div>
|
|
||||||
<Typography.Text className="display">Linear</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</Option>
|
|
||||||
<Option value={LogScale.LOGARITHMIC}>
|
|
||||||
<div className="select-option">
|
|
||||||
<div className="icon">
|
|
||||||
<Spline size={16} />
|
|
||||||
</div>
|
|
||||||
<Typography.Text className="display">Logarithmic</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</Option>
|
|
||||||
</Select>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</SettingsSection>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isLegendSectionVisible && (
|
|
||||||
<SettingsSection title="Legend" icon={<Layers size={14} />}>
|
|
||||||
{allowLegendPosition && (
|
|
||||||
<section className="legend-position control-container">
|
|
||||||
<Typography.Text className="typography">Position</Typography.Text>
|
|
||||||
<Select
|
|
||||||
onChange={(value: LegendPosition): void => setLegendPosition(value)}
|
|
||||||
value={legendPosition}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
className="panel-type-select"
|
|
||||||
defaultValue={LegendPosition.BOTTOM}
|
|
||||||
>
|
|
||||||
<Option value={LegendPosition.BOTTOM}>
|
|
||||||
<div className="select-option">
|
|
||||||
<Typography.Text className="display">Bottom</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</Option>
|
|
||||||
<Option value={LegendPosition.RIGHT}>
|
|
||||||
<div className="select-option">
|
|
||||||
<Typography.Text className="display">Right</Typography.Text>
|
|
||||||
</div>
|
|
||||||
</Option>
|
|
||||||
</Select>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{allowLegendColors && (
|
|
||||||
<section className="legend-colors">
|
|
||||||
<LegendColors
|
|
||||||
customLegendColors={customLegendColors}
|
|
||||||
setCustomLegendColors={setCustomLegendColors}
|
|
||||||
queryResponse={queryResponse}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</SettingsSection>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{allowBucketConfig && (
|
{allowBucketConfig && (
|
||||||
<SettingsSection title="Histogram / Buckets">
|
<section className="bucket-config">
|
||||||
<section className="bucket-config control-container">
|
<Typography.Text className="label">Number of buckets</Typography.Text>
|
||||||
<Typography.Text className="label">Number of buckets</Typography.Text>
|
<InputNumber
|
||||||
<InputNumber
|
value={bucketCount || null}
|
||||||
value={bucketCount || null}
|
type="number"
|
||||||
type="number"
|
min={0}
|
||||||
min={0}
|
rootClassName="bucket-input"
|
||||||
rootClassName="bucket-input"
|
placeholder="Default: 30"
|
||||||
placeholder="Default: 30"
|
onChange={(val): void => {
|
||||||
onChange={(val): void => {
|
setBucketCount(val || 0);
|
||||||
setBucketCount(val || 0);
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
<Typography.Text className="label bucket-size-label">
|
||||||
<Typography.Text className="label bucket-size-label">
|
Bucket width
|
||||||
Bucket width
|
</Typography.Text>
|
||||||
|
<InputNumber
|
||||||
|
value={bucketWidth || null}
|
||||||
|
type="number"
|
||||||
|
precision={2}
|
||||||
|
placeholder="Default: Auto"
|
||||||
|
step={0.1}
|
||||||
|
min={0.0}
|
||||||
|
rootClassName="bucket-input"
|
||||||
|
onChange={(val): void => {
|
||||||
|
setBucketWidth(val || 0);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<section className="combine-hist">
|
||||||
|
<Typography.Text className="label">
|
||||||
|
Merge all series into one
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<InputNumber
|
<Switch
|
||||||
value={bucketWidth || null}
|
checked={combineHistogram}
|
||||||
type="number"
|
size="small"
|
||||||
precision={2}
|
onChange={(checked): void => setCombineHistogram(checked)}
|
||||||
placeholder="Default: Auto"
|
|
||||||
step={0.1}
|
|
||||||
min={0.0}
|
|
||||||
rootClassName="bucket-input"
|
|
||||||
onChange={(val): void => {
|
|
||||||
setBucketWidth(val || 0);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<section className="combine-hist">
|
|
||||||
<Typography.Text className="label">
|
|
||||||
Merge all series into one
|
|
||||||
</Typography.Text>
|
|
||||||
<Switch
|
|
||||||
checked={combineHistogram}
|
|
||||||
size="small"
|
|
||||||
onChange={(checked): void => setCombineHistogram(checked)}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</section>
|
</section>
|
||||||
</SettingsSection>
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{allowLogScale && (
|
||||||
|
<section className="log-scale">
|
||||||
|
<Typography.Text className="typography">Y Axis Scale</Typography.Text>
|
||||||
|
<Select
|
||||||
|
onChange={(value): void => setIsLogScale(value === LogScale.LOGARITHMIC)}
|
||||||
|
value={isLogScale ? LogScale.LOGARITHMIC : LogScale.LINEAR}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
className="panel-type-select"
|
||||||
|
defaultValue={LogScale.LINEAR}
|
||||||
|
>
|
||||||
|
<Option value={LogScale.LINEAR}>
|
||||||
|
<div className="select-option">
|
||||||
|
<div className="icon">
|
||||||
|
<LineChart size={16} />
|
||||||
|
</div>
|
||||||
|
<Typography.Text className="display">Linear</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Option>
|
||||||
|
<Option value={LogScale.LOGARITHMIC}>
|
||||||
|
<div className="select-option">
|
||||||
|
<div className="icon">
|
||||||
|
<Spline size={16} />
|
||||||
|
</div>
|
||||||
|
<Typography.Text className="display">Logarithmic</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Option>
|
||||||
|
</Select>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{allowLegendPosition && (
|
||||||
|
<section className="legend-position">
|
||||||
|
<Typography.Text className="typography">Legend Position</Typography.Text>
|
||||||
|
<Select
|
||||||
|
onChange={(value: LegendPosition): void => setLegendPosition(value)}
|
||||||
|
value={legendPosition}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
className="panel-type-select"
|
||||||
|
defaultValue={LegendPosition.BOTTOM}
|
||||||
|
>
|
||||||
|
<Option value={LegendPosition.BOTTOM}>
|
||||||
|
<div className="select-option">
|
||||||
|
<Typography.Text className="display">Bottom</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Option>
|
||||||
|
<Option value={LegendPosition.RIGHT}>
|
||||||
|
<div className="select-option">
|
||||||
|
<Typography.Text className="display">Right</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Option>
|
||||||
|
</Select>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{allowLegendColors && (
|
||||||
|
<section className="legend-colors">
|
||||||
|
<LegendColors
|
||||||
|
customLegendColors={customLegendColors}
|
||||||
|
setCustomLegendColors={setCustomLegendColors}
|
||||||
|
queryResponse={queryResponse}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -600,25 +541,17 @@ function RightContainer({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{allowContextLinks && (
|
{allowContextLinks && (
|
||||||
<SettingsSection
|
<section className="context-links">
|
||||||
title="Context Links"
|
|
||||||
icon={<Link size={14} />}
|
|
||||||
defaultOpen={!!contextLinks.linksData.length}
|
|
||||||
>
|
|
||||||
<ContextLinks
|
<ContextLinks
|
||||||
contextLinks={contextLinks}
|
contextLinks={contextLinks}
|
||||||
setContextLinks={setContextLinks}
|
setContextLinks={setContextLinks}
|
||||||
selectedWidget={selectedWidget}
|
selectedWidget={selectedWidget}
|
||||||
/>
|
/>
|
||||||
</SettingsSection>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{allowThreshold && (
|
{allowThreshold && (
|
||||||
<SettingsSection
|
<section>
|
||||||
title="Thresholds"
|
|
||||||
icon={<Antenna size={14} />}
|
|
||||||
defaultOpen={!!thresholds.length}
|
|
||||||
>
|
|
||||||
<ThresholdSelector
|
<ThresholdSelector
|
||||||
thresholds={thresholds}
|
thresholds={thresholds}
|
||||||
setThresholds={setThresholds}
|
setThresholds={setThresholds}
|
||||||
@@ -626,7 +559,7 @@ function RightContainer({
|
|||||||
selectedGraph={selectedGraph}
|
selectedGraph={selectedGraph}
|
||||||
columnUnits={columnUnits}
|
columnUnits={columnUnits}
|
||||||
/>
|
/>
|
||||||
</SettingsSection>
|
</section>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const checkStackSeriesState = (
|
|||||||
expect(getByTextUtil(container, 'Stack series')).toBeInTheDocument();
|
expect(getByTextUtil(container, 'Stack series')).toBeInTheDocument();
|
||||||
|
|
||||||
const stackSeriesSection = container.querySelector(
|
const stackSeriesSection = container.querySelector(
|
||||||
'.stack-chart',
|
'section > .stack-chart',
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
expect(stackSeriesSection).toBeInTheDocument();
|
expect(stackSeriesSection).toBeInTheDocument();
|
||||||
|
|
||||||
@@ -326,7 +326,7 @@ describe('Stacking bar in new panel', () => {
|
|||||||
expect(getByText('Stack series')).toBeInTheDocument();
|
expect(getByText('Stack series')).toBeInTheDocument();
|
||||||
|
|
||||||
// Verify section exists
|
// Verify section exists
|
||||||
const section = container.querySelector('.stack-chart');
|
const section = container.querySelector('section > .stack-chart');
|
||||||
expect(section).toBeInTheDocument();
|
expect(section).toBeInTheDocument();
|
||||||
|
|
||||||
// Verify switch is present and enabled (ant-switch-checked)
|
// Verify switch is present and enabled (ant-switch-checked)
|
||||||
|
|||||||
@@ -439,19 +439,6 @@ function NewWidget({
|
|||||||
globalSelectedInterval,
|
globalSelectedInterval,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const navigateToDashboardPage = useCallback(() => {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
const urlVariablesQueryString = query.get(QueryParams.variables);
|
|
||||||
if (urlVariablesQueryString) {
|
|
||||||
params.set(QueryParams.variables, urlVariablesQueryString);
|
|
||||||
}
|
|
||||||
|
|
||||||
const search = params.toString() ? `?${params.toString()}` : '';
|
|
||||||
|
|
||||||
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }) + search);
|
|
||||||
}, [dashboardId, query, safeNavigate]);
|
|
||||||
|
|
||||||
const onClickSaveHandler = useCallback(() => {
|
const onClickSaveHandler = useCallback(() => {
|
||||||
if (!selectedDashboard) {
|
if (!selectedDashboard) {
|
||||||
return;
|
return;
|
||||||
@@ -567,7 +554,9 @@ function NewWidget({
|
|||||||
updateDashboardMutation.mutateAsync(dashboard, {
|
updateDashboardMutation.mutateAsync(dashboard, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setToScrollWidgetId(selectedWidget?.id || '');
|
setToScrollWidgetId(selectedWidget?.id || '');
|
||||||
navigateToDashboardPage();
|
safeNavigate({
|
||||||
|
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
@@ -583,7 +572,7 @@ function NewWidget({
|
|||||||
updateDashboardMutation,
|
updateDashboardMutation,
|
||||||
widgets,
|
widgets,
|
||||||
setToScrollWidgetId,
|
setToScrollWidgetId,
|
||||||
navigateToDashboardPage,
|
safeNavigate,
|
||||||
dashboardId,
|
dashboardId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -592,12 +581,12 @@ function NewWidget({
|
|||||||
setDiscardModal(true);
|
setDiscardModal(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
navigateToDashboardPage();
|
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
|
||||||
}, [isQueryModified, navigateToDashboardPage]);
|
}, [dashboardId, isQueryModified, safeNavigate]);
|
||||||
|
|
||||||
const discardChanges = useCallback(() => {
|
const discardChanges = useCallback(() => {
|
||||||
navigateToDashboardPage();
|
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
|
||||||
}, [navigateToDashboardPage]);
|
}, [dashboardId, safeNavigate]);
|
||||||
|
|
||||||
const setGraphHandler = (type: PANEL_TYPES): void => {
|
const setGraphHandler = (type: PANEL_TYPES): void => {
|
||||||
setIsLoadingPanelData(true);
|
setIsLoadingPanelData(true);
|
||||||
@@ -739,14 +728,12 @@ function NewWidget({
|
|||||||
}
|
}
|
||||||
const widgetId = query.get('widgetId') || '';
|
const widgetId = query.get('widgetId') || '';
|
||||||
const graphType = query.get('graphType') || '';
|
const graphType = query.get('graphType') || '';
|
||||||
const variables = query.get(QueryParams.variables) || '';
|
|
||||||
const queryParams = {
|
const queryParams = {
|
||||||
[QueryParams.expandedWidgetId]: widgetId,
|
[QueryParams.expandedWidgetId]: widgetId,
|
||||||
[QueryParams.graphType]: graphType,
|
[QueryParams.graphType]: graphType,
|
||||||
[QueryParams.compositeQuery]: encodeURIComponent(
|
[QueryParams.compositeQuery]: encodeURIComponent(
|
||||||
JSON.stringify(currentQuery),
|
JSON.stringify(currentQuery),
|
||||||
),
|
),
|
||||||
[QueryParams.variables]: variables,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedSearch = createQueryParams(queryParams);
|
const updatedSearch = createQueryParams(queryParams);
|
||||||
@@ -835,54 +822,56 @@ function NewWidget({
|
|||||||
</LeftContainerWrapper>
|
</LeftContainerWrapper>
|
||||||
|
|
||||||
<RightContainerWrapper>
|
<RightContainerWrapper>
|
||||||
<RightContainer
|
<OverlayScrollbar>
|
||||||
setGraphHandler={setGraphHandler}
|
<RightContainer
|
||||||
title={title}
|
setGraphHandler={setGraphHandler}
|
||||||
setTitle={setTitle}
|
title={title}
|
||||||
description={description}
|
setTitle={setTitle}
|
||||||
setDescription={setDescription}
|
description={description}
|
||||||
stackedBarChart={stackedBarChart}
|
setDescription={setDescription}
|
||||||
setStackedBarChart={setStackedBarChart}
|
stackedBarChart={stackedBarChart}
|
||||||
opacity={opacity}
|
setStackedBarChart={setStackedBarChart}
|
||||||
yAxisUnit={yAxisUnit}
|
opacity={opacity}
|
||||||
columnUnits={columnUnits}
|
yAxisUnit={yAxisUnit}
|
||||||
setColumnUnits={setColumnUnits}
|
columnUnits={columnUnits}
|
||||||
bucketCount={bucketCount}
|
setColumnUnits={setColumnUnits}
|
||||||
bucketWidth={bucketWidth}
|
bucketCount={bucketCount}
|
||||||
combineHistogram={combineHistogram}
|
bucketWidth={bucketWidth}
|
||||||
setCombineHistogram={setCombineHistogram}
|
combineHistogram={combineHistogram}
|
||||||
setBucketWidth={setBucketWidth}
|
setCombineHistogram={setCombineHistogram}
|
||||||
setBucketCount={setBucketCount}
|
setBucketWidth={setBucketWidth}
|
||||||
setOpacity={setOpacity}
|
setBucketCount={setBucketCount}
|
||||||
selectedNullZeroValue={selectedNullZeroValue}
|
setOpacity={setOpacity}
|
||||||
setSelectedNullZeroValue={setSelectedNullZeroValue}
|
selectedNullZeroValue={selectedNullZeroValue}
|
||||||
selectedGraph={graphType}
|
setSelectedNullZeroValue={setSelectedNullZeroValue}
|
||||||
setSelectedTime={setSelectedTime}
|
selectedGraph={graphType}
|
||||||
selectedTime={selectedTime}
|
setSelectedTime={setSelectedTime}
|
||||||
setYAxisUnit={setYAxisUnit}
|
selectedTime={selectedTime}
|
||||||
decimalPrecision={decimalPrecision}
|
setYAxisUnit={setYAxisUnit}
|
||||||
setDecimalPrecision={setDecimalPrecision}
|
decimalPrecision={decimalPrecision}
|
||||||
thresholds={thresholds}
|
setDecimalPrecision={setDecimalPrecision}
|
||||||
setThresholds={setThresholds}
|
thresholds={thresholds}
|
||||||
selectedWidget={selectedWidget}
|
setThresholds={setThresholds}
|
||||||
isFillSpans={isFillSpans}
|
selectedWidget={selectedWidget}
|
||||||
setIsFillSpans={setIsFillSpans}
|
isFillSpans={isFillSpans}
|
||||||
isLogScale={isLogScale}
|
setIsFillSpans={setIsFillSpans}
|
||||||
setIsLogScale={setIsLogScale}
|
isLogScale={isLogScale}
|
||||||
legendPosition={legendPosition}
|
setIsLogScale={setIsLogScale}
|
||||||
setLegendPosition={setLegendPosition}
|
legendPosition={legendPosition}
|
||||||
customLegendColors={customLegendColors}
|
setLegendPosition={setLegendPosition}
|
||||||
setCustomLegendColors={setCustomLegendColors}
|
customLegendColors={customLegendColors}
|
||||||
queryResponse={queryResponse}
|
setCustomLegendColors={setCustomLegendColors}
|
||||||
softMin={softMin}
|
queryResponse={queryResponse}
|
||||||
setSoftMin={setSoftMin}
|
softMin={softMin}
|
||||||
softMax={softMax}
|
setSoftMin={setSoftMin}
|
||||||
setSoftMax={setSoftMax}
|
softMax={softMax}
|
||||||
contextLinks={contextLinks}
|
setSoftMax={setSoftMax}
|
||||||
setContextLinks={setContextLinks}
|
contextLinks={contextLinks}
|
||||||
enableDrillDown={enableDrillDown}
|
setContextLinks={setContextLinks}
|
||||||
isNewDashboard={isNewDashboard}
|
enableDrillDown={enableDrillDown}
|
||||||
/>
|
isNewDashboard={isNewDashboard}
|
||||||
|
/>
|
||||||
|
</OverlayScrollbar>
|
||||||
</RightContainerWrapper>
|
</RightContainerWrapper>
|
||||||
</PanelContainer>
|
</PanelContainer>
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -15,14 +15,7 @@ export const RightContainerWrapper = styled(Col)`
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 0.3rem;
|
width: 0rem;
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background: rgb(136, 136, 136);
|
|
||||||
border-radius: 0.625rem;
|
|
||||||
}
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,339 +0,0 @@
|
|||||||
import { renderHook } from '@testing-library/react';
|
|
||||||
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
|
||||||
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
|
|
||||||
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
|
||||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
|
||||||
|
|
||||||
jest.mock('hooks/dashboard/useDashboardFromLocalStorage');
|
|
||||||
jest.mock('hooks/dashboard/useVariablesFromUrl');
|
|
||||||
|
|
||||||
const mockUseDashboardVariablesFromLocalStorage = useDashboardVariablesFromLocalStorage as jest.MockedFunction<
|
|
||||||
typeof useDashboardVariablesFromLocalStorage
|
|
||||||
>;
|
|
||||||
const mockUseVariablesFromUrl = useVariablesFromUrl as jest.MockedFunction<
|
|
||||||
typeof useVariablesFromUrl
|
|
||||||
>;
|
|
||||||
|
|
||||||
const makeVariable = (
|
|
||||||
overrides: Partial<IDashboardVariable> = {},
|
|
||||||
): IDashboardVariable => ({
|
|
||||||
id: 'existing-id',
|
|
||||||
name: 'env',
|
|
||||||
description: '',
|
|
||||||
type: 'QUERY',
|
|
||||||
sort: 'DISABLED',
|
|
||||||
multiSelect: false,
|
|
||||||
showALLOption: false,
|
|
||||||
selectedValue: 'prod',
|
|
||||||
...overrides,
|
|
||||||
});
|
|
||||||
|
|
||||||
const makeDashboard = (
|
|
||||||
variables: Record<string, IDashboardVariable>,
|
|
||||||
): Dashboard => ({
|
|
||||||
id: 'dash-1',
|
|
||||||
createdAt: '',
|
|
||||||
updatedAt: '',
|
|
||||||
createdBy: '',
|
|
||||||
updatedBy: '',
|
|
||||||
data: {
|
|
||||||
title: 'Test',
|
|
||||||
variables,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const setupHook = (
|
|
||||||
currentDashboard: Record<string, any> = {},
|
|
||||||
urlVariables: Record<string, any> = {},
|
|
||||||
): ReturnType<typeof useTransformDashboardVariables> => {
|
|
||||||
mockUseDashboardVariablesFromLocalStorage.mockReturnValue({
|
|
||||||
currentDashboard,
|
|
||||||
updateLocalStorageDashboardVariables: jest.fn(),
|
|
||||||
});
|
|
||||||
mockUseVariablesFromUrl.mockReturnValue({
|
|
||||||
getUrlVariables: () => urlVariables,
|
|
||||||
setUrlVariables: jest.fn(),
|
|
||||||
updateUrlVariable: jest.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { result } = renderHook(() => useTransformDashboardVariables('dash-1'));
|
|
||||||
return result.current;
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('useTransformDashboardVariables', () => {
|
|
||||||
beforeEach(() => jest.clearAllMocks());
|
|
||||||
|
|
||||||
describe('order assignment', () => {
|
|
||||||
it('assigns order starting from 0 to variables that have none', () => {
|
|
||||||
const { transformDashboardVariables } = setupHook();
|
|
||||||
const dashboard = makeDashboard({
|
|
||||||
v1: makeVariable({ id: 'id1', name: 'v1', order: undefined }),
|
|
||||||
v2: makeVariable({ id: 'id2', name: 'v2', order: undefined }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = transformDashboardVariables(dashboard);
|
|
||||||
|
|
||||||
const orders = Object.values(result.data.variables).map((v) => v.order);
|
|
||||||
expect(orders).toContain(0);
|
|
||||||
expect(orders).toContain(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves existing order values', () => {
|
|
||||||
const { transformDashboardVariables } = setupHook();
|
|
||||||
const dashboard = makeDashboard({
|
|
||||||
v1: makeVariable({ id: 'id1', name: 'v1', order: 5 }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = transformDashboardVariables(dashboard);
|
|
||||||
|
|
||||||
expect(result.data.variables.v1.order).toBe(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('assigns unique orders across multiple variables that all lack an order', () => {
|
|
||||||
const { transformDashboardVariables } = setupHook();
|
|
||||||
const dashboard = makeDashboard({
|
|
||||||
v1: makeVariable({ id: 'id1', name: 'v1', order: undefined }),
|
|
||||||
v2: makeVariable({ id: 'id2', name: 'v2', order: undefined }),
|
|
||||||
v3: makeVariable({ id: 'id3', name: 'v3', order: undefined }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = transformDashboardVariables(dashboard);
|
|
||||||
|
|
||||||
const orders = Object.values(result.data.variables).map((v) => v.order);
|
|
||||||
// All three newly assigned orders must be distinct
|
|
||||||
expect(new Set(orders).size).toBe(3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ID assignment', () => {
|
|
||||||
it('assigns a UUID to variables that have no id', () => {
|
|
||||||
const { transformDashboardVariables } = setupHook();
|
|
||||||
const variable = makeVariable({ name: 'v1' });
|
|
||||||
(variable as any).id = undefined;
|
|
||||||
const dashboard = makeDashboard({ v1: variable });
|
|
||||||
|
|
||||||
const result = transformDashboardVariables(dashboard);
|
|
||||||
|
|
||||||
expect(result.data.variables.v1.id).toMatch(
|
|
||||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves existing IDs', () => {
|
|
||||||
const { transformDashboardVariables } = setupHook();
|
|
||||||
const dashboard = makeDashboard({
|
|
||||||
v1: makeVariable({ id: 'keep-me', name: 'v1' }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = transformDashboardVariables(dashboard);
|
|
||||||
|
|
||||||
expect(result.data.variables.v1.id).toBe('keep-me');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('TEXTBOX backward compatibility', () => {
|
|
||||||
it('copies textboxValue to defaultValue when defaultValue is missing', () => {
|
|
||||||
const { transformDashboardVariables } = setupHook();
|
|
||||||
const dashboard = makeDashboard({
|
|
||||||
v1: makeVariable({
|
|
||||||
id: 'id1',
|
|
||||||
name: 'v1',
|
|
||||||
type: 'TEXTBOX',
|
|
||||||
textboxValue: 'hello',
|
|
||||||
defaultValue: undefined,
|
|
||||||
order: undefined,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = transformDashboardVariables(dashboard);
|
|
||||||
|
|
||||||
expect(result.data.variables.v1.defaultValue).toBe('hello');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not overwrite an existing defaultValue', () => {
|
|
||||||
const { transformDashboardVariables } = setupHook();
|
|
||||||
const dashboard = makeDashboard({
|
|
||||||
v1: makeVariable({
|
|
||||||
id: 'id1',
|
|
||||||
name: 'v1',
|
|
||||||
type: 'TEXTBOX',
|
|
||||||
textboxValue: 'old',
|
|
||||||
defaultValue: 'keep',
|
|
||||||
order: undefined,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = transformDashboardVariables(dashboard);
|
|
||||||
|
|
||||||
expect(result.data.variables.v1.defaultValue).toBe('keep');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('localStorage merge', () => {
|
|
||||||
it('applies localStorage selectedValue over DB value', () => {
|
|
||||||
const { transformDashboardVariables } = setupHook({
|
|
||||||
env: { selectedValue: 'staging', allSelected: false },
|
|
||||||
});
|
|
||||||
const dashboard = makeDashboard({
|
|
||||||
v1: makeVariable({ id: 'id1', name: 'env', selectedValue: 'prod' }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = transformDashboardVariables(dashboard);
|
|
||||||
|
|
||||||
expect(result.data.variables.v1.selectedValue).toBe('staging');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('applies localStorage allSelected over DB value', () => {
|
|
||||||
const { transformDashboardVariables } = setupHook({
|
|
||||||
env: { selectedValue: undefined, allSelected: true },
|
|
||||||
});
|
|
||||||
const dashboard = makeDashboard({
|
|
||||||
v1: makeVariable({
|
|
||||||
id: 'id1',
|
|
||||||
name: 'env',
|
|
||||||
allSelected: false,
|
|
||||||
showALLOption: true,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = transformDashboardVariables(dashboard);
|
|
||||||
|
|
||||||
expect(result.data.variables.v1.allSelected).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('URL variable override', () => {
|
|
||||||
it('sets allSelected=true when URL value is __ALL__', () => {
|
|
||||||
const { transformDashboardVariables } = setupHook(
|
|
||||||
{ env: { selectedValue: 'prod', allSelected: false } },
|
|
||||||
{ env: '__ALL__' },
|
|
||||||
);
|
|
||||||
const dashboard = makeDashboard({
|
|
||||||
v1: makeVariable({
|
|
||||||
id: 'id1',
|
|
||||||
name: 'env',
|
|
||||||
showALLOption: true,
|
|
||||||
allSelected: false,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = transformDashboardVariables(dashboard);
|
|
||||||
|
|
||||||
expect(result.data.variables.v1.allSelected).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets selectedValue from URL and clears allSelected when showALLOption is true', () => {
|
|
||||||
const { transformDashboardVariables } = setupHook(
|
|
||||||
{ env: { selectedValue: undefined, allSelected: true } },
|
|
||||||
{ env: 'dev' },
|
|
||||||
);
|
|
||||||
const dashboard = makeDashboard({
|
|
||||||
v1: makeVariable({
|
|
||||||
id: 'id1',
|
|
||||||
name: 'env',
|
|
||||||
showALLOption: true,
|
|
||||||
allSelected: true,
|
|
||||||
multiSelect: false,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = transformDashboardVariables(dashboard);
|
|
||||||
|
|
||||||
expect(result.data.variables.v1.selectedValue).toBe('dev');
|
|
||||||
expect(result.data.variables.v1.allSelected).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not set allSelected=false when showALLOption is false', () => {
|
|
||||||
const { transformDashboardVariables } = setupHook(
|
|
||||||
{ env: { selectedValue: undefined, allSelected: true } },
|
|
||||||
{ env: 'dev' },
|
|
||||||
);
|
|
||||||
const dashboard = makeDashboard({
|
|
||||||
v1: makeVariable({
|
|
||||||
id: 'id1',
|
|
||||||
name: 'env',
|
|
||||||
showALLOption: false,
|
|
||||||
allSelected: true,
|
|
||||||
multiSelect: false,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = transformDashboardVariables(dashboard);
|
|
||||||
|
|
||||||
expect(result.data.variables.v1.selectedValue).toBe('dev');
|
|
||||||
expect(result.data.variables.v1.allSelected).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('normalizes array URL value to single value for single-select variable', () => {
|
|
||||||
const { transformDashboardVariables } = setupHook(
|
|
||||||
{},
|
|
||||||
{ env: ['prod', 'dev'] },
|
|
||||||
);
|
|
||||||
const dashboard = makeDashboard({
|
|
||||||
v1: makeVariable({
|
|
||||||
id: 'id1',
|
|
||||||
name: 'env',
|
|
||||||
multiSelect: false,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = transformDashboardVariables(dashboard);
|
|
||||||
|
|
||||||
expect(result.data.variables.v1.selectedValue).toBe('prod');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('wraps single URL value in array for multi-select variable', () => {
|
|
||||||
const { transformDashboardVariables } = setupHook({}, { env: 'prod' });
|
|
||||||
const dashboard = makeDashboard({
|
|
||||||
v1: makeVariable({
|
|
||||||
id: 'id1',
|
|
||||||
name: 'env',
|
|
||||||
multiSelect: true,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = transformDashboardVariables(dashboard);
|
|
||||||
|
|
||||||
expect(result.data.variables.v1.selectedValue).toEqual(['prod']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('looks up URL variable by variable id when name is absent', () => {
|
|
||||||
const { transformDashboardVariables } = setupHook(
|
|
||||||
{},
|
|
||||||
{ 'var-uuid': 'fallback' },
|
|
||||||
);
|
|
||||||
const variable = makeVariable({ id: 'var-uuid', multiSelect: false });
|
|
||||||
delete variable.name;
|
|
||||||
const dashboard = makeDashboard({ v1: variable });
|
|
||||||
|
|
||||||
const result = transformDashboardVariables(dashboard);
|
|
||||||
|
|
||||||
expect(result.data.variables.v1.selectedValue).toBe('fallback');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('edge cases', () => {
|
|
||||||
it('returns data unchanged when there are no variables', () => {
|
|
||||||
const { transformDashboardVariables } = setupHook();
|
|
||||||
const dashboard = makeDashboard({});
|
|
||||||
|
|
||||||
const result = transformDashboardVariables(dashboard);
|
|
||||||
|
|
||||||
expect(result.data.variables).toEqual({});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not mutate the original dashboard', () => {
|
|
||||||
const { transformDashboardVariables } = setupHook({
|
|
||||||
env: { selectedValue: 'staging', allSelected: false },
|
|
||||||
});
|
|
||||||
const dashboard = makeDashboard({
|
|
||||||
v1: makeVariable({ id: 'id1', name: 'env', selectedValue: 'prod' }),
|
|
||||||
});
|
|
||||||
const originalValue = dashboard.data.variables.v1.selectedValue;
|
|
||||||
|
|
||||||
transformDashboardVariables(dashboard);
|
|
||||||
|
|
||||||
expect(dashboard.data.variables.v1.selectedValue).toBe(originalValue);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -15,7 +15,7 @@ interface DashboardLocalStorageVariables {
|
|||||||
[id: string]: LocalStoreDashboardVariables;
|
[id: string]: LocalStoreDashboardVariables;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UseDashboardVariablesFromLocalStorageReturn {
|
interface UseDashboardVariablesFromLocalStorageReturn {
|
||||||
currentDashboard: LocalStoreDashboardVariables;
|
currentDashboard: LocalStoreDashboardVariables;
|
||||||
updateLocalStorageDashboardVariables: (
|
updateLocalStorageDashboardVariables: (
|
||||||
id: string,
|
id: string,
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
|
|
||||||
import {
|
|
||||||
useDashboardVariablesFromLocalStorage,
|
|
||||||
UseDashboardVariablesFromLocalStorageReturn,
|
|
||||||
} from 'hooks/dashboard/useDashboardFromLocalStorage';
|
|
||||||
import useVariablesFromUrl, {
|
|
||||||
UseVariablesFromUrlReturn,
|
|
||||||
} from 'hooks/dashboard/useVariablesFromUrl';
|
|
||||||
import { isEmpty } from 'lodash-es';
|
|
||||||
import { normalizeUrlValueForVariable } from 'providers/Dashboard/normalizeUrlValue';
|
|
||||||
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
|
||||||
import { v4 as generateUUID } from 'uuid';
|
|
||||||
|
|
||||||
export function useTransformDashboardVariables(
|
|
||||||
dashboardId: string,
|
|
||||||
): Pick<UseVariablesFromUrlReturn, 'getUrlVariables' | 'updateUrlVariable'> &
|
|
||||||
UseDashboardVariablesFromLocalStorageReturn & {
|
|
||||||
transformDashboardVariables: (data: Dashboard) => Dashboard;
|
|
||||||
} {
|
|
||||||
const {
|
|
||||||
currentDashboard,
|
|
||||||
updateLocalStorageDashboardVariables,
|
|
||||||
} = useDashboardVariablesFromLocalStorage(dashboardId);
|
|
||||||
const { getUrlVariables, updateUrlVariable } = useVariablesFromUrl();
|
|
||||||
|
|
||||||
const mergeDBWithLocalStorage = (
|
|
||||||
data: Dashboard,
|
|
||||||
localStorageVariables: any,
|
|
||||||
): Dashboard => {
|
|
||||||
const updatedData = data;
|
|
||||||
if (data && localStorageVariables) {
|
|
||||||
const updatedVariables = data.data.variables;
|
|
||||||
const variablesFromUrl = getUrlVariables();
|
|
||||||
Object.keys(data.data.variables).forEach((variable) => {
|
|
||||||
const variableData = data.data.variables[variable];
|
|
||||||
|
|
||||||
// values from url
|
|
||||||
const urlVariable = variableData?.name
|
|
||||||
? variablesFromUrl[variableData?.name] || variablesFromUrl[variableData.id]
|
|
||||||
: variablesFromUrl[variableData.id];
|
|
||||||
|
|
||||||
let updatedVariable = {
|
|
||||||
...data.data.variables[variable],
|
|
||||||
...localStorageVariables[variableData.name as any],
|
|
||||||
};
|
|
||||||
|
|
||||||
// respect the url variable if it is set, override the others
|
|
||||||
if (!isEmpty(urlVariable)) {
|
|
||||||
if (urlVariable === ALL_SELECTED_VALUE) {
|
|
||||||
updatedVariable = {
|
|
||||||
...updatedVariable,
|
|
||||||
allSelected: true,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Normalize URL value to match variable's multiSelect configuration
|
|
||||||
const normalizedValue = normalizeUrlValueForVariable(
|
|
||||||
urlVariable,
|
|
||||||
variableData,
|
|
||||||
);
|
|
||||||
|
|
||||||
updatedVariable = {
|
|
||||||
...updatedVariable,
|
|
||||||
selectedValue: normalizedValue,
|
|
||||||
// Only set allSelected to false if showALLOption is available
|
|
||||||
...(updatedVariable?.showALLOption && { allSelected: false }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedVariables[variable] = updatedVariable;
|
|
||||||
});
|
|
||||||
updatedData.data.variables = updatedVariables;
|
|
||||||
}
|
|
||||||
return updatedData;
|
|
||||||
};
|
|
||||||
|
|
||||||
// As we do not have order and ID's in the variables object, we have to process variables to add order and ID if they do not exist in the variables object
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
const transformDashboardVariables = (data: Dashboard): Dashboard => {
|
|
||||||
if (data && data.data && data.data.variables) {
|
|
||||||
const clonedDashboardData = mergeDBWithLocalStorage(
|
|
||||||
JSON.parse(JSON.stringify(data)),
|
|
||||||
currentDashboard,
|
|
||||||
);
|
|
||||||
const { variables } = clonedDashboardData.data;
|
|
||||||
const existingOrders: Set<number> = new Set();
|
|
||||||
|
|
||||||
for (const key in variables) {
|
|
||||||
// eslint-disable-next-line no-prototype-builtins
|
|
||||||
if (variables.hasOwnProperty(key)) {
|
|
||||||
const variable: IDashboardVariable = variables[key];
|
|
||||||
|
|
||||||
// Check if 'order' property doesn't exist or is undefined
|
|
||||||
if (variable.order === undefined) {
|
|
||||||
// Find a unique order starting from 0
|
|
||||||
let order = 0;
|
|
||||||
while (existingOrders.has(order)) {
|
|
||||||
order += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
variable.order = order;
|
|
||||||
existingOrders.add(order);
|
|
||||||
// ! BWC - Specific case for backward compatibility where textboxValue was used instead of defaultValue
|
|
||||||
if (variable.type === 'TEXTBOX' && !variable.defaultValue) {
|
|
||||||
variable.defaultValue = variable.textboxValue || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variable.id === undefined) {
|
|
||||||
variable.id = generateUUID();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return clonedDashboardData;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
transformDashboardVariables,
|
|
||||||
getUrlVariables,
|
|
||||||
updateUrlVariable,
|
|
||||||
currentDashboard,
|
|
||||||
updateLocalStorageDashboardVariables,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,7 @@ export interface LocalStoreDashboardVariables {
|
|||||||
| IDashboardVariable['selectedValue'];
|
| IDashboardVariable['selectedValue'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UseVariablesFromUrlReturn {
|
interface UseVariablesFromUrlReturn {
|
||||||
getUrlVariables: () => LocalStoreDashboardVariables;
|
getUrlVariables: () => LocalStoreDashboardVariables;
|
||||||
setUrlVariables: (variables: LocalStoreDashboardVariables) => void;
|
setUrlVariables: (variables: LocalStoreDashboardVariables) => void;
|
||||||
updateUrlVariable: (
|
updateUrlVariable: (
|
||||||
|
|||||||
@@ -1,143 +0,0 @@
|
|||||||
import { Route } from 'react-router-dom';
|
|
||||||
import * as getDashboardModule from 'api/v1/dashboards/id/get';
|
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
|
||||||
import { rest, server } from 'mocks-server/server';
|
|
||||||
import { render, screen, waitFor } from 'tests/test-utils';
|
|
||||||
|
|
||||||
import DashboardWidget from '../index';
|
|
||||||
|
|
||||||
const DASHBOARD_ID = 'dash-1';
|
|
||||||
const WIDGET_ID = 'widget-abc';
|
|
||||||
|
|
||||||
const mockDashboardResponse = {
|
|
||||||
status: 'success',
|
|
||||||
data: {
|
|
||||||
id: DASHBOARD_ID,
|
|
||||||
createdAt: '2024-01-01T00:00:00Z',
|
|
||||||
createdBy: 'test',
|
|
||||||
updatedAt: '2024-01-01T00:00:00Z',
|
|
||||||
updatedBy: 'test',
|
|
||||||
isLocked: false,
|
|
||||||
data: {
|
|
||||||
collapsableRowsMigrated: true,
|
|
||||||
description: '',
|
|
||||||
name: '',
|
|
||||||
panelMap: {},
|
|
||||||
tags: [],
|
|
||||||
title: 'Test Dashboard',
|
|
||||||
uploadedGrafana: false,
|
|
||||||
uuid: '',
|
|
||||||
version: '',
|
|
||||||
variables: {},
|
|
||||||
widgets: [],
|
|
||||||
layout: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockSafeNavigate = jest.fn();
|
|
||||||
|
|
||||||
jest.mock('hooks/useSafeNavigate', () => ({
|
|
||||||
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
|
|
||||||
safeNavigate: mockSafeNavigate,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('container/NewWidget', () => ({
|
|
||||||
__esModule: true,
|
|
||||||
default: (): JSX.Element => <div data-testid="new-widget">NewWidget</div>,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Wrap component in a Route so useParams can resolve dashboardId.
|
|
||||||
// Query params are passed via the URL so useUrlQuery (react-router) can read them.
|
|
||||||
function renderAtRoute(
|
|
||||||
queryState: Record<string, string | null> = {},
|
|
||||||
): ReturnType<typeof render> {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
Object.entries(queryState).forEach(([k, v]) => {
|
|
||||||
if (v !== null) {
|
|
||||||
params.set(k, v);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const search = params.toString() ? `?${params.toString()}` : '';
|
|
||||||
return render(
|
|
||||||
<Route path="/dashboard/:dashboardId/new">
|
|
||||||
<DashboardWidget />
|
|
||||||
</Route>,
|
|
||||||
undefined,
|
|
||||||
{ initialRoute: `/dashboard/${DASHBOARD_ID}/new${search}` },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockSafeNavigate.mockClear();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('DashboardWidget', () => {
|
|
||||||
it('redirects to dashboard when widgetId is missing', async () => {
|
|
||||||
renderAtRoute({ graphType: PANEL_TYPES.TIME_SERIES });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
const [navigatedTo] = mockSafeNavigate.mock.calls[0];
|
|
||||||
expect(navigatedTo).toContain(`/dashboard/${DASHBOARD_ID}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('redirects to dashboard when graphType is missing', async () => {
|
|
||||||
renderAtRoute({ widgetId: WIDGET_ID });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(mockSafeNavigate).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
const [navigatedTo] = mockSafeNavigate.mock.calls[0];
|
|
||||||
expect(navigatedTo).toContain(`/dashboard/${DASHBOARD_ID}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows spinner while dashboard is loading', () => {
|
|
||||||
// Spy instead of MSW delay('infinite') to avoid leaving an open network handle.
|
|
||||||
jest
|
|
||||||
.spyOn(getDashboardModule, 'default')
|
|
||||||
.mockReturnValue(new Promise(() => {}));
|
|
||||||
|
|
||||||
renderAtRoute({ widgetId: WIDGET_ID, graphType: PANEL_TYPES.TIME_SERIES });
|
|
||||||
|
|
||||||
expect(screen.getByRole('img', { name: 'loading' })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows error message when dashboard fetch fails', async () => {
|
|
||||||
server.use(
|
|
||||||
rest.get(
|
|
||||||
`http://localhost/api/v1/dashboards/${DASHBOARD_ID}`,
|
|
||||||
(_req, res, ctx) => res(ctx.status(500), ctx.json({ status: 'error' })),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
renderAtRoute({ widgetId: WIDGET_ID, graphType: PANEL_TYPES.TIME_SERIES });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders NewWidget when dashboard loads successfully', async () => {
|
|
||||||
server.use(
|
|
||||||
rest.get(
|
|
||||||
`http://localhost/api/v1/dashboards/${DASHBOARD_ID}`,
|
|
||||||
(_req, res, ctx) => res(ctx.status(200), ctx.json(mockDashboardResponse)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
renderAtRoute({ widgetId: WIDGET_ID, graphType: PANEL_TYPES.TIME_SERIES });
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByTestId('new-widget')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,34 +1,29 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { generatePath, useParams } from 'react-router-dom';
|
import { generatePath, useParams } from 'react-router-dom';
|
||||||
import { Card, Typography } from 'antd';
|
import { Card, Typography } from 'antd';
|
||||||
import getDashboard from 'api/v1/dashboards/id/get';
|
import getDashboard from 'api/v1/dashboards/id/get';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { QueryParams } from 'constants/query';
|
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { DASHBOARD_CACHE_TIME } from 'constants/queryCacheTime';
|
import { DASHBOARD_CACHE_TIME } from 'constants/queryCacheTime';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import NewWidget from 'container/NewWidget';
|
import NewWidget from 'container/NewWidget';
|
||||||
import { isDrilldownEnabled } from 'container/QueryTable/Drilldown/drilldownUtils';
|
import { isDrilldownEnabled } from 'container/QueryTable/Drilldown/drilldownUtils';
|
||||||
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
|
|
||||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
||||||
import { setDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
import { setDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
|
||||||
|
|
||||||
function DashboardWidget(): JSX.Element | null {
|
function DashboardWidget(): JSX.Element | null {
|
||||||
const { dashboardId } = useParams<{
|
const { dashboardId } = useParams<{
|
||||||
dashboardId: string;
|
dashboardId: string;
|
||||||
}>();
|
}>();
|
||||||
const query = useUrlQuery();
|
const [widgetId] = useQueryState('widgetId');
|
||||||
const { graphType, widgetId } = useMemo(() => {
|
const [graphType] = useQueryState(
|
||||||
return {
|
'graphType',
|
||||||
graphType: query.get(QueryParams.graphType) as PANEL_TYPES,
|
parseAsStringEnum<PANEL_TYPES>(Object.values(PANEL_TYPES)),
|
||||||
widgetId: query.get(QueryParams.widgetId),
|
);
|
||||||
};
|
|
||||||
}, [query]);
|
|
||||||
|
|
||||||
const { safeNavigate } = useSafeNavigate();
|
const { safeNavigate } = useSafeNavigate();
|
||||||
|
|
||||||
@@ -62,15 +57,8 @@ function DashboardWidgetInternal({
|
|||||||
widgetId: string;
|
widgetId: string;
|
||||||
graphType: PANEL_TYPES;
|
graphType: PANEL_TYPES;
|
||||||
}): JSX.Element | null {
|
}): JSX.Element | null {
|
||||||
const [selectedDashboard, setSelectedDashboard] = useState<
|
|
||||||
Dashboard | undefined
|
|
||||||
>(undefined);
|
|
||||||
|
|
||||||
const { transformDashboardVariables } = useTransformDashboardVariables(
|
|
||||||
dashboardId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
data: dashboardResponse,
|
||||||
isFetching: isFetchingDashboardResponse,
|
isFetching: isFetchingDashboardResponse,
|
||||||
isError: isErrorDashboardResponse,
|
isError: isErrorDashboardResponse,
|
||||||
} = useQuery([REACT_QUERY_KEY.DASHBOARD_BY_ID, dashboardId, widgetId], {
|
} = useQuery([REACT_QUERY_KEY.DASHBOARD_BY_ID, dashboardId, widgetId], {
|
||||||
@@ -82,15 +70,17 @@ function DashboardWidgetInternal({
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
cacheTime: DASHBOARD_CACHE_TIME,
|
cacheTime: DASHBOARD_CACHE_TIME,
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
const updatedDashboardData = transformDashboardVariables(response.data);
|
|
||||||
setSelectedDashboard(updatedDashboardData);
|
|
||||||
setDashboardVariablesStore({
|
setDashboardVariablesStore({
|
||||||
dashboardId,
|
dashboardId,
|
||||||
variables: updatedDashboardData.data.variables,
|
variables: response.data.data.variables,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedDashboard = useMemo(() => dashboardResponse?.data, [
|
||||||
|
dashboardResponse?.data,
|
||||||
|
]);
|
||||||
|
|
||||||
if (isFetchingDashboardResponse) {
|
if (isFetchingDashboardResponse) {
|
||||||
return <Spinner tip="Loading.." />;
|
return <Spinner tip="Loading.." />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,18 +17,21 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||||||
import { Modal } from 'antd';
|
import { Modal } from 'antd';
|
||||||
import getDashboard from 'api/v1/dashboards/id/get';
|
import getDashboard from 'api/v1/dashboards/id/get';
|
||||||
import locked from 'api/v1/dashboards/id/lock';
|
import locked from 'api/v1/dashboards/id/lock';
|
||||||
|
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import { useTransformDashboardVariables } from 'hooks/dashboard/useTransformDashboardVariables';
|
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||||
|
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
||||||
import useTabVisibility from 'hooks/useTabFocus';
|
import useTabVisibility from 'hooks/useTabFocus';
|
||||||
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
||||||
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
||||||
import { defaultTo } from 'lodash-es';
|
import { defaultTo, isEmpty } from 'lodash-es';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import isUndefined from 'lodash-es/isUndefined';
|
import isUndefined from 'lodash-es/isUndefined';
|
||||||
import omitBy from 'lodash-es/omitBy';
|
import omitBy from 'lodash-es/omitBy';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
|
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
|
||||||
|
import { normalizeUrlValueForVariable } from 'providers/Dashboard/normalizeUrlValue';
|
||||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
@@ -36,9 +39,10 @@ import { AppState } from 'store/reducers';
|
|||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
import { UPDATE_TIME_INTERVAL } from 'types/actions/globalTime';
|
||||||
import { SuccessResponseV2 } from 'types/api';
|
import { SuccessResponseV2 } from 'types/api';
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
import APIError from 'types/api/error';
|
import APIError from 'types/api/error';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DASHBOARD_CACHE_TIME,
|
DASHBOARD_CACHE_TIME,
|
||||||
@@ -133,10 +137,9 @@ export function DashboardProvider({
|
|||||||
const {
|
const {
|
||||||
currentDashboard,
|
currentDashboard,
|
||||||
updateLocalStorageDashboardVariables,
|
updateLocalStorageDashboardVariables,
|
||||||
getUrlVariables,
|
} = useDashboardVariablesFromLocalStorage(dashboardId);
|
||||||
updateUrlVariable,
|
|
||||||
transformDashboardVariables,
|
const { getUrlVariables, updateUrlVariable } = useVariablesFromUrl();
|
||||||
} = useTransformDashboardVariables(dashboardId);
|
|
||||||
|
|
||||||
const updatedTimeRef = useRef<Dayjs | null>(null); // Using ref to store the updated time
|
const updatedTimeRef = useRef<Dayjs | null>(null); // Using ref to store the updated time
|
||||||
const modalRef = useRef<any>(null);
|
const modalRef = useRef<any>(null);
|
||||||
@@ -148,6 +151,99 @@ export function DashboardProvider({
|
|||||||
|
|
||||||
const [isDashboardFetching, setIsDashboardFetching] = useState<boolean>(false);
|
const [isDashboardFetching, setIsDashboardFetching] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const mergeDBWithLocalStorage = (
|
||||||
|
data: Dashboard,
|
||||||
|
localStorageVariables: any,
|
||||||
|
): Dashboard => {
|
||||||
|
const updatedData = data;
|
||||||
|
if (data && localStorageVariables) {
|
||||||
|
const updatedVariables = data.data.variables;
|
||||||
|
const variablesFromUrl = getUrlVariables();
|
||||||
|
Object.keys(data.data.variables).forEach((variable) => {
|
||||||
|
const variableData = data.data.variables[variable];
|
||||||
|
|
||||||
|
// values from url
|
||||||
|
const urlVariable = variableData?.name
|
||||||
|
? variablesFromUrl[variableData?.name] || variablesFromUrl[variableData.id]
|
||||||
|
: variablesFromUrl[variableData.id];
|
||||||
|
|
||||||
|
let updatedVariable = {
|
||||||
|
...data.data.variables[variable],
|
||||||
|
...localStorageVariables[variableData.name as any],
|
||||||
|
};
|
||||||
|
|
||||||
|
// respect the url variable if it is set, override the others
|
||||||
|
if (!isEmpty(urlVariable)) {
|
||||||
|
if (urlVariable === ALL_SELECTED_VALUE) {
|
||||||
|
updatedVariable = {
|
||||||
|
...updatedVariable,
|
||||||
|
allSelected: true,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Normalize URL value to match variable's multiSelect configuration
|
||||||
|
const normalizedValue = normalizeUrlValueForVariable(
|
||||||
|
urlVariable,
|
||||||
|
variableData,
|
||||||
|
);
|
||||||
|
|
||||||
|
updatedVariable = {
|
||||||
|
...updatedVariable,
|
||||||
|
selectedValue: normalizedValue,
|
||||||
|
// Only set allSelected to false if showALLOption is available
|
||||||
|
...(updatedVariable?.showALLOption && { allSelected: false }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedVariables[variable] = updatedVariable;
|
||||||
|
});
|
||||||
|
updatedData.data.variables = updatedVariables;
|
||||||
|
}
|
||||||
|
return updatedData;
|
||||||
|
};
|
||||||
|
// As we do not have order and ID's in the variables object, we have to process variables to add order and ID if they do not exist in the variables object
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
const transformDashboardVariables = (data: Dashboard): Dashboard => {
|
||||||
|
if (data && data.data && data.data.variables) {
|
||||||
|
const clonedDashboardData = mergeDBWithLocalStorage(
|
||||||
|
JSON.parse(JSON.stringify(data)),
|
||||||
|
currentDashboard,
|
||||||
|
);
|
||||||
|
const { variables } = clonedDashboardData.data;
|
||||||
|
const existingOrders: Set<number> = new Set();
|
||||||
|
|
||||||
|
for (const key in variables) {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (variables.hasOwnProperty(key)) {
|
||||||
|
const variable: IDashboardVariable = variables[key];
|
||||||
|
|
||||||
|
// Check if 'order' property doesn't exist or is undefined
|
||||||
|
if (variable.order === undefined) {
|
||||||
|
// Find a unique order starting from 0
|
||||||
|
let order = 0;
|
||||||
|
while (existingOrders.has(order)) {
|
||||||
|
order += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
variable.order = order;
|
||||||
|
existingOrders.add(order);
|
||||||
|
// ! BWC - Specific case for backward compatibility where textboxValue was used instead of defaultValue
|
||||||
|
if (variable.type === 'TEXTBOX' && !variable.defaultValue) {
|
||||||
|
variable.defaultValue = variable.textboxValue || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variable.id === undefined) {
|
||||||
|
variable.id = generateUUID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clonedDashboardData;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
const dashboardResponse = useQuery(
|
const dashboardResponse = useQuery(
|
||||||
[
|
[
|
||||||
REACT_QUERY_KEY.DASHBOARD_BY_ID,
|
REACT_QUERY_KEY.DASHBOARD_BY_ID,
|
||||||
@@ -178,14 +274,13 @@ export function DashboardProvider({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onSuccess: (data: SuccessResponseV2<Dashboard>) => {
|
onSuccess: (data: SuccessResponseV2<Dashboard>) => {
|
||||||
const updatedDashboardData = transformDashboardVariables(data?.data);
|
// if the url variable is not set for any variable, set it to the default value
|
||||||
|
const variables = data?.data?.data?.variables;
|
||||||
// initialize URL variables after dashboard state is set to avoid race conditions
|
|
||||||
const variables = updatedDashboardData?.data?.variables;
|
|
||||||
if (variables) {
|
if (variables) {
|
||||||
initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable);
|
initializeDefaultVariables(variables, getUrlVariables, updateUrlVariable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updatedDashboardData = transformDashboardVariables(data?.data);
|
||||||
const updatedDate = dayjs(updatedDashboardData?.updatedAt);
|
const updatedDate = dayjs(updatedDashboardData?.updatedAt);
|
||||||
|
|
||||||
setIsDashboardLocked(updatedDashboardData?.locked || false);
|
setIsDashboardLocked(updatedDashboardData?.locked || false);
|
||||||
|
|||||||
@@ -381,7 +381,6 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
|||||||
multiSelect: false,
|
multiSelect: false,
|
||||||
allSelected: false,
|
allSelected: false,
|
||||||
showALLOption: true,
|
showALLOption: true,
|
||||||
order: 0,
|
|
||||||
},
|
},
|
||||||
services: {
|
services: {
|
||||||
id: 'svc-id',
|
id: 'svc-id',
|
||||||
@@ -389,7 +388,6 @@ describe('Dashboard Provider - URL Variables Integration', () => {
|
|||||||
multiSelect: true,
|
multiSelect: true,
|
||||||
allSelected: false,
|
allSelected: false,
|
||||||
showALLOption: true,
|
showALLOption: true,
|
||||||
order: 1,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mockGetUrlVariables,
|
mockGetUrlVariables,
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
package cloudintegration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Module interface {
|
|
||||||
// CreateConnectionArtifact generates cloud provider specific connection information,
|
|
||||||
// client side handles how this information is shown
|
|
||||||
CreateConnectionArtifact(
|
|
||||||
ctx context.Context,
|
|
||||||
orgID valuer.UUID,
|
|
||||||
provider cloudintegrationtypes.CloudProviderType,
|
|
||||||
request *cloudintegrationtypes.ConnectionArtifactRequest,
|
|
||||||
) (*cloudintegrationtypes.ConnectionArtifact, error)
|
|
||||||
|
|
||||||
// GetAccountStatus returns agent connection status for a cloud integration account
|
|
||||||
GetAccountStatus(ctx context.Context, orgID, accountID valuer.UUID) (*cloudintegrationtypes.AccountStatus, error)
|
|
||||||
|
|
||||||
// ListConnectedAccounts lists accounts where agent is connected
|
|
||||||
ListConnectedAccounts(ctx context.Context, orgID valuer.UUID) (*cloudintegrationtypes.ConnectedAccounts, error)
|
|
||||||
|
|
||||||
// DisconnectAccount soft deletes/removes a cloud integration account.
|
|
||||||
DisconnectAccount(ctx context.Context, orgID, accountID valuer.UUID) error
|
|
||||||
|
|
||||||
// UpdateAccountConfig updates the configuration of an existing cloud account for a specific organization.
|
|
||||||
UpdateAccountConfig(
|
|
||||||
ctx context.Context,
|
|
||||||
orgID,
|
|
||||||
accountID valuer.UUID,
|
|
||||||
config *cloudintegrationtypes.UpdateAccountConfigRequest,
|
|
||||||
) (*cloudintegrationtypes.Account, error)
|
|
||||||
|
|
||||||
// ListServicesMetadata returns list of services metadata for a cloud provider attached with the integrationID.
|
|
||||||
// This just returns a summary of the service and not the whole service definition
|
|
||||||
ListServicesMetadata(ctx context.Context, orgID valuer.UUID, integrationID *valuer.UUID) (*cloudintegrationtypes.ServicesMetadata, error)
|
|
||||||
|
|
||||||
// GetService returns service definition details for a serviceID. This returns config and
|
|
||||||
// other details required to show in service details page on web client.
|
|
||||||
GetService(ctx context.Context, orgID valuer.UUID, integrationID *valuer.UUID, serviceID string) (*cloudintegrationtypes.Service, error)
|
|
||||||
|
|
||||||
// UpdateServiceConfig updates cloud integration service config
|
|
||||||
UpdateServiceConfig(
|
|
||||||
ctx context.Context,
|
|
||||||
orgID valuer.UUID,
|
|
||||||
serviceID string,
|
|
||||||
config *cloudintegrationtypes.UpdateServiceConfigRequest,
|
|
||||||
) (*cloudintegrationtypes.UpdateServiceConfigResponse, error)
|
|
||||||
|
|
||||||
// AgentCheckIn is called by agent to heartbeat and get latest config in response.
|
|
||||||
AgentCheckIn(
|
|
||||||
ctx context.Context,
|
|
||||||
orgID valuer.UUID,
|
|
||||||
req *cloudintegrationtypes.AgentCheckInRequest,
|
|
||||||
) (*cloudintegrationtypes.AgentCheckInResponse, error)
|
|
||||||
|
|
||||||
// GetDashboardByID returns dashboard JSON for a given dashboard id.
|
|
||||||
// this only returns the dashboard when the service (embedded in dashboard id) is enabled
|
|
||||||
// in the org for any cloud integration account
|
|
||||||
GetDashboardByID(ctx context.Context, orgID valuer.UUID, id string) (*dashboardtypes.Dashboard, error)
|
|
||||||
|
|
||||||
// GetAllDashboards returns list of dashboards across all connected cloud integration accounts
|
|
||||||
// for enabled services in the org. This list gets added to dashboard list page
|
|
||||||
GetAllDashboards(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloudProvider is the interface each cloud provider must implement.
|
|
||||||
type CloudProvider interface {
|
|
||||||
CreateArtifact(
|
|
||||||
ctx context.Context,
|
|
||||||
orgID valuer.UUID,
|
|
||||||
request *cloudintegrationtypes.ConnectionArtifactRequest,
|
|
||||||
creds cloudintegrationtypes.SignozCredentials,
|
|
||||||
accountID valuer.UUID,
|
|
||||||
) (artifact *cloudintegrationtypes.ConnectionArtifact, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Handler interface {
|
|
||||||
AgentCheckIn(http.ResponseWriter, *http.Request)
|
|
||||||
GenerateConnectionArtifact(http.ResponseWriter, *http.Request)
|
|
||||||
ListConnectedAccounts(http.ResponseWriter, *http.Request)
|
|
||||||
GetAccountStatus(http.ResponseWriter, *http.Request)
|
|
||||||
ListServices(http.ResponseWriter, *http.Request)
|
|
||||||
GetServiceDetails(http.ResponseWriter, *http.Request)
|
|
||||||
UpdateAccountConfig(http.ResponseWriter, *http.Request)
|
|
||||||
UpdateServiceConfig(http.ResponseWriter, *http.Request)
|
|
||||||
DisconnectAccount(http.ResponseWriter, *http.Request)
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package implcloudintegration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
|
||||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
|
||||||
)
|
|
||||||
|
|
||||||
type module struct{}
|
|
||||||
|
|
||||||
func NewModule() cloudintegration.Module {
|
|
||||||
return &module{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) CreateConnectionArtifact(_ context.Context, _ valuer.UUID, _ cloudintegrationtypes.CloudProviderType, _ *cloudintegrationtypes.ConnectionArtifactRequest) (*cloudintegrationtypes.ConnectionArtifact, error) {
|
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) GetAccountStatus(_ context.Context, _, _ valuer.UUID) (*cloudintegrationtypes.AccountStatus, error) {
|
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) ListConnectedAccounts(_ context.Context, _ valuer.UUID) (*cloudintegrationtypes.ConnectedAccounts, error) {
|
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) DisconnectAccount(_ context.Context, _, _ valuer.UUID) error {
|
|
||||||
return errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) UpdateAccountConfig(_ context.Context, _, _ valuer.UUID, _ *cloudintegrationtypes.UpdateAccountConfigRequest) (*cloudintegrationtypes.Account, error) {
|
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) ListServicesSummary(_ context.Context, _ valuer.UUID, _ *valuer.UUID) (*cloudintegrationtypes.ServicesSummary, error) {
|
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) GetService(_ context.Context, _ valuer.UUID, _ string, _ *valuer.UUID) (*cloudintegrationtypes.Service, error) {
|
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) UpdateServiceConfig(_ context.Context, _ string, _ valuer.UUID, _ *cloudintegrationtypes.UpdateServiceConfigRequest) (*cloudintegrationtypes.ServiceSummary, error) {
|
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) AgentCheckIn(_ context.Context, _ valuer.UUID, _ *cloudintegrationtypes.AgentCheckInRequest) (cloudintegrationtypes.AgentCheckInResponse, error) {
|
|
||||||
return cloudintegrationtypes.AgentCheckInResponse{}, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) GetDashboardByID(_ context.Context, _ string, _ valuer.UUID) (*dashboardtypes.Dashboard, error) {
|
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *module) GetAllDashboards(_ context.Context, _ valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
|
|
||||||
return nil, errors.New(errors.TypeUnsupported, errors.CodeUnsupported, "cloud integration is an enterprise feature")
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
package implcloudintegration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
|
||||||
)
|
|
||||||
|
|
||||||
type store struct {
|
|
||||||
store sqlstore.SQLStore
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStore(sqlStore sqlstore.SQLStore) cloudintegrationtypes.Store {
|
|
||||||
return &store{store: sqlStore}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *store) GetAccountByID(ctx context.Context, orgID, id valuer.UUID, provider cloudintegrationtypes.CloudProviderType) (*cloudintegrationtypes.StorableCloudIntegration, error) {
|
|
||||||
account := new(cloudintegrationtypes.StorableCloudIntegration)
|
|
||||||
err := s.store.BunDB().NewSelect().Model(account).
|
|
||||||
Where("id = ?", id).
|
|
||||||
Where("org_id = ?", orgID).
|
|
||||||
Where("provider = ?", provider).
|
|
||||||
Scan(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, s.store.WrapNotFoundErrf(err, cloudintegrationtypes.ErrCodeCloudIntegrationNotFound, "cloud integration account with id %s not found", id)
|
|
||||||
}
|
|
||||||
return account, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *store) UpsertAccount(ctx context.Context, account *cloudintegrationtypes.StorableCloudIntegration) error {
|
|
||||||
account.UpdatedAt = time.Now()
|
|
||||||
_, err := s.store.BunDBCtx(ctx).NewInsert().Model(account).
|
|
||||||
On("CONFLICT (id, provider, org_id) DO UPDATE").
|
|
||||||
Set("config = EXCLUDED.config").
|
|
||||||
Set("account_id = EXCLUDED.account_id").
|
|
||||||
Set("last_agent_report = EXCLUDED.last_agent_report").
|
|
||||||
Set("removed_at = EXCLUDED.removed_at").
|
|
||||||
Set("updated_at = EXCLUDED.updated_at").
|
|
||||||
Exec(ctx)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *store) RemoveAccount(ctx context.Context, orgID, id valuer.UUID, provider cloudintegrationtypes.CloudProviderType) error {
|
|
||||||
_, err := s.store.BunDBCtx(ctx).NewUpdate().Model((*cloudintegrationtypes.StorableCloudIntegration)(nil)).
|
|
||||||
Set("removed_at = ?", time.Now()).
|
|
||||||
Where("id = ?", id).
|
|
||||||
Where("org_id = ?", orgID).
|
|
||||||
Where("provider = ?", provider).
|
|
||||||
Exec(ctx)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *store) GetConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType) ([]*cloudintegrationtypes.StorableCloudIntegration, error) {
|
|
||||||
var accounts []*cloudintegrationtypes.StorableCloudIntegration
|
|
||||||
err := s.store.BunDB().NewSelect().Model(&accounts).
|
|
||||||
Where("org_id = ?", orgID).
|
|
||||||
Where("provider = ?", provider).
|
|
||||||
Where("removed_at IS NULL").
|
|
||||||
Where("account_id IS NOT NULL").
|
|
||||||
Where("last_agent_report IS NOT NULL").
|
|
||||||
Order("created_at ASC").
|
|
||||||
Scan(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return accounts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *store) GetConnectedAccount(ctx context.Context, orgID valuer.UUID, provider cloudintegrationtypes.CloudProviderType, providerAccountID string) (*cloudintegrationtypes.StorableCloudIntegration, error) {
|
|
||||||
account := new(cloudintegrationtypes.StorableCloudIntegration)
|
|
||||||
err := s.store.BunDB().NewSelect().Model(account).
|
|
||||||
Where("org_id = ?", orgID).
|
|
||||||
Where("provider = ?", provider).
|
|
||||||
Where("account_id = ?", providerAccountID).
|
|
||||||
Where("last_agent_report IS NOT NULL").
|
|
||||||
Where("removed_at IS NULL").
|
|
||||||
Scan(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, s.store.WrapNotFoundErrf(err, cloudintegrationtypes.ErrCodeCloudIntegrationNotFound, "connected account with provider account id %s not found", providerAccountID)
|
|
||||||
}
|
|
||||||
return account, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *store) GetServiceByType(ctx context.Context, cloudIntegrationID valuer.UUID, serviceType string) (*cloudintegrationtypes.StorableCloudIntegrationService, error) {
|
|
||||||
service := new(cloudintegrationtypes.StorableCloudIntegrationService)
|
|
||||||
err := s.store.BunDB().NewSelect().Model(service).
|
|
||||||
Where("cloud_integration_id = ?", cloudIntegrationID).
|
|
||||||
Where("type = ?", serviceType).
|
|
||||||
Scan(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, s.store.WrapNotFoundErrf(err, cloudintegrationtypes.ErrCodeCloudIntegrationNotFound, "cloud integration service with type %s not found", serviceType)
|
|
||||||
}
|
|
||||||
return service, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *store) UpsertService(ctx context.Context, service *cloudintegrationtypes.StorableCloudIntegrationService) error {
|
|
||||||
service.UpdatedAt = time.Now()
|
|
||||||
_, err := s.store.BunDBCtx(ctx).NewInsert().Model(service).
|
|
||||||
On("CONFLICT (cloud_integration_id, type) DO UPDATE").
|
|
||||||
Set("config = EXCLUDED.config").
|
|
||||||
Set("updated_at = EXCLUDED.updated_at").
|
|
||||||
Exec(ctx)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *store) GetServices(ctx context.Context, cloudIntegrationID valuer.UUID) ([]*cloudintegrationtypes.StorableCloudIntegrationService, error) {
|
|
||||||
var services []*cloudintegrationtypes.StorableCloudIntegrationService
|
|
||||||
err := s.store.BunDB().NewSelect().Model(&services).
|
|
||||||
Where("cloud_integration_id = ?", cloudIntegrationID).
|
|
||||||
Scan(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return services, nil
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/factory/factorytest"
|
"github.com/SigNoz/signoz/pkg/factory/factorytest"
|
||||||
"github.com/SigNoz/signoz/pkg/flagger"
|
"github.com/SigNoz/signoz/pkg/flagger"
|
||||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
|
|
||||||
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||||
@@ -51,7 +50,7 @@ func TestNewHandlers(t *testing.T) {
|
|||||||
|
|
||||||
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), flagger)
|
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), flagger)
|
||||||
|
|
||||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, implcloudintegration.NewModule())
|
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter)
|
||||||
|
|
||||||
querierHandler := querier.NewHandler(providerSettings, nil, nil)
|
querierHandler := querier.NewHandler(providerSettings, nil, nil)
|
||||||
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil)
|
handlers := NewHandlers(modules, providerSettings, nil, querierHandler, nil, nil, nil, nil, nil, nil, nil)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/modules/apdex/implapdex"
|
"github.com/SigNoz/signoz/pkg/modules/apdex/implapdex"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/authdomain"
|
"github.com/SigNoz/signoz/pkg/modules/authdomain"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/authdomain/implauthdomain"
|
"github.com/SigNoz/signoz/pkg/modules/authdomain/implauthdomain"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
|
||||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer/implmetricsexplorer"
|
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer/implmetricsexplorer"
|
||||||
@@ -52,25 +51,24 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Modules struct {
|
type Modules struct {
|
||||||
OrgGetter organization.Getter
|
OrgGetter organization.Getter
|
||||||
OrgSetter organization.Setter
|
OrgSetter organization.Setter
|
||||||
Preference preference.Module
|
Preference preference.Module
|
||||||
User user.Module
|
User user.Module
|
||||||
UserGetter user.Getter
|
UserGetter user.Getter
|
||||||
SavedView savedview.Module
|
SavedView savedview.Module
|
||||||
Apdex apdex.Module
|
Apdex apdex.Module
|
||||||
Dashboard dashboard.Module
|
Dashboard dashboard.Module
|
||||||
QuickFilter quickfilter.Module
|
QuickFilter quickfilter.Module
|
||||||
TraceFunnel tracefunnel.Module
|
TraceFunnel tracefunnel.Module
|
||||||
RawDataExport rawdataexport.Module
|
RawDataExport rawdataexport.Module
|
||||||
AuthDomain authdomain.Module
|
AuthDomain authdomain.Module
|
||||||
Session session.Module
|
Session session.Module
|
||||||
Services services.Module
|
Services services.Module
|
||||||
SpanPercentile spanpercentile.Module
|
SpanPercentile spanpercentile.Module
|
||||||
MetricsExplorer metricsexplorer.Module
|
MetricsExplorer metricsexplorer.Module
|
||||||
Promote promote.Module
|
Promote promote.Module
|
||||||
ServiceAccount serviceaccount.Module
|
ServiceAccount serviceaccount.Module
|
||||||
CloudIntegration cloudintegration.Module
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModules(
|
func NewModules(
|
||||||
@@ -91,7 +89,6 @@ func NewModules(
|
|||||||
config Config,
|
config Config,
|
||||||
dashboard dashboard.Module,
|
dashboard dashboard.Module,
|
||||||
userGetter user.Getter,
|
userGetter user.Getter,
|
||||||
cloudIntegration cloudintegration.Module,
|
|
||||||
) Modules {
|
) Modules {
|
||||||
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
|
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
|
||||||
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
|
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
|
||||||
@@ -99,24 +96,23 @@ func NewModules(
|
|||||||
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
|
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
|
||||||
|
|
||||||
return Modules{
|
return Modules{
|
||||||
OrgGetter: orgGetter,
|
OrgGetter: orgGetter,
|
||||||
OrgSetter: orgSetter,
|
OrgSetter: orgSetter,
|
||||||
Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()),
|
Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()),
|
||||||
SavedView: implsavedview.NewModule(sqlstore),
|
SavedView: implsavedview.NewModule(sqlstore),
|
||||||
Apdex: implapdex.NewModule(sqlstore),
|
Apdex: implapdex.NewModule(sqlstore),
|
||||||
Dashboard: dashboard,
|
Dashboard: dashboard,
|
||||||
User: user,
|
User: user,
|
||||||
UserGetter: userGetter,
|
UserGetter: userGetter,
|
||||||
QuickFilter: quickfilter,
|
QuickFilter: quickfilter,
|
||||||
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),
|
TraceFunnel: impltracefunnel.NewModule(impltracefunnel.NewStore(sqlstore)),
|
||||||
RawDataExport: implrawdataexport.NewModule(querier),
|
RawDataExport: implrawdataexport.NewModule(querier),
|
||||||
AuthDomain: implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs),
|
AuthDomain: implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs),
|
||||||
Session: implsession.NewModule(providerSettings, authNs, user, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter),
|
Session: implsession.NewModule(providerSettings, authNs, user, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore), authNs), tokenizer, orgGetter),
|
||||||
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
|
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
|
||||||
Services: implservices.NewModule(querier, telemetryStore),
|
Services: implservices.NewModule(querier, telemetryStore),
|
||||||
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer),
|
MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer),
|
||||||
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
|
Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore),
|
||||||
ServiceAccount: implserviceaccount.NewModule(implserviceaccount.NewStore(sqlstore), authz, emailing, providerSettings),
|
ServiceAccount: implserviceaccount.NewModule(implserviceaccount.NewStore(sqlstore), authz, emailing, providerSettings),
|
||||||
CloudIntegration: cloudIntegration,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/factory/factorytest"
|
"github.com/SigNoz/signoz/pkg/factory/factorytest"
|
||||||
"github.com/SigNoz/signoz/pkg/flagger"
|
"github.com/SigNoz/signoz/pkg/flagger"
|
||||||
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
"github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration/implcloudintegration"
|
|
||||||
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
||||||
@@ -50,7 +49,7 @@ func TestNewModules(t *testing.T) {
|
|||||||
|
|
||||||
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), flagger)
|
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings), flagger)
|
||||||
|
|
||||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter, implcloudintegration.NewModule())
|
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, userGetter)
|
||||||
|
|
||||||
reflectVal := reflect.ValueOf(modules)
|
reflectVal := reflect.ValueOf(modules)
|
||||||
for i := 0; i < reflectVal.NumField(); i++ {
|
for i := 0; i < reflectVal.NumField(); i++ {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/gateway"
|
"github.com/SigNoz/signoz/pkg/gateway"
|
||||||
"github.com/SigNoz/signoz/pkg/instrumentation"
|
"github.com/SigNoz/signoz/pkg/instrumentation"
|
||||||
"github.com/SigNoz/signoz/pkg/licensing"
|
"github.com/SigNoz/signoz/pkg/licensing"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
|
||||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
"github.com/SigNoz/signoz/pkg/modules/organization/implorganization"
|
||||||
@@ -44,7 +43,6 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/version"
|
"github.com/SigNoz/signoz/pkg/version"
|
||||||
"github.com/SigNoz/signoz/pkg/zeus"
|
"github.com/SigNoz/signoz/pkg/zeus"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/global"
|
|
||||||
"github.com/SigNoz/signoz/pkg/web"
|
"github.com/SigNoz/signoz/pkg/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -93,7 +91,6 @@ func New(
|
|||||||
dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, queryparser.QueryParser, querier.Querier, licensing.Licensing) dashboard.Module,
|
dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, queryparser.QueryParser, querier.Querier, licensing.Licensing) dashboard.Module,
|
||||||
gatewayProviderFactory func(licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config],
|
gatewayProviderFactory func(licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config],
|
||||||
querierHandlerCallback func(factory.ProviderSettings, querier.Querier, analytics.Analytics) querier.Handler,
|
querierHandlerCallback func(factory.ProviderSettings, querier.Querier, analytics.Analytics) querier.Handler,
|
||||||
cloudIntegrationModuleCallback func(sqlstore.SQLStore, licensing.Licensing, zeus.Zeus, gateway.Gateway, global.Config) cloudintegration.Module,
|
|
||||||
) (*SigNoz, error) {
|
) (*SigNoz, error) {
|
||||||
// Initialize instrumentation
|
// Initialize instrumentation
|
||||||
instrumentation, err := instrumentation.New(ctx, config.Instrumentation, version.Info, "signoz")
|
instrumentation, err := instrumentation.New(ctx, config.Instrumentation, version.Info, "signoz")
|
||||||
@@ -390,11 +387,8 @@ func New(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize cloudintegration module via callback
|
|
||||||
cloudIntegrationModule := cloudIntegrationModuleCallback(sqlstore, licensing, zeus, gateway, config.Global)
|
|
||||||
|
|
||||||
// Initialize all modules
|
// Initialize all modules
|
||||||
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter, cloudIntegrationModule)
|
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, userGetter)
|
||||||
|
|
||||||
userService := impluser.NewService(providerSettings, impluser.NewStore(sqlstore, providerSettings), modules.User, orgGetter, authz, config.User.Root)
|
userService := impluser.NewService(providerSettings, impluser.NewStore(sqlstore, providerSettings), modules.User, orgGetter, authz, config.User.Root)
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
package cloudintegrationtypes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
ConnectedAccounts struct {
|
|
||||||
Accounts []*Account `json:"accounts"`
|
|
||||||
}
|
|
||||||
|
|
||||||
GettableConnectedAccounts = ConnectedAccounts
|
|
||||||
|
|
||||||
UpdateAccountConfigRequest struct {
|
|
||||||
AWS *AWSAccountConfig `json:"aws"`
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdatableAccountConfig = UpdateAccountConfigRequest
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
Account struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
ProviderAccountId *string `json:"providerAccountID,omitempty"`
|
|
||||||
Provider CloudProviderType `json:"provider"`
|
|
||||||
RemovedAt *time.Time `json:"removedAt,omitempty"`
|
|
||||||
AgentReport *AgentReport `json:"agentReport,omitempty"`
|
|
||||||
OrgID valuer.UUID `json:"orgID"`
|
|
||||||
Config *AccountConfig `json:"accountConfig,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
GettableAccount = Account
|
|
||||||
)
|
|
||||||
|
|
||||||
// AgentReport represents heartbeats sent by the agent.
|
|
||||||
type AgentReport struct {
|
|
||||||
TimestampMillis int64 `json:"timestampMillis"`
|
|
||||||
Data map[string]any `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AccountConfig struct {
|
|
||||||
AWS *AWSAccountConfig `json:"aws,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AWSAccountConfig struct {
|
|
||||||
Regions []string `json:"regions"`
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
package cloudintegrationtypes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql/driver"
|
|
||||||
"encoding/json"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
|
||||||
"github.com/uptrace/bun"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrCodeCloudIntegrationNotFound = errors.MustNewCode("cloud_integration_not_found")
|
|
||||||
)
|
|
||||||
|
|
||||||
// StorableCloudIntegration represents a cloud integration stored in the database.
|
|
||||||
// This is also referred as "Account" in the context of cloud integrations.
|
|
||||||
type StorableCloudIntegration struct {
|
|
||||||
bun.BaseModel `bun:"table:cloud_integration"`
|
|
||||||
|
|
||||||
types.Identifiable
|
|
||||||
types.TimeAuditable
|
|
||||||
Provider CloudProviderType `json:"provider" bun:"provider,type:text"`
|
|
||||||
// Config is provider specific data in JSON string format
|
|
||||||
Config string `json:"config" bun:"config,type:text"`
|
|
||||||
AccountID *string `json:"account_id" bun:"account_id,type:text"`
|
|
||||||
LastAgentReport *StorableAgentReport `json:"last_agent_report" bun:"last_agent_report,type:text"`
|
|
||||||
RemovedAt *time.Time `json:"removed_at" bun:"removed_at,type:timestamp,nullzero"`
|
|
||||||
OrgID valuer.UUID `bun:"org_id,type:text"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// StorableAgentReport represents the last heartbeat and arbitrary data sent by the agent
|
|
||||||
// as of now there is no use case for Data field, but keeping it for backwards compatibility with older structure.
|
|
||||||
type StorableAgentReport struct {
|
|
||||||
TimestampMillis int64 `json:"timestamp_millis"`
|
|
||||||
Data map[string]any `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// StorableCloudIntegrationService is to store service config for a cloud integration, which is a cloud provider specific configuration.
|
|
||||||
type StorableCloudIntegrationService struct {
|
|
||||||
bun.BaseModel `bun:"table:cloud_integration_service,alias:cis"`
|
|
||||||
|
|
||||||
types.Identifiable
|
|
||||||
types.TimeAuditable
|
|
||||||
Type valuer.String `bun:"type,type:text,notnull,unique:cloud_integration_id_type"`
|
|
||||||
// Config is cloud provider's service specific data in JSON string format
|
|
||||||
Config string `bun:"config,type:text"`
|
|
||||||
CloudIntegrationID valuer.UUID `bun:"cloud_integration_id,type:text,notnull,unique:cloud_integration_id_type,references:cloud_integration(id),on_delete:cascade"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan scans value from DB.
|
|
||||||
func (r *StorableAgentReport) Scan(src any) error {
|
|
||||||
var data []byte
|
|
||||||
switch v := src.(type) {
|
|
||||||
case []byte:
|
|
||||||
data = v
|
|
||||||
case string:
|
|
||||||
data = []byte(v)
|
|
||||||
default:
|
|
||||||
return errors.NewInternalf(errors.CodeInternal, "tried to scan from %T instead of string or bytes", src)
|
|
||||||
}
|
|
||||||
return json.Unmarshal(data, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value creates value to be stored in DB.
|
|
||||||
func (r *StorableAgentReport) Value() (driver.Value, error) {
|
|
||||||
if r == nil {
|
|
||||||
return nil, errors.NewInternalf(errors.CodeInternal, "agent report is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
serialized, err := json.Marshal(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WrapInternalf(
|
|
||||||
err, errors.CodeInternal, "couldn't serialize agent report to JSON",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// Return as string instead of []byte to ensure PostgreSQL stores as text, not bytes
|
|
||||||
return string(serialized), nil
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package cloudintegrationtypes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CloudProviderType type alias.
|
|
||||||
type CloudProviderType struct{ valuer.String }
|
|
||||||
|
|
||||||
var (
|
|
||||||
// cloud providers.
|
|
||||||
CloudProviderTypeAWS = CloudProviderType{valuer.NewString("aws")}
|
|
||||||
CloudProviderTypeAzure = CloudProviderType{valuer.NewString("azure")}
|
|
||||||
|
|
||||||
// errors.
|
|
||||||
ErrCodeCloudProviderInvalidInput = errors.MustNewCode("invalid_cloud_provider")
|
|
||||||
|
|
||||||
AWSIntegrationUserEmail = valuer.MustNewEmail("aws-integration@signoz.io")
|
|
||||||
AzureIntegrationUserEmail = valuer.MustNewEmail("azure-integration@signoz.io")
|
|
||||||
)
|
|
||||||
|
|
||||||
// CloudIntegrationUserEmails is the list of valid emails for Cloud One Click integrations.
|
|
||||||
// This is used for validation and restrictions in different contexts, across codebase.
|
|
||||||
var CloudIntegrationUserEmails = []valuer.Email{
|
|
||||||
AWSIntegrationUserEmail,
|
|
||||||
AzureIntegrationUserEmail,
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCloudProvider returns a new CloudProviderType from a string.
|
|
||||||
// It validates the input and returns an error if the input is not valid cloud provider.
|
|
||||||
func NewCloudProvider(provider string) (CloudProviderType, error) {
|
|
||||||
switch provider {
|
|
||||||
case CloudProviderTypeAWS.StringValue():
|
|
||||||
return CloudProviderTypeAWS, nil
|
|
||||||
case CloudProviderTypeAzure.StringValue():
|
|
||||||
return CloudProviderTypeAzure, nil
|
|
||||||
default:
|
|
||||||
return CloudProviderType{}, errors.NewInvalidInputf(ErrCodeCloudProviderInvalidInput, "invalid cloud provider: %s", provider)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
package cloudintegrationtypes
|
|
||||||
|
|
||||||
import "github.com/SigNoz/signoz/pkg/types/integrationtypes"
|
|
||||||
|
|
||||||
// request for creating connection artifact.
|
|
||||||
type (
|
|
||||||
PostableConnectionArtifact = ConnectionArtifactRequest
|
|
||||||
|
|
||||||
ConnectionArtifactRequest struct {
|
|
||||||
Aws *AWSConnectionArtifactRequest `json:"aws"`
|
|
||||||
}
|
|
||||||
|
|
||||||
AWSConnectionArtifactRequest struct {
|
|
||||||
DeploymentRegion string `json:"deploymentRegion"`
|
|
||||||
Regions []string `json:"regions"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
// SignozCredentials is used to configure Agents to connect with Signoz
|
|
||||||
SignozCredentials struct {
|
|
||||||
SigNozAPIUrl string
|
|
||||||
SigNozAPIKey string // PAT
|
|
||||||
IngestionUrl string
|
|
||||||
IngestionKey string
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectionArtifact struct {
|
|
||||||
Aws *AWSConnectionArtifact `json:"aws"`
|
|
||||||
}
|
|
||||||
|
|
||||||
AWSConnectionArtifact struct {
|
|
||||||
ConnectionUrl string `json:"connectionURL"`
|
|
||||||
}
|
|
||||||
|
|
||||||
GettableConnectionArtifact = ConnectionArtifact
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
AccountStatus struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
ProviderAccountId *string `json:"providerAccountID,omitempty"`
|
|
||||||
Status integrationtypes.AccountStatus `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
GettableAccountStatus = AccountStatus
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
AgentCheckInRequest struct {
|
|
||||||
// older backward compatible fields are mapped to new fields
|
|
||||||
// CloudIntegrationId string `json:"cloudIntegrationId"`
|
|
||||||
// AccountId string `json:"accountId"`
|
|
||||||
|
|
||||||
// New fields
|
|
||||||
ProviderAccountId string `json:"providerAccountId"`
|
|
||||||
CloudAccountId string `json:"cloudAccountId"`
|
|
||||||
|
|
||||||
Data map[string]any `json:"data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
PostableAgentCheckInRequest struct {
|
|
||||||
AgentCheckInRequest
|
|
||||||
// following are backward compatible fields for older running agents
|
|
||||||
// which gets mapped to new fields in AgentCheckInRequest
|
|
||||||
CloudIntegrationId string `json:"cloud_integration_id"`
|
|
||||||
CloudAccountId string `json:"cloud_account_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
GettableAgentCheckInResponse struct {
|
|
||||||
AgentCheckInResponse
|
|
||||||
|
|
||||||
CloudIntegrationId string `json:"cloud_integration_id"`
|
|
||||||
AccountId string `json:"account_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
AgentCheckInResponse struct {
|
|
||||||
// Older fields for backward compatibility are mapped to new fields below
|
|
||||||
// CloudIntegrationId string `json:"cloud_integration_id"`
|
|
||||||
// AccountId string `json:"account_id"`
|
|
||||||
|
|
||||||
// New fields
|
|
||||||
ProviderAccountId string `json:"providerAccountId"`
|
|
||||||
CloudAccountId string `json:"cloudAccountId"`
|
|
||||||
|
|
||||||
// IntegrationConfig populates data related to integration that is required for an agent
|
|
||||||
// to start collecting telemetry data
|
|
||||||
// keeping JSON key snake_case for backward compatibility
|
|
||||||
IntegrationConfig *IntegrationConfig `json:"integration_config,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
IntegrationConfig struct {
|
|
||||||
EnabledRegions []string `json:"enabledRegions"` // backward compatible
|
|
||||||
Telemetry *AWSCollectionStrategy `json:"telemetry,omitempty"` // backward compatible
|
|
||||||
|
|
||||||
// new fields
|
|
||||||
AWS *AWSIntegrationConfig `json:"aws,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
AWSIntegrationConfig struct {
|
|
||||||
EnabledRegions []string `json:"enabledRegions"`
|
|
||||||
Telemetry *AWSCollectionStrategy `json:"telemetry,omitempty"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
package cloudintegrationtypes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
CodeInvalidCloudRegion = errors.MustNewCode("invalid_cloud_region")
|
|
||||||
CodeMismatchCloudProvider = errors.MustNewCode("cloud_provider_mismatch")
|
|
||||||
)
|
|
||||||
|
|
||||||
// List of all valid cloud regions on Amazon Web Services.
|
|
||||||
var ValidAWSRegions = map[string]struct{}{
|
|
||||||
"af-south-1": {}, // Africa (Cape Town).
|
|
||||||
"ap-east-1": {}, // Asia Pacific (Hong Kong).
|
|
||||||
"ap-northeast-1": {}, // Asia Pacific (Tokyo).
|
|
||||||
"ap-northeast-2": {}, // Asia Pacific (Seoul).
|
|
||||||
"ap-northeast-3": {}, // Asia Pacific (Osaka).
|
|
||||||
"ap-south-1": {}, // Asia Pacific (Mumbai).
|
|
||||||
"ap-south-2": {}, // Asia Pacific (Hyderabad).
|
|
||||||
"ap-southeast-1": {}, // Asia Pacific (Singapore).
|
|
||||||
"ap-southeast-2": {}, // Asia Pacific (Sydney).
|
|
||||||
"ap-southeast-3": {}, // Asia Pacific (Jakarta).
|
|
||||||
"ap-southeast-4": {}, // Asia Pacific (Melbourne).
|
|
||||||
"ca-central-1": {}, // Canada (Central).
|
|
||||||
"ca-west-1": {}, // Canada West (Calgary).
|
|
||||||
"eu-central-1": {}, // Europe (Frankfurt).
|
|
||||||
"eu-central-2": {}, // Europe (Zurich).
|
|
||||||
"eu-north-1": {}, // Europe (Stockholm).
|
|
||||||
"eu-south-1": {}, // Europe (Milan).
|
|
||||||
"eu-south-2": {}, // Europe (Spain).
|
|
||||||
"eu-west-1": {}, // Europe (Ireland).
|
|
||||||
"eu-west-2": {}, // Europe (London).
|
|
||||||
"eu-west-3": {}, // Europe (Paris).
|
|
||||||
"il-central-1": {}, // Israel (Tel Aviv).
|
|
||||||
"me-central-1": {}, // Middle East (UAE).
|
|
||||||
"me-south-1": {}, // Middle East (Bahrain).
|
|
||||||
"sa-east-1": {}, // South America (Sao Paulo).
|
|
||||||
"us-east-1": {}, // US East (N. Virginia).
|
|
||||||
"us-east-2": {}, // US East (Ohio).
|
|
||||||
"us-west-1": {}, // US West (N. California).
|
|
||||||
"us-west-2": {}, // US West (Oregon).
|
|
||||||
}
|
|
||||||
|
|
||||||
// List of all valid cloud regions for Microsoft Azure.
|
|
||||||
var ValidAzureRegions = map[string]struct{}{
|
|
||||||
"australiacentral": {}, // Australia Central
|
|
||||||
"australiacentral2": {}, // Australia Central 2
|
|
||||||
"australiaeast": {}, // Australia East
|
|
||||||
"australiasoutheast": {}, // Australia Southeast
|
|
||||||
"austriaeast": {}, // Austria East
|
|
||||||
"belgiumcentral": {}, // Belgium Central
|
|
||||||
"brazilsouth": {}, // Brazil South
|
|
||||||
"brazilsoutheast": {}, // Brazil Southeast
|
|
||||||
"canadacentral": {}, // Canada Central
|
|
||||||
"canadaeast": {}, // Canada East
|
|
||||||
"centralindia": {}, // Central India
|
|
||||||
"centralus": {}, // Central US
|
|
||||||
"chilecentral": {}, // Chile Central
|
|
||||||
"denmarkeast": {}, // Denmark East
|
|
||||||
"eastasia": {}, // East Asia
|
|
||||||
"eastus": {}, // East US
|
|
||||||
"eastus2": {}, // East US 2
|
|
||||||
"francecentral": {}, // France Central
|
|
||||||
"francesouth": {}, // France South
|
|
||||||
"germanynorth": {}, // Germany North
|
|
||||||
"germanywestcentral": {}, // Germany West Central
|
|
||||||
"indonesiacentral": {}, // Indonesia Central
|
|
||||||
"israelcentral": {}, // Israel Central
|
|
||||||
"italynorth": {}, // Italy North
|
|
||||||
"japaneast": {}, // Japan East
|
|
||||||
"japanwest": {}, // Japan West
|
|
||||||
"koreacentral": {}, // Korea Central
|
|
||||||
"koreasouth": {}, // Korea South
|
|
||||||
"malaysiawest": {}, // Malaysia West
|
|
||||||
"mexicocentral": {}, // Mexico Central
|
|
||||||
"newzealandnorth": {}, // New Zealand North
|
|
||||||
"northcentralus": {}, // North Central US
|
|
||||||
"northeurope": {}, // North Europe
|
|
||||||
"norwayeast": {}, // Norway East
|
|
||||||
"norwaywest": {}, // Norway West
|
|
||||||
"polandcentral": {}, // Poland Central
|
|
||||||
"qatarcentral": {}, // Qatar Central
|
|
||||||
"southafricanorth": {}, // South Africa North
|
|
||||||
"southafricawest": {}, // South Africa West
|
|
||||||
"southcentralus": {}, // South Central US
|
|
||||||
"southindia": {}, // South India
|
|
||||||
"southeastasia": {}, // Southeast Asia
|
|
||||||
"spaincentral": {}, // Spain Central
|
|
||||||
"swedencentral": {}, // Sweden Central
|
|
||||||
"switzerlandnorth": {}, // Switzerland North
|
|
||||||
"switzerlandwest": {}, // Switzerland West
|
|
||||||
"uaecentral": {}, // UAE Central
|
|
||||||
"uaenorth": {}, // UAE North
|
|
||||||
"uksouth": {}, // UK South
|
|
||||||
"ukwest": {}, // UK West
|
|
||||||
"westcentralus": {}, // West Central US
|
|
||||||
"westeurope": {}, // West Europe
|
|
||||||
"westindia": {}, // West India
|
|
||||||
"westus": {}, // West US
|
|
||||||
"westus2": {}, // West US 2
|
|
||||||
"westus3": {}, // West US 3
|
|
||||||
}
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
package cloudintegrationtypes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
|
||||||
)
|
|
||||||
|
|
||||||
var S3Sync = valuer.NewString("s3sync")
|
|
||||||
|
|
||||||
type (
|
|
||||||
ServicesMetadata struct {
|
|
||||||
Services []*ServiceMetadata `json:"services"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceMetadata helps to quickly list available services and whether it is enabled or not.
|
|
||||||
// As getting complete service definition is a heavy operation and the response is also large,
|
|
||||||
// initial integration page load can be very slow.
|
|
||||||
ServiceMetadata struct {
|
|
||||||
ServiceDefinitionMetadata
|
|
||||||
// if the service is enabled for the account
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
GettableServicesMetadata = ServicesMetadata
|
|
||||||
|
|
||||||
Service struct {
|
|
||||||
ServiceDefinition
|
|
||||||
ServiceConfig *ServiceConfig `json:"serviceConfig"`
|
|
||||||
}
|
|
||||||
|
|
||||||
GettableService = Service
|
|
||||||
|
|
||||||
UpdateServiceConfigRequest struct {
|
|
||||||
CloudIntegrationId valuer.UUID `json:"cloudIntegrationId"`
|
|
||||||
ServiceConfig *ServiceConfig `json:"serviceConfig"`
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateServiceConfigResponse struct {
|
|
||||||
Id string `json:"id"` // service id
|
|
||||||
CloudIntegrationId valuer.UUID `json:"cloudIntegrationId"`
|
|
||||||
ServiceConfig *ServiceConfig `json:"serviceConfig"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type ServiceConfig struct {
|
|
||||||
AWS *AWSServiceConfig `json:"aws,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AWSServiceConfig struct {
|
|
||||||
Logs *AWSServiceLogsConfig `json:"logs"`
|
|
||||||
Metrics *AWSServiceMetricsConfig `json:"metrics"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AWSServiceLogsConfig is AWS specific logs config for a service
|
|
||||||
// NOTE: the JSON keys are snake case for backward compatibility with existing agents.
|
|
||||||
type AWSServiceLogsConfig struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
S3Buckets map[string][]string `json:"s3_buckets,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AWSServiceMetricsConfig struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefinitionMetadata represents service definition metadata. This is useful for showing service overview.
|
|
||||||
type ServiceDefinitionMetadata struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Icon string `json:"icon"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ServiceDefinition struct {
|
|
||||||
ServiceDefinitionMetadata
|
|
||||||
Overview string `json:"overview"` // markdown
|
|
||||||
Assets Assets `json:"assets"`
|
|
||||||
SupportedSignals SupportedSignals `json:"supported_signals"`
|
|
||||||
DataCollected DataCollected `json:"dataCollected"`
|
|
||||||
Strategy *CollectionStrategy `json:"telemetryCollectionStrategy"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CollectionStrategy is cloud provider specific configuration for signal collection,
|
|
||||||
// this is used by agent to understand the nitty-gritty for collecting telemetry for the cloud provider.
|
|
||||||
type CollectionStrategy struct {
|
|
||||||
AWS *AWSCollectionStrategy `json:"aws,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assets represents the collection of dashboards.
|
|
||||||
type Assets struct {
|
|
||||||
Dashboards []Dashboard `json:"dashboards"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SupportedSignals for cloud provider's service.
|
|
||||||
type SupportedSignals struct {
|
|
||||||
Logs bool `json:"logs"`
|
|
||||||
Metrics bool `json:"metrics"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DataCollected is curated static list of metrics and logs, this is shown as part of service overview.
|
|
||||||
type DataCollected struct {
|
|
||||||
Logs []CollectedLogAttribute `json:"logs"`
|
|
||||||
Metrics []CollectedMetric `json:"metrics"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CollectedLogAttribute represents a log attribute that is present in all log entries for a service,
|
|
||||||
// this is shown as part of service overview.
|
|
||||||
type CollectedLogAttribute struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CollectedMetric represents a metric that is collected for a service, this is shown as part of service overview.
|
|
||||||
type CollectedMetric struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Unit string `json:"unit"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AWSCollectionStrategy represents signal collection strategy for AWS services.
|
|
||||||
// this is AWS specific.
|
|
||||||
// NOTE: this structure is still using snake case, for backward compatibility,
|
|
||||||
// with existing agents.
|
|
||||||
type AWSCollectionStrategy struct {
|
|
||||||
Metrics *AWSMetricsStrategy `json:"aws_metrics,omitempty"`
|
|
||||||
Logs *AWSLogsStrategy `json:"aws_logs,omitempty"`
|
|
||||||
S3Buckets map[string][]string `json:"s3_buckets,omitempty"` // Only available in S3 Sync Service Type in AWS
|
|
||||||
}
|
|
||||||
|
|
||||||
// AWSMetricsStrategy represents metrics collection strategy for AWS services.
|
|
||||||
// this is AWS specific.
|
|
||||||
// NOTE: this structure is still using snake case, for backward compatibility,
|
|
||||||
// with existing agents.
|
|
||||||
type AWSMetricsStrategy struct {
|
|
||||||
// to be used as https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudwatch-metricstream.html#cfn-cloudwatch-metricstream-includefilters
|
|
||||||
StreamFilters []struct {
|
|
||||||
// json tags here are in the shape expected by AWS API as detailed at
|
|
||||||
// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudwatch-metricstream-metricstreamfilter.html
|
|
||||||
Namespace string `json:"Namespace"`
|
|
||||||
MetricNames []string `json:"MetricNames,omitempty"`
|
|
||||||
} `json:"cloudwatch_metric_stream_filters"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AWSLogsStrategy represents logs collection strategy for AWS services.
|
|
||||||
// this is AWS specific.
|
|
||||||
// NOTE: this structure is still using snake case, for backward compatibility,
|
|
||||||
// with existing agents.
|
|
||||||
type AWSLogsStrategy struct {
|
|
||||||
Subscriptions []struct {
|
|
||||||
// subscribe to all logs groups with specified prefix.
|
|
||||||
// eg: `/aws/rds/`
|
|
||||||
LogGroupNamePrefix string `json:"log_group_name_prefix"`
|
|
||||||
|
|
||||||
// https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html
|
|
||||||
// "" implies no filtering is required.
|
|
||||||
FilterPattern string `json:"filter_pattern"`
|
|
||||||
} `json:"cloudwatch_logs_subscriptions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dashboard represents a dashboard definition for cloud integration.
|
|
||||||
// This is used to show available pre-made dashboards for a service,
|
|
||||||
// hence has additional fields like name and description and url for redirection to the right dashboard on click.
|
|
||||||
type Dashboard struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
Url string `json:"url"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Image string `json:"image"`
|
|
||||||
Definition dashboardtypes.StorableDashboardData `json:"definition,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UTILS
|
|
||||||
|
|
||||||
// GetCloudIntegrationDashboardID returns the dashboard id for a cloud integration, given the cloud provider, service id, and dashboard id.
|
|
||||||
// This is used to generate unique dashboard ids for cloud integration, and also to parse the dashboard id to get the cloud provider and service id when needed.
|
|
||||||
func GetCloudIntegrationDashboardID(cloudProvider CloudProviderType, svcId, dashboardId string) string {
|
|
||||||
return fmt.Sprintf("cloud-integration--%s--%s--%s", cloudProvider, svcId, dashboardId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDashboardsFromAssets returns the list of dashboards for the cloud provider service from definition.
|
|
||||||
func GetDashboardsFromAssets(
|
|
||||||
svcId string,
|
|
||||||
orgID valuer.UUID,
|
|
||||||
cloudProvider CloudProviderType,
|
|
||||||
createdAt time.Time,
|
|
||||||
assets Assets,
|
|
||||||
) []*dashboardtypes.Dashboard {
|
|
||||||
dashboards := make([]*dashboardtypes.Dashboard, 0)
|
|
||||||
|
|
||||||
for _, d := range assets.Dashboards {
|
|
||||||
author := fmt.Sprintf("%s-integration", cloudProvider)
|
|
||||||
dashboards = append(dashboards, &dashboardtypes.Dashboard{
|
|
||||||
ID: GetCloudIntegrationDashboardID(cloudProvider, svcId, d.Id),
|
|
||||||
Locked: true,
|
|
||||||
OrgID: orgID,
|
|
||||||
Data: d.Definition,
|
|
||||||
TimeAuditable: types.TimeAuditable{
|
|
||||||
CreatedAt: createdAt,
|
|
||||||
UpdatedAt: createdAt,
|
|
||||||
},
|
|
||||||
UserAuditable: types.UserAuditable{
|
|
||||||
CreatedBy: author,
|
|
||||||
UpdatedBy: author,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return dashboards
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package cloudintegrationtypes
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Store interface {
|
|
||||||
// GetAccountByID returns a cloud integration account by id
|
|
||||||
GetAccountByID(ctx context.Context, orgID, id valuer.UUID, provider CloudProviderType) (*StorableCloudIntegration, error)
|
|
||||||
|
|
||||||
// CreateAccount creates a new cloud integration account
|
|
||||||
CreateAccount(ctx context.Context, orgID valuer.UUID, account *StorableCloudIntegration) (*StorableCloudIntegration, error)
|
|
||||||
|
|
||||||
// UpdateAccount updates an existing cloud integration account
|
|
||||||
UpdateAccount(ctx context.Context, account *StorableCloudIntegration) error
|
|
||||||
|
|
||||||
// RemoveAccount marks a cloud integration account as removed by setting the RemovedAt field
|
|
||||||
RemoveAccount(ctx context.Context, orgID, id valuer.UUID, provider CloudProviderType) error
|
|
||||||
|
|
||||||
// GetConnectedAccounts returns all the cloud integration accounts for the org and cloud provider
|
|
||||||
GetConnectedAccounts(ctx context.Context, orgID valuer.UUID, provider CloudProviderType) ([]*StorableCloudIntegration, error)
|
|
||||||
|
|
||||||
// GetConnectedAccount for given provider
|
|
||||||
GetConnectedAccount(ctx context.Context, orgID valuer.UUID, provider CloudProviderType, providerAccountID string) (*StorableCloudIntegration, error)
|
|
||||||
|
|
||||||
// cloud_integration_service related methods
|
|
||||||
|
|
||||||
// GetServiceByType returns the cloud integration service for the given cloud integration id and service type
|
|
||||||
GetServiceByType(ctx context.Context, cloudIntegrationID valuer.UUID, serviceType string) (*StorableCloudIntegrationService, error)
|
|
||||||
|
|
||||||
// CreateService creates a new cloud integration service for the given cloud integration id and service type
|
|
||||||
CreateService(ctx context.Context, cloudIntegrationID valuer.UUID, service *StorableCloudIntegrationService) (*StorableCloudIntegrationService, error)
|
|
||||||
|
|
||||||
// UpdateService updates an existing cloud integration service for the given cloud integration id and service type
|
|
||||||
UpdateService(ctx context.Context, cloudIntegrationID valuer.UUID, service *StorableCloudIntegrationService) error
|
|
||||||
|
|
||||||
// GetServices returns all the cloud integration services for the given cloud integration id
|
|
||||||
GetServices(ctx context.Context, cloudIntegrationID valuer.UUID) ([]*StorableCloudIntegrationService, error)
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
package zeustypes
|
package zeustypes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,27 +35,6 @@ type Host struct {
|
|||||||
URL string `json:"url" required:"true"`
|
URL string `json:"url" required:"true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GettableDeployment represents the parsed deployment info from zeus.GetDeployment.
|
|
||||||
type GettableDeployment struct {
|
|
||||||
Name string
|
|
||||||
SignozAPIUrl string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewGettableDeployment parses raw GetDeployment bytes into a GettableDeployment.
|
|
||||||
func NewGettableDeployment(data []byte) (*GettableDeployment, error) {
|
|
||||||
parsed := gjson.ParseBytes(data)
|
|
||||||
name := parsed.Get("name").String()
|
|
||||||
dns := parsed.Get("cluster.region.dns").String()
|
|
||||||
if name == "" || dns == "" {
|
|
||||||
return nil, errors.NewInternalf(errors.CodeInternal,
|
|
||||||
"deployment info response missing name or cluster region dns")
|
|
||||||
}
|
|
||||||
return &GettableDeployment{
|
|
||||||
Name: name,
|
|
||||||
SignozAPIUrl: fmt.Sprintf("https://%s.%s", name, dns),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewGettableHost(data []byte) *GettableHost {
|
func NewGettableHost(data []byte) *GettableHost {
|
||||||
parsed := gjson.ParseBytes(data)
|
parsed := gjson.ParseBytes(data)
|
||||||
dns := parsed.Get("cluster.region.dns").String()
|
dns := parsed.Get("cluster.region.dns").String()
|
||||||
|
|||||||
Reference in New Issue
Block a user