mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-29 16:14:42 +00:00
Merge branch 'main' into fix/add-span-links-to-span-details
This commit is contained in:
commit
91c8b0fa12
4
.github/pull_request_template.md
vendored
4
.github/pull_request_template.md
vendored
@ -32,9 +32,7 @@ ex:
|
|||||||
|
|
||||||
> Tag the relevant teams for review:
|
> Tag the relevant teams for review:
|
||||||
|
|
||||||
- [ ] @SigNoz/frontend
|
- frontend / backend / devops
|
||||||
- [ ] @SigNoz/backend
|
|
||||||
- [ ] @SigNoz/devops
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
5
.github/workflows/build-enterprise.yaml
vendored
5
.github/workflows/build-enterprise.yaml
vendored
@ -67,9 +67,8 @@ jobs:
|
|||||||
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
|
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> frontend/.env
|
||||||
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
|
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> frontend/.env
|
||||||
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
|
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> frontend/.env
|
||||||
echo 'CUSTOMERIO_ID="${{ secrets.CUSTOMERIO_ID }}"' >> frontend/.env
|
echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> frontend/.env
|
||||||
echo 'CUSTOMERIO_SITE_ID="${{ secrets.CUSTOMERIO_SITE_ID }}"' >> frontend/.env
|
echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> frontend/.env
|
||||||
echo 'USERPILOT_KEY="${{ secrets.USERPILOT_KEY }}"' >> frontend/.env
|
|
||||||
- name: cache-dotenv
|
- name: cache-dotenv
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
3
.github/workflows/build-staging.yaml
vendored
3
.github/workflows/build-staging.yaml
vendored
@ -66,7 +66,8 @@ jobs:
|
|||||||
echo 'CI=1' > frontend/.env
|
echo 'CI=1' > frontend/.env
|
||||||
echo 'TUNNEL_URL="${{ secrets.NP_TUNNEL_URL }}"' >> frontend/.env
|
echo 'TUNNEL_URL="${{ secrets.NP_TUNNEL_URL }}"' >> frontend/.env
|
||||||
echo 'TUNNEL_DOMAIN="${{ secrets.NP_TUNNEL_DOMAIN }}"' >> frontend/.env
|
echo 'TUNNEL_DOMAIN="${{ secrets.NP_TUNNEL_DOMAIN }}"' >> frontend/.env
|
||||||
echo 'USERPILOT_KEY="${{ secrets.NP_USERPILOT_KEY }}"' >> frontend/.env
|
echo 'PYLON_APP_ID="${{ secrets.NP_PYLON_APP_ID }}"' >> frontend/.env
|
||||||
|
echo 'APPCUES_APP_ID="${{ secrets.NP_APPCUES_APP_ID }}"' >> frontend/.env
|
||||||
- name: cache-dotenv
|
- name: cache-dotenv
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
5
.github/workflows/gor-signoz.yaml
vendored
5
.github/workflows/gor-signoz.yaml
vendored
@ -33,9 +33,8 @@ jobs:
|
|||||||
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> .env
|
echo 'TUNNEL_URL="${{ secrets.TUNNEL_URL }}"' >> .env
|
||||||
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> .env
|
echo 'TUNNEL_DOMAIN="${{ secrets.TUNNEL_DOMAIN }}"' >> .env
|
||||||
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> .env
|
echo 'POSTHOG_KEY="${{ secrets.POSTHOG_KEY }}"' >> .env
|
||||||
echo 'CUSTOMERIO_ID="${{ secrets.CUSTOMERIO_ID }}"' >> .env
|
echo 'PYLON_APP_ID="${{ secrets.PYLON_APP_ID }}"' >> .env
|
||||||
echo 'CUSTOMERIO_SITE_ID="${{ secrets.CUSTOMERIO_SITE_ID }}"' >> .env
|
echo 'APPCUES_APP_ID="${{ secrets.APPCUES_APP_ID }}"' >> .env
|
||||||
echo 'USERPILOT_KEY="${{ secrets.USERPILOT_KEY }}"' >> .env
|
|
||||||
- name: build-frontend
|
- name: build-frontend
|
||||||
run: make js-build
|
run: make js-build
|
||||||
- name: upload-frontend-artifact
|
- name: upload-frontend-artifact
|
||||||
|
|||||||
@ -1,4 +1,33 @@
|
|||||||
|
linters:
|
||||||
|
default: standard
|
||||||
|
enable:
|
||||||
|
- bodyclose
|
||||||
|
- misspell
|
||||||
|
- nilnil
|
||||||
|
- sloglint
|
||||||
|
- depguard
|
||||||
|
- iface
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
sloglint:
|
||||||
|
no-mixed-args: true
|
||||||
|
kv-only: true
|
||||||
|
no-global: all
|
||||||
|
context: all
|
||||||
|
static-msg: true
|
||||||
|
msg-style: lowercased
|
||||||
|
key-naming-case: snake
|
||||||
|
depguard:
|
||||||
|
rules:
|
||||||
|
nozap:
|
||||||
|
deny:
|
||||||
|
- pkg: "go.uber.org/zap"
|
||||||
|
desc: "Do not use zap logger. Use slog instead."
|
||||||
|
iface:
|
||||||
|
enable:
|
||||||
|
- identical
|
||||||
issues:
|
issues:
|
||||||
exclude-dirs:
|
exclude-dirs:
|
||||||
- "pkg/query-service"
|
- "pkg/query-service"
|
||||||
- "ee/query-service"
|
- "ee/query-service"
|
||||||
|
- "scripts/"
|
||||||
|
|||||||
26
ee/licensing/config.go
Normal file
26
ee/licensing/config.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package licensing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/licensing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
config licensing.Config
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// initializes the licensing configuration
|
||||||
|
func Config(pollInterval time.Duration, failureThreshold int) licensing.Config {
|
||||||
|
once.Do(func() {
|
||||||
|
config = licensing.Config{PollInterval: pollInterval, FailureThreshold: failureThreshold}
|
||||||
|
if err := config.Validate(); err != nil {
|
||||||
|
panic(fmt.Errorf("invalid licensing config: %w", err))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
168
ee/licensing/httplicensing/api.go
Normal file
168
ee/licensing/httplicensing/api.go
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
package httplicensing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/http/render"
|
||||||
|
"github.com/SigNoz/signoz/pkg/licensing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type licensingAPI struct {
|
||||||
|
licensing licensing.Licensing
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLicensingAPI(licensing licensing.Licensing) licensing.API {
|
||||||
|
return &licensingAPI{licensing: licensing}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *licensingAPI) Activate(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(licensetypes.PostableLicense)
|
||||||
|
err = json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = api.licensing.Activate(r.Context(), orgID, req.Key)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Success(rw, http.StatusAccepted, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *licensingAPI) GetActive(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
license, err := api.licensing.GetActive(r.Context(), orgID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gettableLicense := licensetypes.NewGettableLicense(license.Data, license.Key)
|
||||||
|
render.Success(rw, http.StatusOK, gettableLicense)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *licensingAPI) Refresh(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = api.licensing.Refresh(r.Context(), orgID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Success(rw, http.StatusNoContent, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *licensingAPI) Checkout(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(licensetypes.PostableSubscription)
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gettableSubscription, err := api.licensing.Checkout(ctx, orgID, req)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Success(rw, http.StatusCreated, gettableSubscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *licensingAPI) Portal(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(licensetypes.PostableSubscription)
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gettableSubscription, err := api.licensing.Portal(ctx, orgID, req)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render.Success(rw, http.StatusCreated, gettableSubscription)
|
||||||
|
}
|
||||||
280
ee/licensing/httplicensing/provider.go
Normal file
280
ee/licensing/httplicensing/provider.go
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
package httplicensing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore"
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"github.com/SigNoz/signoz/pkg/licensing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
"github.com/SigNoz/signoz/pkg/zeus"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type provider struct {
|
||||||
|
store licensetypes.Store
|
||||||
|
zeus zeus.Zeus
|
||||||
|
config licensing.Config
|
||||||
|
settings factory.ScopedProviderSettings
|
||||||
|
stopChan chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
|
||||||
|
return factory.NewProviderFactory(factory.MustNewName("http"), func(ctx context.Context, providerSettings factory.ProviderSettings, config licensing.Config) (licensing.Licensing, error) {
|
||||||
|
return New(ctx, providerSettings, config, store, zeus)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus) (licensing.Licensing, error) {
|
||||||
|
settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/ee/licensing/httplicensing")
|
||||||
|
licensestore := sqllicensingstore.New(sqlstore)
|
||||||
|
return &provider{store: licensestore, zeus: zeus, config: config, settings: settings, stopChan: make(chan struct{})}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) Start(ctx context.Context) error {
|
||||||
|
tick := time.NewTicker(provider.config.PollInterval)
|
||||||
|
defer tick.Stop()
|
||||||
|
|
||||||
|
err := provider.Validate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
provider.settings.Logger().ErrorContext(ctx, "failed to validate license from upstream server", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-provider.stopChan:
|
||||||
|
return nil
|
||||||
|
case <-tick.C:
|
||||||
|
err := provider.Validate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
provider.settings.Logger().ErrorContext(ctx, "failed to validate license from upstream server", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) Stop(ctx context.Context) error {
|
||||||
|
provider.settings.Logger().DebugContext(ctx, "license validation stopped")
|
||||||
|
close(provider.stopChan)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) Validate(ctx context.Context) error {
|
||||||
|
organizations, err := provider.store.ListOrganizations(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, organizationID := range organizations {
|
||||||
|
err := provider.Refresh(ctx, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(organizations) == 0 {
|
||||||
|
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) Activate(ctx context.Context, organizationID valuer.UUID, key string) error {
|
||||||
|
data, err := provider.zeus.GetLicense(ctx, key)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to fetch license data with upstream server")
|
||||||
|
}
|
||||||
|
|
||||||
|
license, err := licensetypes.NewLicense(data, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity")
|
||||||
|
}
|
||||||
|
|
||||||
|
storableLicense := licensetypes.NewStorableLicenseFromLicense(license)
|
||||||
|
err = provider.store.Create(ctx, storableLicense)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = provider.InitFeatures(ctx, license.Features)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) GetActive(ctx context.Context, organizationID valuer.UUID) (*licensetypes.License, error) {
|
||||||
|
storableLicenses, err := provider.store.GetAll(ctx, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
activeLicense, err := licensetypes.GetActiveLicenseFromStorableLicenses(storableLicenses, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeLicense, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUID) error {
|
||||||
|
activeLicense, err := provider.GetActive(ctx, organizationID)
|
||||||
|
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||||
|
provider.settings.Logger().ErrorContext(ctx, "license validation failed", "org_id", organizationID.StringValue())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && errors.Ast(err, errors.TypeNotFound) {
|
||||||
|
provider.settings.Logger().DebugContext(ctx, "no active license found, defaulting to basic plan", "org_id", organizationID.StringValue())
|
||||||
|
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := provider.zeus.GetLicense(ctx, activeLicense.Key)
|
||||||
|
if err != nil {
|
||||||
|
if time.Since(activeLicense.LastValidatedAt) > time.Duration(provider.config.FailureThreshold)*provider.config.PollInterval {
|
||||||
|
provider.settings.Logger().ErrorContext(ctx, "license validation failed for consecutive poll intervals, defaulting to basic plan", "failure_threshold", provider.config.FailureThreshold, "license_id", activeLicense.ID.StringValue(), "org_id", organizationID.StringValue())
|
||||||
|
err = provider.InitFeatures(ctx, licensetypes.BasicPlan)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = activeLicense.Update(data)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity from license data")
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedStorableLicense := licensetypes.NewStorableLicenseFromLicense(activeLicense)
|
||||||
|
err = provider.store.Update(ctx, organizationID, updatedStorableLicense)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) Checkout(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) {
|
||||||
|
activeLicense, err := provider.GetActive(ctx, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(postableSubscription)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal checkout payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := provider.zeus.GetCheckoutURL(ctx, activeLicense.Key, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to generate checkout session")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) Portal(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) {
|
||||||
|
activeLicense, err := provider.GetActive(ctx, organizationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.Marshal(postableSubscription)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal portal payload")
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := provider.zeus.GetPortalURL(ctx, activeLicense.Key, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to generate portal session")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// feature surrogate
|
||||||
|
func (provider *provider) CheckFeature(ctx context.Context, key string) error {
|
||||||
|
feature, err := provider.store.GetFeature(ctx, key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if feature.Active {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.Newf(errors.TypeUnsupported, licensing.ErrCodeFeatureUnavailable, "feature unavailable: %s", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) GetFeatureFlag(ctx context.Context, key string) (*featuretypes.GettableFeature, error) {
|
||||||
|
featureStatus, err := provider.store.GetFeature(ctx, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &featuretypes.GettableFeature{
|
||||||
|
Name: featureStatus.Name,
|
||||||
|
Active: featureStatus.Active,
|
||||||
|
Usage: int64(featureStatus.Usage),
|
||||||
|
UsageLimit: int64(featureStatus.UsageLimit),
|
||||||
|
Route: featureStatus.Route,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) GetFeatureFlags(ctx context.Context) ([]*featuretypes.GettableFeature, error) {
|
||||||
|
storableFeatures, err := provider.store.GetAllFeatures(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gettableFeatures := make([]*featuretypes.GettableFeature, len(storableFeatures))
|
||||||
|
for idx, gettableFeature := range storableFeatures {
|
||||||
|
gettableFeatures[idx] = &featuretypes.GettableFeature{
|
||||||
|
Name: gettableFeature.Name,
|
||||||
|
Active: gettableFeature.Active,
|
||||||
|
Usage: int64(gettableFeature.Usage),
|
||||||
|
UsageLimit: int64(gettableFeature.UsageLimit),
|
||||||
|
Route: gettableFeature.Route,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return gettableFeatures, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) InitFeatures(ctx context.Context, features []*featuretypes.GettableFeature) error {
|
||||||
|
featureStatus := make([]*featuretypes.StorableFeature, len(features))
|
||||||
|
for i, f := range features {
|
||||||
|
featureStatus[i] = &featuretypes.StorableFeature{
|
||||||
|
Name: f.Name,
|
||||||
|
Active: f.Active,
|
||||||
|
Usage: int(f.Usage),
|
||||||
|
UsageLimit: int(f.UsageLimit),
|
||||||
|
Route: f.Route,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider.store.InitFeatures(ctx, featureStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *provider) UpdateFeatureFlag(ctx context.Context, feature *featuretypes.GettableFeature) error {
|
||||||
|
return provider.store.UpdateFeature(ctx, &featuretypes.StorableFeature{
|
||||||
|
Name: feature.Name,
|
||||||
|
Active: feature.Active,
|
||||||
|
Usage: int(feature.Usage),
|
||||||
|
UsageLimit: int(feature.UsageLimit),
|
||||||
|
Route: feature.Route,
|
||||||
|
})
|
||||||
|
}
|
||||||
186
ee/licensing/licensingstore/sqllicensingstore/store.go
Normal file
186
ee/licensing/licensingstore/sqllicensingstore/store.go
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
package sqllicensingstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/licensetypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type store struct {
|
||||||
|
sqlstore sqlstore.SQLStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(sqlstore sqlstore.SQLStore) licensetypes.Store {
|
||||||
|
return &store{sqlstore}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *store) Create(ctx context.Context, storableLicense *licensetypes.StorableLicense) error {
|
||||||
|
_, err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewInsert().
|
||||||
|
Model(storableLicense).
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "license with ID: %s already exists", storableLicense.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *store) Get(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) (*licensetypes.StorableLicense, error) {
|
||||||
|
storableLicense := new(licensetypes.StorableLicense)
|
||||||
|
err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
|
Model(storableLicense).
|
||||||
|
Where("org_id = ?", organizationID).
|
||||||
|
Where("id = ?", licenseID).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "license with ID: %s does not exist", licenseID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return storableLicense, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *store) GetAll(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.StorableLicense, error) {
|
||||||
|
storableLicenses := make([]*licensetypes.StorableLicense, 0)
|
||||||
|
err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
|
Model(&storableLicenses).
|
||||||
|
Where("org_id = ?", organizationID).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "licenses for organizationID: %s does not exists", organizationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return storableLicenses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *store) Update(ctx context.Context, organizationID valuer.UUID, storableLicense *licensetypes.StorableLicense) error {
|
||||||
|
_, err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewUpdate().
|
||||||
|
Model(storableLicense).
|
||||||
|
WherePK().
|
||||||
|
Where("org_id = ?", organizationID).
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to update license with ID: %s", storableLicense.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *store) ListOrganizations(ctx context.Context) ([]valuer.UUID, error) {
|
||||||
|
orgIDStrs := make([]string, 0)
|
||||||
|
err := store.sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
|
Model(new(types.Organization)).
|
||||||
|
Column("id").
|
||||||
|
Scan(ctx, &orgIDStrs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
orgIDs := make([]valuer.UUID, len(orgIDStrs))
|
||||||
|
for idx, orgIDStr := range orgIDStrs {
|
||||||
|
orgID, err := valuer.NewUUID(orgIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
orgIDs[idx] = orgID
|
||||||
|
}
|
||||||
|
|
||||||
|
return orgIDs, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *store) CreateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error {
|
||||||
|
_, err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewInsert().
|
||||||
|
Model(storableFeature).
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "feature with name:%s already exists", storableFeature.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *store) GetFeature(ctx context.Context, key string) (*featuretypes.StorableFeature, error) {
|
||||||
|
storableFeature := new(featuretypes.StorableFeature)
|
||||||
|
err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
|
Model(storableFeature).
|
||||||
|
Where("name = ?", key).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "feature with name:%s does not exist", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return storableFeature, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *store) GetAllFeatures(ctx context.Context) ([]*featuretypes.StorableFeature, error) {
|
||||||
|
storableFeatures := make([]*featuretypes.StorableFeature, 0)
|
||||||
|
err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewSelect().
|
||||||
|
Model(&storableFeatures).
|
||||||
|
Scan(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "features do not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
return storableFeatures, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *store) InitFeatures(ctx context.Context, storableFeatures []*featuretypes.StorableFeature) error {
|
||||||
|
_, err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewInsert().
|
||||||
|
Model(&storableFeatures).
|
||||||
|
On("CONFLICT (name) DO UPDATE").
|
||||||
|
Set("active = EXCLUDED.active").
|
||||||
|
Set("usage = EXCLUDED.usage").
|
||||||
|
Set("usage_limit = EXCLUDED.usage_limit").
|
||||||
|
Set("route = EXCLUDED.route").
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to initialise features")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *store) UpdateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error {
|
||||||
|
_, err := store.
|
||||||
|
sqlstore.
|
||||||
|
BunDB().
|
||||||
|
NewUpdate().
|
||||||
|
Model(storableFeature).
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to update feature with key: %s", storableFeature.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -1,416 +0,0 @@
|
|||||||
package impluser
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"slices"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
|
||||||
"github.com/SigNoz/signoz/pkg/http/render"
|
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EnterpriseHandler embeds the base handler implementation
|
|
||||||
type Handler struct {
|
|
||||||
user.Handler // Embed the base handler interface
|
|
||||||
module user.Module
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHandler(module user.Module) user.Handler {
|
|
||||||
baseHandler := impluser.NewHandler(module)
|
|
||||||
return &Handler{
|
|
||||||
Handler: baseHandler,
|
|
||||||
module: module,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
var req types.PostableLoginRequest
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.RefreshToken == "" {
|
|
||||||
// the EE handler wrapper passes the feature flag value in context
|
|
||||||
ssoAvailable, ok := ctx.Value(types.SSOAvailable).(bool)
|
|
||||||
if !ok {
|
|
||||||
render.Error(w, errors.New(errors.TypeInternal, errors.CodeInternal, "failed to retrieve SSO availability"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ssoAvailable {
|
|
||||||
_, err := h.module.CanUsePassword(ctx, req.Email)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := h.module.GetAuthenticatedUser(ctx, req.OrgID, req.Email, req.Password, req.RefreshToken)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jwt, err := h.module.GetJWTForUser(ctx, user)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
gettableLoginResponse := &types.GettableLoginResponse{
|
|
||||||
GettableUserJwt: jwt,
|
|
||||||
UserID: user.ID.String(),
|
|
||||||
}
|
|
||||||
|
|
||||||
render.Success(w, http.StatusOK, gettableLoginResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override only the methods you need with enterprise-specific implementations
|
|
||||||
func (h *Handler) LoginPrecheck(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// assume user is valid unless proven otherwise and assign default values for rest of the fields
|
|
||||||
|
|
||||||
email := r.URL.Query().Get("email")
|
|
||||||
sourceUrl := r.URL.Query().Get("ref")
|
|
||||||
orgID := r.URL.Query().Get("orgID")
|
|
||||||
|
|
||||||
resp, err := h.module.LoginPrecheck(ctx, orgID, email, sourceUrl)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
render.Success(w, http.StatusOK, resp)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
req := new(types.PostableAcceptInvite)
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
|
|
||||||
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode user"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get invite object
|
|
||||||
invite, err := h.module.GetInviteByToken(ctx, req.InviteToken)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
orgDomain, err := h.module.GetAuthDomainByEmail(ctx, invite.Email)
|
|
||||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
precheckResp := &types.GettableLoginPrecheck{
|
|
||||||
SSO: false,
|
|
||||||
IsUser: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
if invite.Name == "" && req.DisplayName != "" {
|
|
||||||
invite.Name = req.DisplayName
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := types.NewUser(invite.Name, invite.Email, invite.Role, invite.OrgID)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if orgDomain != nil && orgDomain.SsoEnabled {
|
|
||||||
// sso is enabled, create user and respond precheck data
|
|
||||||
err = h.module.CreateUser(ctx, user)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if sso is enforced for the org
|
|
||||||
precheckResp, err = h.module.LoginPrecheck(ctx, invite.OrgID, user.Email, req.SourceURL)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
password, err := types.NewFactorPassword(req.Password)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = h.module.CreateUserWithPassword(ctx, user, password)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
precheckResp.IsUser = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete the invite
|
|
||||||
if err := h.module.DeleteInvite(ctx, invite.OrgID, invite.ID); err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
render.Success(w, http.StatusOK, precheckResp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) GetInvite(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
token := mux.Vars(r)["token"]
|
|
||||||
sourceUrl := r.URL.Query().Get("ref")
|
|
||||||
invite, err := h.module.GetInviteByToken(ctx, token)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// precheck the user
|
|
||||||
precheckResp, err := h.module.LoginPrecheck(ctx, invite.OrgID, invite.Email, sourceUrl)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
gettableInvite := &types.GettableEEInvite{
|
|
||||||
GettableInvite: *invite,
|
|
||||||
PreCheck: precheckResp,
|
|
||||||
}
|
|
||||||
|
|
||||||
render.Success(w, http.StatusOK, gettableInvite)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) CreateAPIKey(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is not a valid uuid-v7"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userID, err := valuer.NewUUID(claims.UserID)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "userId is not a valid uuid-v7"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req := new(types.PostableAPIKey)
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
|
|
||||||
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode api key"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
apiKey, err := types.NewStorableAPIKey(
|
|
||||||
req.Name,
|
|
||||||
userID,
|
|
||||||
req.Role,
|
|
||||||
req.ExpiresInDays,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.module.CreateAPIKey(ctx, apiKey)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
createdApiKey, err := h.module.GetAPIKey(ctx, orgID, apiKey.ID)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// just corrected the status code, response is same,
|
|
||||||
render.Success(w, http.StatusCreated, createdApiKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) ListAPIKeys(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is not a valid uuid-v7"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
apiKeys, err := h.module.ListAPIKeys(ctx, orgID)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// for backward compatibility
|
|
||||||
if len(apiKeys) == 0 {
|
|
||||||
render.Success(w, http.StatusOK, []types.GettableAPIKey{})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make([]*types.GettableAPIKey, len(apiKeys))
|
|
||||||
for i, apiKey := range apiKeys {
|
|
||||||
result[i] = types.NewGettableAPIKeyFromStorableAPIKey(apiKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
render.Success(w, http.StatusOK, result)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is not a valid uuid-v7"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userID, err := valuer.NewUUID(claims.UserID)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "userId is not a valid uuid-v7"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req := types.StorableAPIKey{}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode api key"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
idStr := mux.Vars(r)["id"]
|
|
||||||
id, err := valuer.NewUUID(idStr)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//get the API Key
|
|
||||||
existingAPIKey, err := h.module.GetAPIKey(ctx, orgID, id)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the user
|
|
||||||
createdByUser, err := h.module.GetUserByID(ctx, orgID.String(), existingAPIKey.UserID.String())
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(createdByUser.Email)) {
|
|
||||||
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "API Keys for integration users cannot be revoked"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.module.UpdateAPIKey(ctx, id, &req, userID)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
render.Success(w, http.StatusNoContent, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) RevokeAPIKey(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
idStr := mux.Vars(r)["id"]
|
|
||||||
id, err := valuer.NewUUID(idStr)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is not a valid uuid-v7"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userID, err := valuer.NewUUID(claims.UserID)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "userId is not a valid uuid-v7"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
//get the API Key
|
|
||||||
existingAPIKey, err := h.module.GetAPIKey(ctx, orgID, id)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the user
|
|
||||||
createdByUser, err := h.module.GetUserByID(ctx, orgID.String(), existingAPIKey.UserID.String())
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if slices.Contains(types.AllIntegrationUserEmails, types.IntegrationUserEmail(createdByUser.Email)) {
|
|
||||||
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "API Keys for integration users cannot be revoked"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.module.RevokeAPIKey(ctx, id, userID); err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
render.Success(w, http.StatusNoContent, nil)
|
|
||||||
}
|
|
||||||
@ -1,252 +0,0 @@
|
|||||||
package impluser
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
|
||||||
"github.com/SigNoz/signoz/pkg/emailing"
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
|
||||||
baseimpl "github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EnterpriseModule embeds the base module implementation
|
|
||||||
type Module struct {
|
|
||||||
user.Module // Embed the base module implementation
|
|
||||||
store types.UserStore
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewModule(store types.UserStore, jwt *authtypes.JWT, emailing emailing.Emailing, providerSettings factory.ProviderSettings) user.Module {
|
|
||||||
baseModule := baseimpl.NewModule(store, jwt, emailing, providerSettings)
|
|
||||||
return &Module{
|
|
||||||
Module: baseModule,
|
|
||||||
store: store,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Module) createUserForSAMLRequest(ctx context.Context, email string) (*types.User, error) {
|
|
||||||
// get auth domain from email domain
|
|
||||||
_, err := m.GetAuthDomainByEmail(ctx, email)
|
|
||||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// get name from email
|
|
||||||
parts := strings.Split(email, "@")
|
|
||||||
if len(parts) < 2 {
|
|
||||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid email format")
|
|
||||||
}
|
|
||||||
name := parts[0]
|
|
||||||
|
|
||||||
defaultOrgID, err := m.store.GetDefaultOrgID(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := types.NewUser(name, email, types.RoleViewer.String(), defaultOrgID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = m.CreateUser(ctx, user)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Module) PrepareSsoRedirect(ctx context.Context, redirectUri, email string, jwt *authtypes.JWT) (string, error) {
|
|
||||||
users, err := m.GetUsersByEmail(ctx, email)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("failed to get user with email received from auth provider", zap.String("error", err.Error()))
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
user := &types.User{}
|
|
||||||
|
|
||||||
if len(users) == 0 {
|
|
||||||
newUser, err := m.createUserForSAMLRequest(ctx, email)
|
|
||||||
user = newUser
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("failed to create user with email received from auth provider", zap.Error(err))
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
user = &users[0].User
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenStore, err := m.GetJWTForUser(ctx, user)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("failed to generate token for SSO login user", zap.Error(err))
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s?jwt=%s&usr=%s&refreshjwt=%s",
|
|
||||||
redirectUri,
|
|
||||||
tokenStore.AccessJwt,
|
|
||||||
user.ID,
|
|
||||||
tokenStore.RefreshJwt), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Module) CanUsePassword(ctx context.Context, email string) (bool, error) {
|
|
||||||
domain, err := m.GetAuthDomainByEmail(ctx, email)
|
|
||||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if domain != nil && domain.SsoEnabled {
|
|
||||||
// sso is enabled, check if the user has admin role
|
|
||||||
users, err := m.GetUsersByEmail(ctx, email)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(users) == 0 {
|
|
||||||
return false, errors.New(errors.TypeNotFound, errors.CodeNotFound, "user not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
if users[0].Role != types.RoleAdmin.String() {
|
|
||||||
return false, errors.New(errors.TypeForbidden, errors.CodeForbidden, "auth method not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Module) LoginPrecheck(ctx context.Context, orgID, email, sourceUrl string) (*types.GettableLoginPrecheck, error) {
|
|
||||||
resp := &types.GettableLoginPrecheck{IsUser: true, CanSelfRegister: false}
|
|
||||||
|
|
||||||
// check if email is a valid user
|
|
||||||
users, err := m.GetUsersByEmail(ctx, email)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(users) == 0 {
|
|
||||||
resp.IsUser = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// give them an option to select an org
|
|
||||||
if orgID == "" && len(users) > 1 {
|
|
||||||
resp.SelectOrg = true
|
|
||||||
resp.Orgs = make([]string, len(users))
|
|
||||||
for i, user := range users {
|
|
||||||
resp.Orgs[i] = user.OrgID
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// select the user with the corresponding orgID
|
|
||||||
if len(users) > 1 {
|
|
||||||
found := false
|
|
||||||
for _, tuser := range users {
|
|
||||||
if tuser.OrgID == orgID {
|
|
||||||
// user = tuser
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
resp.IsUser = false
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// the EE handler wrapper passes the feature flag value in context
|
|
||||||
ssoAvailable, ok := ctx.Value(types.SSOAvailable).(bool)
|
|
||||||
if !ok {
|
|
||||||
zap.L().Error("failed to retrieve ssoAvailable from context")
|
|
||||||
return nil, errors.New(errors.TypeInternal, errors.CodeInternal, "failed to retrieve SSO availability")
|
|
||||||
}
|
|
||||||
|
|
||||||
if ssoAvailable {
|
|
||||||
|
|
||||||
// TODO(Nitya): in multitenancy this should use orgId as well.
|
|
||||||
orgDomain, err := m.GetAuthDomainByEmail(ctx, email)
|
|
||||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if orgDomain != nil && orgDomain.SsoEnabled {
|
|
||||||
// this is to allow self registration
|
|
||||||
resp.IsUser = true
|
|
||||||
|
|
||||||
// saml is enabled for this domain, lets prepare sso url
|
|
||||||
if sourceUrl == "" {
|
|
||||||
sourceUrl = constants.GetDefaultSiteURL()
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse source url that generated the login request
|
|
||||||
var err error
|
|
||||||
escapedUrl, _ := url.QueryUnescape(sourceUrl)
|
|
||||||
siteUrl, err := url.Parse(escapedUrl)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse referer")
|
|
||||||
}
|
|
||||||
|
|
||||||
// build Idp URL that will authenticat the user
|
|
||||||
// the front-end will redirect user to this url
|
|
||||||
resp.SSOUrl, err = orgDomain.BuildSsoUrl(siteUrl)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("failed to prepare saml request for domain", zap.String("domain", orgDomain.Name), zap.Error(err))
|
|
||||||
return nil, errors.New(errors.TypeInternal, errors.CodeInternal, "failed to prepare saml request for domain")
|
|
||||||
}
|
|
||||||
|
|
||||||
// set SSO to true, as the url is generated correctly
|
|
||||||
resp.SSO = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Module) GetAuthDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, error) {
|
|
||||||
|
|
||||||
if email == "" {
|
|
||||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "email is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
components := strings.Split(email, "@")
|
|
||||||
if len(components) < 2 {
|
|
||||||
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid email format")
|
|
||||||
}
|
|
||||||
|
|
||||||
domain, err := m.store.GetDomainByName(ctx, components[1])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
gettableDomain := &types.GettableOrgDomain{StorableOrgDomain: *domain}
|
|
||||||
if err := gettableDomain.LoadConfig(domain.Data); err != nil {
|
|
||||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to load domain config")
|
|
||||||
}
|
|
||||||
return gettableDomain, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Module) CreateAPIKey(ctx context.Context, apiKey *types.StorableAPIKey) error {
|
|
||||||
return m.store.CreateAPIKey(ctx, apiKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Module) UpdateAPIKey(ctx context.Context, id valuer.UUID, apiKey *types.StorableAPIKey, updaterID valuer.UUID) error {
|
|
||||||
return m.store.UpdateAPIKey(ctx, id, apiKey, updaterID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Module) ListAPIKeys(ctx context.Context, orgID valuer.UUID) ([]*types.StorableAPIKeyUser, error) {
|
|
||||||
return m.store.ListAPIKeys(ctx, orgID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Module) GetAPIKey(ctx context.Context, orgID, id valuer.UUID) (*types.StorableAPIKeyUser, error) {
|
|
||||||
return m.store.GetAPIKey(ctx, orgID, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Module) RevokeAPIKey(ctx context.Context, id, removedByUserID valuer.UUID) error {
|
|
||||||
return m.store.RevokeAPIKey(ctx, id, removedByUserID)
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
package impluser
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
|
||||||
baseimpl "github.com/SigNoz/signoz/pkg/modules/user/impluser"
|
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
type store struct {
|
|
||||||
*baseimpl.Store
|
|
||||||
sqlstore sqlstore.SQLStore
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStore(sqlstore sqlstore.SQLStore) types.UserStore {
|
|
||||||
baseStore := baseimpl.NewStore(sqlstore).(*baseimpl.Store)
|
|
||||||
return &store{
|
|
||||||
Store: baseStore,
|
|
||||||
sqlstore: sqlstore,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *store) GetDomainByName(ctx context.Context, name string) (*types.StorableOrgDomain, error) {
|
|
||||||
domain := new(types.StorableOrgDomain)
|
|
||||||
err := s.sqlstore.BunDB().NewSelect().
|
|
||||||
Model(domain).
|
|
||||||
Where("name = ?", name).
|
|
||||||
Limit(1).
|
|
||||||
Scan(ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, errors.TypeNotFound, errors.CodeNotFound, "failed to get domain from name")
|
|
||||||
}
|
|
||||||
return domain, nil
|
|
||||||
}
|
|
||||||
@ -6,42 +6,30 @@ import (
|
|||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/dao"
|
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
||||||
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
|
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
|
||||||
"github.com/SigNoz/signoz/ee/query-service/interfaces"
|
"github.com/SigNoz/signoz/ee/query-service/interfaces"
|
||||||
"github.com/SigNoz/signoz/ee/query-service/license"
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/usage"
|
"github.com/SigNoz/signoz/ee/query-service/usage"
|
||||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||||
"github.com/SigNoz/signoz/pkg/apis/fields"
|
"github.com/SigNoz/signoz/pkg/apis/fields"
|
||||||
"github.com/SigNoz/signoz/pkg/errors"
|
|
||||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||||
"github.com/SigNoz/signoz/pkg/http/render"
|
|
||||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
|
||||||
quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core"
|
|
||||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
|
||||||
baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces"
|
|
||||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||||
rules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
rules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||||
"github.com/SigNoz/signoz/pkg/signoz"
|
"github.com/SigNoz/signoz/pkg/signoz"
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/version"
|
"github.com/SigNoz/signoz/pkg/version"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type APIHandlerOptions struct {
|
type APIHandlerOptions struct {
|
||||||
DataConnector interfaces.DataConnector
|
DataConnector interfaces.DataConnector
|
||||||
PreferSpanMetrics bool
|
PreferSpanMetrics bool
|
||||||
AppDao dao.ModelDao
|
|
||||||
RulesManager *rules.Manager
|
RulesManager *rules.Manager
|
||||||
UsageManager *usage.Manager
|
UsageManager *usage.Manager
|
||||||
FeatureFlags baseint.FeatureLookup
|
|
||||||
LicenseManager *license.Manager
|
|
||||||
IntegrationsController *integrations.Controller
|
IntegrationsController *integrations.Controller
|
||||||
CloudIntegrationsController *cloudintegrations.Controller
|
CloudIntegrationsController *cloudintegrations.Controller
|
||||||
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
|
LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController
|
||||||
@ -61,22 +49,18 @@ type APIHandler struct {
|
|||||||
|
|
||||||
// NewAPIHandler returns an APIHandler
|
// NewAPIHandler returns an APIHandler
|
||||||
func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler, error) {
|
func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler, error) {
|
||||||
quickfiltermodule := quickfilterscore.NewQuickFilters(quickfilterscore.NewStore(signoz.SQLStore))
|
|
||||||
quickFilter := quickfilter.NewAPI(quickfiltermodule)
|
|
||||||
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
|
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
|
||||||
Reader: opts.DataConnector,
|
Reader: opts.DataConnector,
|
||||||
PreferSpanMetrics: opts.PreferSpanMetrics,
|
PreferSpanMetrics: opts.PreferSpanMetrics,
|
||||||
RuleManager: opts.RulesManager,
|
RuleManager: opts.RulesManager,
|
||||||
FeatureFlags: opts.FeatureFlags,
|
|
||||||
IntegrationsController: opts.IntegrationsController,
|
IntegrationsController: opts.IntegrationsController,
|
||||||
CloudIntegrationsController: opts.CloudIntegrationsController,
|
CloudIntegrationsController: opts.CloudIntegrationsController,
|
||||||
LogsParsingPipelineController: opts.LogsParsingPipelineController,
|
LogsParsingPipelineController: opts.LogsParsingPipelineController,
|
||||||
FluxInterval: opts.FluxInterval,
|
FluxInterval: opts.FluxInterval,
|
||||||
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
|
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
|
||||||
FieldsAPI: fields.NewAPI(signoz.TelemetryStore),
|
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
|
||||||
|
FieldsAPI: fields.NewAPI(signoz.TelemetryStore, signoz.Instrumentation.Logger()),
|
||||||
Signoz: signoz,
|
Signoz: signoz,
|
||||||
QuickFilters: quickFilter,
|
|
||||||
QuickFilterModule: quickfiltermodule,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -90,32 +74,20 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler,
|
|||||||
return ah, nil
|
return ah, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ah *APIHandler) FF() baseint.FeatureLookup {
|
|
||||||
return ah.opts.FeatureFlags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) RM() *rules.Manager {
|
func (ah *APIHandler) RM() *rules.Manager {
|
||||||
return ah.opts.RulesManager
|
return ah.opts.RulesManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ah *APIHandler) LM() *license.Manager {
|
|
||||||
return ah.opts.LicenseManager
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) UM() *usage.Manager {
|
func (ah *APIHandler) UM() *usage.Manager {
|
||||||
return ah.opts.UsageManager
|
return ah.opts.UsageManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ah *APIHandler) AppDao() dao.ModelDao {
|
|
||||||
return ah.opts.AppDao
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) Gateway() *httputil.ReverseProxy {
|
func (ah *APIHandler) Gateway() *httputil.ReverseProxy {
|
||||||
return ah.opts.Gateway
|
return ah.opts.Gateway
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ah *APIHandler) CheckFeature(f string) bool {
|
func (ah *APIHandler) CheckFeature(ctx context.Context, key string) bool {
|
||||||
err := ah.FF().CheckFeature(f)
|
err := ah.Signoz.Licensing.CheckFeature(ctx, key)
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,43 +98,29 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
|||||||
// routes available only in ee version
|
// routes available only in ee version
|
||||||
|
|
||||||
router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/api/v1/loginPrecheck", am.OpenAccess(ah.loginPrecheck)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v1/loginPrecheck", am.OpenAccess(ah.Signoz.Handlers.User.LoginPrecheck)).Methods(http.MethodGet)
|
||||||
|
|
||||||
// invite
|
// invite
|
||||||
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(ah.getInvite)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(ah.Signoz.Handlers.User.GetInvite)).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/api/v1/invite/accept", am.OpenAccess(ah.acceptInvite)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v1/invite/accept", am.OpenAccess(ah.Signoz.Handlers.User.AcceptInvite)).Methods(http.MethodPost)
|
||||||
|
|
||||||
// paid plans specific routes
|
// paid plans specific routes
|
||||||
router.HandleFunc("/api/v1/complete/saml", am.OpenAccess(ah.receiveSAML)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v1/complete/saml", am.OpenAccess(ah.receiveSAML)).Methods(http.MethodPost)
|
||||||
router.HandleFunc("/api/v1/complete/google", am.OpenAccess(ah.receiveGoogleAuth)).Methods(http.MethodGet)
|
|
||||||
router.HandleFunc("/api/v1/orgs/{orgId}/domains", am.AdminAccess(ah.listDomainsByOrg)).Methods(http.MethodGet)
|
|
||||||
|
|
||||||
router.HandleFunc("/api/v1/domains", am.AdminAccess(ah.postDomain)).Methods(http.MethodPost)
|
|
||||||
router.HandleFunc("/api/v1/domains/{id}", am.AdminAccess(ah.putDomain)).Methods(http.MethodPut)
|
|
||||||
router.HandleFunc("/api/v1/domains/{id}", am.AdminAccess(ah.deleteDomain)).Methods(http.MethodDelete)
|
|
||||||
|
|
||||||
// base overrides
|
// base overrides
|
||||||
router.HandleFunc("/api/v1/version", am.OpenAccess(ah.getVersion)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v1/version", am.OpenAccess(ah.getVersion)).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/api/v1/login", am.OpenAccess(ah.loginUser)).Methods(http.MethodPost)
|
|
||||||
|
|
||||||
// PAT APIs
|
router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.LicensingAPI.Checkout)).Methods(http.MethodPost)
|
||||||
router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.Signoz.Handlers.User.CreateAPIKey)).Methods(http.MethodPost)
|
|
||||||
router.HandleFunc("/api/v1/pats", am.AdminAccess(ah.Signoz.Handlers.User.ListAPIKeys)).Methods(http.MethodGet)
|
|
||||||
router.HandleFunc("/api/v1/pats/{id}", am.AdminAccess(ah.Signoz.Handlers.User.UpdateAPIKey)).Methods(http.MethodPut)
|
|
||||||
router.HandleFunc("/api/v1/pats/{id}", am.AdminAccess(ah.Signoz.Handlers.User.RevokeAPIKey)).Methods(http.MethodDelete)
|
|
||||||
|
|
||||||
router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.checkout)).Methods(http.MethodPost)
|
|
||||||
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.portalSession)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingAPI.Portal)).Methods(http.MethodPost)
|
||||||
|
|
||||||
router.HandleFunc("/api/v1/dashboards/{uuid}/lock", am.EditAccess(ah.lockDashboard)).Methods(http.MethodPut)
|
router.HandleFunc("/api/v1/dashboards/{uuid}/lock", am.EditAccess(ah.lockDashboard)).Methods(http.MethodPut)
|
||||||
router.HandleFunc("/api/v1/dashboards/{uuid}/unlock", am.EditAccess(ah.unlockDashboard)).Methods(http.MethodPut)
|
router.HandleFunc("/api/v1/dashboards/{uuid}/unlock", am.EditAccess(ah.unlockDashboard)).Methods(http.MethodPut)
|
||||||
|
|
||||||
// v3
|
// v3
|
||||||
router.HandleFunc("/api/v3/licenses", am.ViewAccess(ah.listLicensesV3)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Activate)).Methods(http.MethodPost)
|
||||||
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.applyLicenseV3)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Refresh)).Methods(http.MethodPut)
|
||||||
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.refreshLicensesV3)).Methods(http.MethodPut)
|
router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(ah.LicensingAPI.GetActive)).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(ah.getActiveLicenseV3)).Methods(http.MethodGet)
|
|
||||||
|
|
||||||
// v4
|
// v4
|
||||||
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)
|
||||||
@ -174,54 +132,6 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(nitya): remove this once we know how to get the FF's
|
|
||||||
func (ah *APIHandler) updateRequestContext(w http.ResponseWriter, r *http.Request) (*http.Request, error) {
|
|
||||||
ssoAvailable := true
|
|
||||||
err := ah.FF().CheckFeature(model.SSO)
|
|
||||||
if err != nil {
|
|
||||||
switch err.(type) {
|
|
||||||
case basemodel.ErrFeatureUnavailable:
|
|
||||||
// do nothing, just skip sso
|
|
||||||
ssoAvailable = false
|
|
||||||
default:
|
|
||||||
zap.L().Error("feature check failed", zap.String("featureKey", model.SSO), zap.Error(err))
|
|
||||||
return r, errors.New(errors.TypeInternal, errors.CodeInternal, "error checking SSO feature")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx := context.WithValue(r.Context(), types.SSOAvailable, ssoAvailable)
|
|
||||||
return r.WithContext(ctx), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) loginPrecheck(w http.ResponseWriter, r *http.Request) {
|
|
||||||
r, err := ah.updateRequestContext(w, r)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ah.Signoz.Handlers.User.LoginPrecheck(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) acceptInvite(w http.ResponseWriter, r *http.Request) {
|
|
||||||
r, err := ah.updateRequestContext(w, r)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ah.Signoz.Handlers.User.AcceptInvite(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) getInvite(w http.ResponseWriter, r *http.Request) {
|
|
||||||
r, err := ah.updateRequestContext(w, r)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ah.Signoz.Handlers.User.GetInvite(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) RegisterCloudIntegrationsRoutes(router *mux.Router, am *middleware.AuthZ) {
|
func (ah *APIHandler) RegisterCloudIntegrationsRoutes(router *mux.Router, am *middleware.AuthZ) {
|
||||||
|
|
||||||
ah.APIHandler.RegisterCloudIntegrationsRoutes(router, am)
|
ah.APIHandler.RegisterCloudIntegrationsRoutes(router, am)
|
||||||
|
|||||||
@ -3,41 +3,18 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
|
||||||
"github.com/SigNoz/signoz/pkg/http/render"
|
"github.com/SigNoz/signoz/pkg/http/render"
|
||||||
|
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseRequest(r *http.Request, req interface{}) error {
|
|
||||||
defer r.Body.Close()
|
|
||||||
requestBody, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(requestBody, &req)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// loginUser overrides base handler and considers SSO case.
|
|
||||||
func (ah *APIHandler) loginUser(w http.ResponseWriter, r *http.Request) {
|
|
||||||
r, err := ah.updateRequestContext(w, r)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ah.Signoz.Handlers.User.Login(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSsoError(w http.ResponseWriter, r *http.Request, redirectURL string) {
|
func handleSsoError(w http.ResponseWriter, r *http.Request, redirectURL string) {
|
||||||
ssoError := []byte("Login failed. Please contact your system administrator")
|
ssoError := []byte("Login failed. Please contact your system administrator")
|
||||||
dst := make([]byte, base64.StdEncoding.EncodedLen(len(ssoError)))
|
dst := make([]byte, base64.StdEncoding.EncodedLen(len(ssoError)))
|
||||||
@ -46,85 +23,31 @@ func handleSsoError(w http.ResponseWriter, r *http.Request, redirectURL string)
|
|||||||
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectURL, string(dst)), http.StatusSeeOther)
|
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectURL, string(dst)), http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
// receiveGoogleAuth completes google OAuth response and forwards a request
|
|
||||||
// to front-end to sign user in
|
|
||||||
func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request) {
|
|
||||||
redirectUri := constants.GetDefaultSiteURL()
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
if !ah.CheckFeature(model.SSO) {
|
|
||||||
zap.L().Error("[receiveGoogleAuth] sso requested but feature unavailable in org domain")
|
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
q := r.URL.Query()
|
|
||||||
if errType := q.Get("error"); errType != "" {
|
|
||||||
zap.L().Error("[receiveGoogleAuth] failed to login with google auth", zap.String("error", errType), zap.String("error_description", q.Get("error_description")))
|
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "failed to login through SSO "), http.StatusMovedPermanently)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
relayState := q.Get("state")
|
|
||||||
zap.L().Debug("[receiveGoogleAuth] relay state received", zap.String("state", relayState))
|
|
||||||
|
|
||||||
parsedState, err := url.Parse(relayState)
|
|
||||||
if err != nil || relayState == "" {
|
|
||||||
zap.L().Error("[receiveGoogleAuth] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
|
|
||||||
handleSsoError(w, r, redirectUri)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// upgrade redirect url from the relay state for better accuracy
|
|
||||||
redirectUri = fmt.Sprintf("%s://%s%s", parsedState.Scheme, parsedState.Host, "/login")
|
|
||||||
|
|
||||||
// fetch domain by parsing relay state.
|
|
||||||
domain, err := ah.AppDao().GetDomainFromSsoResponse(ctx, parsedState)
|
|
||||||
if err != nil {
|
|
||||||
handleSsoError(w, r, redirectUri)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// now that we have domain, use domain to fetch sso settings.
|
|
||||||
// prepare google callback handler using parsedState -
|
|
||||||
// which contains redirect URL (front-end endpoint)
|
|
||||||
callbackHandler, err := domain.PrepareGoogleOAuthProvider(parsedState)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("[receiveGoogleAuth] failed to prepare google oauth provider", zap.String("domain", domain.String()), zap.Error(err))
|
|
||||||
handleSsoError(w, r, redirectUri)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
identity, err := callbackHandler.HandleCallback(r)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("[receiveGoogleAuth] failed to process HandleCallback ", zap.String("domain", domain.String()), zap.Error(err))
|
|
||||||
handleSsoError(w, r, redirectUri)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nextPage, err := ah.Signoz.Modules.User.PrepareSsoRedirect(ctx, redirectUri, identity.Email, ah.opts.JWT)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("[receiveGoogleAuth] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
|
|
||||||
handleSsoError(w, r, redirectUri)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(w, r, nextPage, http.StatusSeeOther)
|
|
||||||
}
|
|
||||||
|
|
||||||
// receiveSAML completes a SAML request and gets user logged in
|
// receiveSAML completes a SAML request and gets user logged in
|
||||||
func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
|
func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
|
||||||
|
claims, err := authtypes.ClaimsFromContext(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// this is the source url that initiated the login request
|
// this is the source url that initiated the login request
|
||||||
redirectUri := constants.GetDefaultSiteURL()
|
redirectUri := constants.GetDefaultSiteURL()
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
if !ah.CheckFeature(model.SSO) {
|
_, err = ah.Signoz.Licensing.GetActive(ctx, orgID)
|
||||||
|
if err != nil {
|
||||||
zap.L().Error("[receiveSAML] sso requested but feature unavailable in org domain")
|
zap.L().Error("[receiveSAML] sso requested but feature unavailable in org domain")
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
|
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := r.ParseForm()
|
err = r.ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("[receiveSAML] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
|
zap.L().Error("[receiveSAML] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
|
||||||
handleSsoError(w, r, redirectUri)
|
handleSsoError(w, r, redirectUri)
|
||||||
@ -147,7 +70,7 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
|
|||||||
redirectUri = fmt.Sprintf("%s://%s%s", parsedState.Scheme, parsedState.Host, "/login")
|
redirectUri = fmt.Sprintf("%s://%s%s", parsedState.Scheme, parsedState.Host, "/login")
|
||||||
|
|
||||||
// fetch domain by parsing relay state.
|
// fetch domain by parsing relay state.
|
||||||
domain, err := ah.AppDao().GetDomainFromSsoResponse(ctx, parsedState)
|
domain, err := ah.Signoz.Modules.User.GetDomainFromSsoResponse(ctx, parsedState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleSsoError(w, r, redirectUri)
|
handleSsoError(w, r, redirectUri)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -36,6 +36,12 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cloudProvider := mux.Vars(r)["cloudProvider"]
|
cloudProvider := mux.Vars(r)["cloudProvider"]
|
||||||
if cloudProvider != "aws" {
|
if cloudProvider != "aws" {
|
||||||
RespondError(w, basemodel.BadRequest(fmt.Errorf(
|
RespondError(w, basemodel.BadRequest(fmt.Errorf(
|
||||||
@ -56,11 +62,9 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW
|
|||||||
SigNozAPIKey: apiKey,
|
SigNozAPIKey: apiKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
license, apiErr := ah.LM().GetRepo().GetActiveLicense(r.Context())
|
license, err := ah.Signoz.Licensing.GetActive(r.Context(), orgID)
|
||||||
if apiErr != nil {
|
if err != nil {
|
||||||
RespondError(w, basemodel.WrapApiError(
|
render.Error(w, err)
|
||||||
apiErr, "couldn't look for active license",
|
|
||||||
), nil)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (ah *APIHandler) listDomainsByOrg(w http.ResponseWriter, r *http.Request) {
|
|
||||||
orgId := mux.Vars(r)["orgId"]
|
|
||||||
domains, apierr := ah.AppDao().ListDomains(context.Background(), orgId)
|
|
||||||
if apierr != nil {
|
|
||||||
RespondError(w, apierr, domains)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ah.Respond(w, domains)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) postDomain(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
req := types.GettableOrgDomain{}
|
|
||||||
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
RespondError(w, model.BadRequest(err), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := req.ValidNew(); err != nil {
|
|
||||||
RespondError(w, model.BadRequest(err), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if apierr := ah.AppDao().CreateDomain(ctx, &req); apierr != nil {
|
|
||||||
RespondError(w, apierr, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ah.Respond(w, &req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) putDomain(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
domainIdStr := mux.Vars(r)["id"]
|
|
||||||
domainId, err := uuid.Parse(domainIdStr)
|
|
||||||
if err != nil {
|
|
||||||
RespondError(w, model.BadRequest(err), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req := types.GettableOrgDomain{StorableOrgDomain: types.StorableOrgDomain{ID: domainId}}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
||||||
RespondError(w, model.BadRequest(err), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
req.ID = domainId
|
|
||||||
if err := req.Valid(nil); err != nil {
|
|
||||||
RespondError(w, model.BadRequest(err), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
if apierr := ah.AppDao().UpdateDomain(ctx, &req); apierr != nil {
|
|
||||||
RespondError(w, apierr, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ah.Respond(w, &req)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) deleteDomain(w http.ResponseWriter, r *http.Request) {
|
|
||||||
domainIdStr := mux.Vars(r)["id"]
|
|
||||||
|
|
||||||
domainId, err := uuid.Parse(domainIdStr)
|
|
||||||
if err != nil {
|
|
||||||
RespondError(w, model.BadRequest(fmt.Errorf("invalid domain id")), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
apierr := ah.AppDao().DeleteDomain(context.Background(), domainId)
|
|
||||||
if apierr != nil {
|
|
||||||
RespondError(w, apierr, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ah.Respond(w, nil)
|
|
||||||
}
|
|
||||||
@ -9,13 +9,29 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
pkgError "github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/http/render"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
featureSet, err := ah.FF().GetFeatureFlags()
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(w, pkgError.Newf(pkgError.TypeInvalidInput, pkgError.CodeInvalidInput, "orgId is invalid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
featureSet, err := ah.Signoz.Licensing.GetFeatureFlags(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ah.HandleError(w, err, http.StatusInternalServerError)
|
ah.HandleError(w, err, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@ -23,7 +39,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if constants.FetchFeatures == "true" {
|
if constants.FetchFeatures == "true" {
|
||||||
zap.L().Debug("fetching license")
|
zap.L().Debug("fetching license")
|
||||||
license, err := ah.LM().GetRepo().GetActiveLicense(ctx)
|
license, err := ah.Signoz.Licensing.GetActive(ctx, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("failed to fetch license", zap.Error(err))
|
zap.L().Error("failed to fetch license", zap.Error(err))
|
||||||
} else if license == nil {
|
} else if license == nil {
|
||||||
@ -44,9 +60,8 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ah.opts.PreferSpanMetrics {
|
if ah.opts.PreferSpanMetrics {
|
||||||
for idx := range featureSet {
|
for idx, feature := range featureSet {
|
||||||
feature := &featureSet[idx]
|
if feature.Name == featuretypes.UseSpanMetrics {
|
||||||
if feature.Name == basemodel.UseSpanMetrics {
|
|
||||||
featureSet[idx].Active = true
|
featureSet[idx].Active = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,7 +72,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// fetchZeusFeatures makes an HTTP GET request to the /zeusFeatures endpoint
|
// fetchZeusFeatures makes an HTTP GET request to the /zeusFeatures endpoint
|
||||||
// and returns the FeatureSet.
|
// and returns the FeatureSet.
|
||||||
func fetchZeusFeatures(url, licenseKey string) (basemodel.FeatureSet, error) {
|
func fetchZeusFeatures(url, licenseKey string) ([]*featuretypes.GettableFeature, error) {
|
||||||
// Check if the URL is empty
|
// Check if the URL is empty
|
||||||
if url == "" {
|
if url == "" {
|
||||||
return nil, fmt.Errorf("url is empty")
|
return nil, fmt.Errorf("url is empty")
|
||||||
@ -117,13 +132,13 @@ func fetchZeusFeatures(url, licenseKey string) (basemodel.FeatureSet, error) {
|
|||||||
|
|
||||||
type ZeusFeaturesResponse struct {
|
type ZeusFeaturesResponse struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Data basemodel.FeatureSet `json:"data"`
|
Data []*featuretypes.GettableFeature `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MergeFeatureSets merges two FeatureSet arrays with precedence to zeusFeatures.
|
// MergeFeatureSets merges two FeatureSet arrays with precedence to zeusFeatures.
|
||||||
func MergeFeatureSets(zeusFeatures, internalFeatures basemodel.FeatureSet) basemodel.FeatureSet {
|
func MergeFeatureSets(zeusFeatures, internalFeatures []*featuretypes.GettableFeature) []*featuretypes.GettableFeature {
|
||||||
// Create a map to store the merged features
|
// Create a map to store the merged features
|
||||||
featureMap := make(map[string]basemodel.Feature)
|
featureMap := make(map[string]*featuretypes.GettableFeature)
|
||||||
|
|
||||||
// Add all features from the otherFeatures set to the map
|
// Add all features from the otherFeatures set to the map
|
||||||
for _, feature := range internalFeatures {
|
for _, feature := range internalFeatures {
|
||||||
@ -137,7 +152,7 @@ func MergeFeatureSets(zeusFeatures, internalFeatures basemodel.FeatureSet) basem
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert the map back to a FeatureSet slice
|
// Convert the map back to a FeatureSet slice
|
||||||
var mergedFeatures basemodel.FeatureSet
|
var mergedFeatures []*featuretypes.GettableFeature
|
||||||
for _, feature := range featureMap {
|
for _, feature := range featureMap {
|
||||||
mergedFeatures = append(mergedFeatures, feature)
|
mergedFeatures = append(mergedFeatures, feature)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,58 +3,58 @@ package api
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
"github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMergeFeatureSets(t *testing.T) {
|
func TestMergeFeatureSets(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
zeusFeatures basemodel.FeatureSet
|
zeusFeatures []*featuretypes.GettableFeature
|
||||||
internalFeatures basemodel.FeatureSet
|
internalFeatures []*featuretypes.GettableFeature
|
||||||
expected basemodel.FeatureSet
|
expected []*featuretypes.GettableFeature
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty zeusFeatures and internalFeatures",
|
name: "empty zeusFeatures and internalFeatures",
|
||||||
zeusFeatures: basemodel.FeatureSet{},
|
zeusFeatures: []*featuretypes.GettableFeature{},
|
||||||
internalFeatures: basemodel.FeatureSet{},
|
internalFeatures: []*featuretypes.GettableFeature{},
|
||||||
expected: basemodel.FeatureSet{},
|
expected: []*featuretypes.GettableFeature{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "non-empty zeusFeatures and empty internalFeatures",
|
name: "non-empty zeusFeatures and empty internalFeatures",
|
||||||
zeusFeatures: basemodel.FeatureSet{
|
zeusFeatures: []*featuretypes.GettableFeature{
|
||||||
{Name: "Feature1", Active: true},
|
{Name: "Feature1", Active: true},
|
||||||
{Name: "Feature2", Active: false},
|
{Name: "Feature2", Active: false},
|
||||||
},
|
},
|
||||||
internalFeatures: basemodel.FeatureSet{},
|
internalFeatures: []*featuretypes.GettableFeature{},
|
||||||
expected: basemodel.FeatureSet{
|
expected: []*featuretypes.GettableFeature{
|
||||||
{Name: "Feature1", Active: true},
|
{Name: "Feature1", Active: true},
|
||||||
{Name: "Feature2", Active: false},
|
{Name: "Feature2", Active: false},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "empty zeusFeatures and non-empty internalFeatures",
|
name: "empty zeusFeatures and non-empty internalFeatures",
|
||||||
zeusFeatures: basemodel.FeatureSet{},
|
zeusFeatures: []*featuretypes.GettableFeature{},
|
||||||
internalFeatures: basemodel.FeatureSet{
|
internalFeatures: []*featuretypes.GettableFeature{
|
||||||
{Name: "Feature1", Active: true},
|
{Name: "Feature1", Active: true},
|
||||||
{Name: "Feature2", Active: false},
|
{Name: "Feature2", Active: false},
|
||||||
},
|
},
|
||||||
expected: basemodel.FeatureSet{
|
expected: []*featuretypes.GettableFeature{
|
||||||
{Name: "Feature1", Active: true},
|
{Name: "Feature1", Active: true},
|
||||||
{Name: "Feature2", Active: false},
|
{Name: "Feature2", Active: false},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "non-empty zeusFeatures and non-empty internalFeatures with no conflicts",
|
name: "non-empty zeusFeatures and non-empty internalFeatures with no conflicts",
|
||||||
zeusFeatures: basemodel.FeatureSet{
|
zeusFeatures: []*featuretypes.GettableFeature{
|
||||||
{Name: "Feature1", Active: true},
|
{Name: "Feature1", Active: true},
|
||||||
{Name: "Feature3", Active: false},
|
{Name: "Feature3", Active: false},
|
||||||
},
|
},
|
||||||
internalFeatures: basemodel.FeatureSet{
|
internalFeatures: []*featuretypes.GettableFeature{
|
||||||
{Name: "Feature2", Active: true},
|
{Name: "Feature2", Active: true},
|
||||||
{Name: "Feature4", Active: false},
|
{Name: "Feature4", Active: false},
|
||||||
},
|
},
|
||||||
expected: basemodel.FeatureSet{
|
expected: []*featuretypes.GettableFeature{
|
||||||
{Name: "Feature1", Active: true},
|
{Name: "Feature1", Active: true},
|
||||||
{Name: "Feature2", Active: true},
|
{Name: "Feature2", Active: true},
|
||||||
{Name: "Feature3", Active: false},
|
{Name: "Feature3", Active: false},
|
||||||
@ -63,15 +63,15 @@ func TestMergeFeatureSets(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "non-empty zeusFeatures and non-empty internalFeatures with conflicts",
|
name: "non-empty zeusFeatures and non-empty internalFeatures with conflicts",
|
||||||
zeusFeatures: basemodel.FeatureSet{
|
zeusFeatures: []*featuretypes.GettableFeature{
|
||||||
{Name: "Feature1", Active: true},
|
{Name: "Feature1", Active: true},
|
||||||
{Name: "Feature2", Active: false},
|
{Name: "Feature2", Active: false},
|
||||||
},
|
},
|
||||||
internalFeatures: basemodel.FeatureSet{
|
internalFeatures: []*featuretypes.GettableFeature{
|
||||||
{Name: "Feature1", Active: false},
|
{Name: "Feature1", Active: false},
|
||||||
{Name: "Feature3", Active: true},
|
{Name: "Feature3", Active: true},
|
||||||
},
|
},
|
||||||
expected: basemodel.FeatureSet{
|
expected: []*featuretypes.GettableFeature{
|
||||||
{Name: "Feature1", Active: true},
|
{Name: "Feature1", Active: true},
|
||||||
{Name: "Feature2", Active: false},
|
{Name: "Feature2", Active: false},
|
||||||
{Name: "Feature3", Active: true},
|
{Name: "Feature3", Active: true},
|
||||||
|
|||||||
@ -5,10 +5,26 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
|
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/http/render"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ah *APIHandler) ServeGatewayHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (ah *APIHandler) ServeGatewayHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
ctx := req.Context()
|
ctx := req.Context()
|
||||||
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||||
|
if err != nil {
|
||||||
|
render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
validPath := false
|
validPath := false
|
||||||
for _, allowedPrefix := range gateway.AllowedPrefix {
|
for _, allowedPrefix := range gateway.AllowedPrefix {
|
||||||
if strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+allowedPrefix) {
|
if strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+allowedPrefix) {
|
||||||
@ -22,9 +38,9 @@ func (ah *APIHandler) ServeGatewayHTTP(rw http.ResponseWriter, req *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
license, err := ah.LM().GetRepo().GetActiveLicense(ctx)
|
license, err := ah.Signoz.Licensing.GetActive(ctx, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
RespondError(rw, err, nil)
|
render.Error(rw, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,11 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||||
"github.com/SigNoz/signoz/ee/query-service/integrations/signozio"
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
"github.com/SigNoz/signoz/ee/query-service/model"
|
||||||
"github.com/SigNoz/signoz/pkg/http/render"
|
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DayWiseBreakdown struct {
|
type DayWiseBreakdown struct {
|
||||||
@ -49,10 +45,6 @@ type details struct {
|
|||||||
BillTotal float64 `json:"billTotal"`
|
BillTotal float64 `json:"billTotal"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Redirect struct {
|
|
||||||
RedirectURL string `json:"redirectURL"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type billingDetails struct {
|
type billingDetails struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Data struct {
|
Data struct {
|
||||||
@ -64,97 +56,6 @@ type billingDetails struct {
|
|||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApplyLicenseRequest struct {
|
|
||||||
LicenseKey string `json:"key"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) listLicensesV3(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ah.listLicensesV2(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) getActiveLicenseV3(w http.ResponseWriter, r *http.Request) {
|
|
||||||
activeLicense, err := ah.LM().GetRepo().GetActiveLicenseV3(r.Context())
|
|
||||||
if err != nil {
|
|
||||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// return 404 not found if there is no active license
|
|
||||||
if activeLicense == nil {
|
|
||||||
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no active license found")}, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO deprecate this when we move away from key for stripe
|
|
||||||
activeLicense.Data["key"] = activeLicense.Key
|
|
||||||
render.Success(w, http.StatusOK, activeLicense.Data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// this function is called by zeus when inserting licenses in the query-service
|
|
||||||
func (ah *APIHandler) applyLicenseV3(w http.ResponseWriter, r *http.Request) {
|
|
||||||
claims, err := authtypes.ClaimsFromContext(r.Context())
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var licenseKey ApplyLicenseRequest
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&licenseKey); err != nil {
|
|
||||||
RespondError(w, model.BadRequest(err), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if licenseKey.LicenseKey == "" {
|
|
||||||
RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = ah.LM().ActivateV3(r.Context(), licenseKey.LicenseKey)
|
|
||||||
if err != nil {
|
|
||||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED, map[string]interface{}{"err": err.Error()}, claims.Email, true, false)
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
render.Success(w, http.StatusAccepted, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) refreshLicensesV3(w http.ResponseWriter, r *http.Request) {
|
|
||||||
err := ah.LM().RefreshLicense(r.Context())
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
render.Success(w, http.StatusNoContent, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCheckoutPortalResponse(redirectURL string) *Redirect {
|
|
||||||
return &Redirect{RedirectURL: redirectURL}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) checkout(w http.ResponseWriter, r *http.Request) {
|
|
||||||
checkoutRequest := &model.CheckoutRequest{}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(checkoutRequest); err != nil {
|
|
||||||
RespondError(w, model.BadRequest(err), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
license := ah.LM().GetActiveLicense()
|
|
||||||
if license == nil {
|
|
||||||
RespondError(w, model.BadRequestStr("cannot proceed with checkout without license key"), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectUrl, err := signozio.CheckoutSession(r.Context(), checkoutRequest, license.Key, ah.Signoz.Zeus)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ah.Respond(w, getCheckoutPortalResponse(redirectUrl))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
|
func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
|
||||||
licenseKey := r.URL.Query().Get("licenseKey")
|
licenseKey := r.URL.Query().Get("licenseKey")
|
||||||
|
|
||||||
@ -188,71 +89,3 @@ func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
|
|||||||
// TODO(srikanthccv):Fetch the current day usage and add it to the response
|
// TODO(srikanthccv):Fetch the current day usage and add it to the response
|
||||||
ah.Respond(w, billingResponse.Data)
|
ah.Respond(w, billingResponse.Data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertLicenseV3ToLicenseV2(licenses []*model.LicenseV3) []model.License {
|
|
||||||
licensesV2 := []model.License{}
|
|
||||||
for _, l := range licenses {
|
|
||||||
planKeyFromPlanName, ok := model.MapOldPlanKeyToNewPlanName[l.PlanName]
|
|
||||||
if !ok {
|
|
||||||
planKeyFromPlanName = model.Basic
|
|
||||||
}
|
|
||||||
licenseV2 := model.License{
|
|
||||||
Key: l.Key,
|
|
||||||
ActivationId: "",
|
|
||||||
PlanDetails: "",
|
|
||||||
FeatureSet: l.Features,
|
|
||||||
ValidationMessage: "",
|
|
||||||
IsCurrent: l.IsCurrent,
|
|
||||||
LicensePlan: model.LicensePlan{
|
|
||||||
PlanKey: planKeyFromPlanName,
|
|
||||||
ValidFrom: l.ValidFrom,
|
|
||||||
ValidUntil: l.ValidUntil,
|
|
||||||
Status: l.Status},
|
|
||||||
}
|
|
||||||
licensesV2 = append(licensesV2, licenseV2)
|
|
||||||
}
|
|
||||||
return licensesV2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
|
|
||||||
licensesV3, apierr := ah.LM().GetLicensesV3(r.Context())
|
|
||||||
if apierr != nil {
|
|
||||||
RespondError(w, apierr, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
licenses := convertLicenseV3ToLicenseV2(licensesV3)
|
|
||||||
|
|
||||||
resp := model.Licenses{
|
|
||||||
TrialStart: -1,
|
|
||||||
TrialEnd: -1,
|
|
||||||
OnTrial: false,
|
|
||||||
WorkSpaceBlock: false,
|
|
||||||
TrialConvertedToSubscription: false,
|
|
||||||
GracePeriodEnd: -1,
|
|
||||||
Licenses: licenses,
|
|
||||||
}
|
|
||||||
|
|
||||||
ah.Respond(w, resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ah *APIHandler) portalSession(w http.ResponseWriter, r *http.Request) {
|
|
||||||
portalRequest := &model.PortalRequest{}
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(portalRequest); err != nil {
|
|
||||||
RespondError(w, model.BadRequest(err), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
license := ah.LM().GetActiveLicense()
|
|
||||||
if license == nil {
|
|
||||||
RespondError(w, model.BadRequestStr("cannot request the portal session without license key"), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
redirectUrl, err := signozio.PortalSession(r.Context(), portalRequest, license.Key, ah.Signoz.Zeus)
|
|
||||||
if err != nil {
|
|
||||||
render.Error(w, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ah.Respond(w, getCheckoutPortalResponse(redirectUrl))
|
|
||||||
}
|
|
||||||
|
|||||||
@ -11,13 +11,12 @@ import (
|
|||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
|
||||||
eemiddleware "github.com/SigNoz/signoz/ee/http/middleware"
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/app/api"
|
"github.com/SigNoz/signoz/ee/query-service/app/api"
|
||||||
"github.com/SigNoz/signoz/ee/query-service/app/db"
|
"github.com/SigNoz/signoz/ee/query-service/app/db"
|
||||||
"github.com/SigNoz/signoz/ee/query-service/constants"
|
"github.com/SigNoz/signoz/ee/query-service/constants"
|
||||||
"github.com/SigNoz/signoz/ee/query-service/dao/sqlite"
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
|
"github.com/SigNoz/signoz/ee/query-service/integrations/gateway"
|
||||||
"github.com/SigNoz/signoz/ee/query-service/rules"
|
"github.com/SigNoz/signoz/ee/query-service/rules"
|
||||||
|
"github.com/SigNoz/signoz/ee/query-service/usage"
|
||||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||||
"github.com/SigNoz/signoz/pkg/cache"
|
"github.com/SigNoz/signoz/pkg/cache"
|
||||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||||
@ -30,9 +29,6 @@ import (
|
|||||||
"github.com/rs/cors"
|
"github.com/rs/cors"
|
||||||
"github.com/soheilhy/cmux"
|
"github.com/soheilhy/cmux"
|
||||||
|
|
||||||
licensepkg "github.com/SigNoz/signoz/ee/query-service/license"
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/usage"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||||
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
|
||||||
@ -90,18 +86,11 @@ func (s Server) HealthCheckStatus() chan healthcheck.Status {
|
|||||||
|
|
||||||
// NewServer creates and initializes Server
|
// NewServer creates and initializes Server
|
||||||
func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||||
modelDao := sqlite.NewModelDao(serverOptions.SigNoz.SQLStore)
|
|
||||||
gatewayProxy, err := gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
|
gatewayProxy, err := gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// initiate license manager
|
|
||||||
lm, err := licensepkg.StartManager(serverOptions.SigNoz.SQLStore.SQLxDB(), serverOptions.SigNoz.SQLStore, serverOptions.SigNoz.Zeus)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
fluxIntervalForTraceDetail, err := time.ParseDuration(serverOptions.FluxIntervalForTraceDetail)
|
fluxIntervalForTraceDetail, err := time.ParseDuration(serverOptions.FluxIntervalForTraceDetail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -168,11 +157,11 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// start the usagemanager
|
// start the usagemanager
|
||||||
usageManager, err := usage.New(modelDao, lm.GetRepo(), serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.SigNoz.Zeus)
|
usageManager, err := usage.New(serverOptions.SigNoz.Licensing, serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.SigNoz.Zeus, serverOptions.SigNoz.Modules.Organization)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = usageManager.Start()
|
err = usageManager.Start(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -194,11 +183,8 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
apiOpts := api.APIHandlerOptions{
|
apiOpts := api.APIHandlerOptions{
|
||||||
DataConnector: reader,
|
DataConnector: reader,
|
||||||
PreferSpanMetrics: serverOptions.PreferSpanMetrics,
|
PreferSpanMetrics: serverOptions.PreferSpanMetrics,
|
||||||
AppDao: modelDao,
|
|
||||||
RulesManager: rm,
|
RulesManager: rm,
|
||||||
UsageManager: usageManager,
|
UsageManager: usageManager,
|
||||||
FeatureFlags: lm,
|
|
||||||
LicenseManager: lm,
|
|
||||||
IntegrationsController: integrationsController,
|
IntegrationsController: integrationsController,
|
||||||
CloudIntegrationsController: cloudIntegrationsController,
|
CloudIntegrationsController: cloudIntegrationsController,
|
||||||
LogsParsingPipelineController: logParsingPipelineController,
|
LogsParsingPipelineController: logParsingPipelineController,
|
||||||
@ -257,15 +243,15 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server,
|
|||||||
|
|
||||||
r := baseapp.NewRouter()
|
r := baseapp.NewRouter()
|
||||||
|
|
||||||
r.Use(middleware.NewAuth(zap.L(), s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
|
r.Use(middleware.NewAuth(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
|
||||||
r.Use(eemiddleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}).Wrap)
|
r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap)
|
||||||
r.Use(middleware.NewTimeout(zap.L(),
|
r.Use(middleware.NewTimeout(s.serverOptions.SigNoz.Instrumentation.Logger(),
|
||||||
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
|
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
|
||||||
s.serverOptions.Config.APIServer.Timeout.Default,
|
s.serverOptions.Config.APIServer.Timeout.Default,
|
||||||
s.serverOptions.Config.APIServer.Timeout.Max,
|
s.serverOptions.Config.APIServer.Timeout.Max,
|
||||||
).Wrap)
|
).Wrap)
|
||||||
r.Use(middleware.NewAnalytics(zap.L()).Wrap)
|
r.Use(middleware.NewAnalytics().Wrap)
|
||||||
r.Use(middleware.NewLogging(zap.L(), s.serverOptions.Config.APIServer.Logging.ExcludedRoutes).Wrap)
|
r.Use(middleware.NewLogging(s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.Config.APIServer.Logging.ExcludedRoutes).Wrap)
|
||||||
|
|
||||||
apiHandler.RegisterPrivateRoutes(r)
|
apiHandler.RegisterPrivateRoutes(r)
|
||||||
|
|
||||||
@ -289,15 +275,15 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
|||||||
r := baseapp.NewRouter()
|
r := baseapp.NewRouter()
|
||||||
am := middleware.NewAuthZ(s.serverOptions.SigNoz.Instrumentation.Logger())
|
am := middleware.NewAuthZ(s.serverOptions.SigNoz.Instrumentation.Logger())
|
||||||
|
|
||||||
r.Use(middleware.NewAuth(zap.L(), s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
|
r.Use(middleware.NewAuth(s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap)
|
||||||
r.Use(eemiddleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}).Wrap)
|
r.Use(middleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.serverOptions.SigNoz.Instrumentation.Logger()).Wrap)
|
||||||
r.Use(middleware.NewTimeout(zap.L(),
|
r.Use(middleware.NewTimeout(s.serverOptions.SigNoz.Instrumentation.Logger(),
|
||||||
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
|
s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes,
|
||||||
s.serverOptions.Config.APIServer.Timeout.Default,
|
s.serverOptions.Config.APIServer.Timeout.Default,
|
||||||
s.serverOptions.Config.APIServer.Timeout.Max,
|
s.serverOptions.Config.APIServer.Timeout.Max,
|
||||||
).Wrap)
|
).Wrap)
|
||||||
r.Use(middleware.NewAnalytics(zap.L()).Wrap)
|
r.Use(middleware.NewAnalytics().Wrap)
|
||||||
r.Use(middleware.NewLogging(zap.L(), s.serverOptions.Config.APIServer.Logging.ExcludedRoutes).Wrap)
|
r.Use(middleware.NewLogging(s.serverOptions.SigNoz.Instrumentation.Logger(), s.serverOptions.Config.APIServer.Logging.ExcludedRoutes).Wrap)
|
||||||
|
|
||||||
apiHandler.RegisterRoutes(r, am)
|
apiHandler.RegisterRoutes(r, am)
|
||||||
apiHandler.RegisterLogsRoutes(r, am)
|
apiHandler.RegisterLogsRoutes(r, am)
|
||||||
@ -431,15 +417,15 @@ func (s *Server) Start(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Stop() error {
|
func (s *Server) Stop(ctx context.Context) error {
|
||||||
if s.httpServer != nil {
|
if s.httpServer != nil {
|
||||||
if err := s.httpServer.Shutdown(context.Background()); err != nil {
|
if err := s.httpServer.Shutdown(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.privateHTTP != nil {
|
if s.privateHTTP != nil {
|
||||||
if err := s.privateHTTP.Shutdown(context.Background()); err != nil {
|
if err := s.privateHTTP.Shutdown(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -447,11 +433,11 @@ func (s *Server) Stop() error {
|
|||||||
s.opampServer.Stop()
|
s.opampServer.Stop()
|
||||||
|
|
||||||
if s.ruleManager != nil {
|
if s.ruleManager != nil {
|
||||||
s.ruleManager.Stop(context.Background())
|
s.ruleManager.Stop(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop usage manager
|
// stop usage manager
|
||||||
s.usageManager.Stop()
|
s.usageManager.Stop(ctx)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
DefaultSiteURL = "https://localhost:8080"
|
|
||||||
)
|
|
||||||
|
|
||||||
var LicenseSignozIo = "https://license.signoz.io/api/v1"
|
var LicenseSignozIo = "https://license.signoz.io/api/v1"
|
||||||
var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "")
|
var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "")
|
||||||
var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
|
var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
|
||||||
@ -24,12 +20,3 @@ func GetOrDefaultEnv(key string, fallback string) string {
|
|||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
// constant functions that override env vars
|
|
||||||
|
|
||||||
// GetDefaultSiteURL returns default site url, primarily
|
|
||||||
// used to send saml request and allowing backend to
|
|
||||||
// handle http redirect
|
|
||||||
func GetDefaultSiteURL() string {
|
|
||||||
return GetOrDefaultEnv("SIGNOZ_SITE_URL", DefaultSiteURL)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
package dao
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ModelDao interface {
|
|
||||||
// auth methods
|
|
||||||
GetDomainFromSsoResponse(ctx context.Context, relayState *url.URL) (*types.GettableOrgDomain, error)
|
|
||||||
|
|
||||||
// org domain (auth domains) CRUD ops
|
|
||||||
ListDomains(ctx context.Context, orgId string) ([]types.GettableOrgDomain, basemodel.BaseApiError)
|
|
||||||
GetDomain(ctx context.Context, id uuid.UUID) (*types.GettableOrgDomain, basemodel.BaseApiError)
|
|
||||||
CreateDomain(ctx context.Context, d *types.GettableOrgDomain) basemodel.BaseApiError
|
|
||||||
UpdateDomain(ctx context.Context, domain *types.GettableOrgDomain) basemodel.BaseApiError
|
|
||||||
DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.BaseApiError
|
|
||||||
GetDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, basemodel.BaseApiError)
|
|
||||||
}
|
|
||||||
@ -1,271 +0,0 @@
|
|||||||
package sqlite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
|
||||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetDomainFromSsoResponse uses relay state received from IdP to fetch
|
|
||||||
// user domain. The domain is further used to process validity of the response.
|
|
||||||
// when sending login request to IdP we send relay state as URL (site url)
|
|
||||||
// with domainId or domainName as query parameter.
|
|
||||||
func (m *modelDao) GetDomainFromSsoResponse(ctx context.Context, relayState *url.URL) (*types.GettableOrgDomain, error) {
|
|
||||||
// derive domain id from relay state now
|
|
||||||
var domainIdStr string
|
|
||||||
var domainNameStr string
|
|
||||||
var domain *types.GettableOrgDomain
|
|
||||||
|
|
||||||
for k, v := range relayState.Query() {
|
|
||||||
if k == "domainId" && len(v) > 0 {
|
|
||||||
domainIdStr = strings.Replace(v[0], ":", "-", -1)
|
|
||||||
}
|
|
||||||
if k == "domainName" && len(v) > 0 {
|
|
||||||
domainNameStr = v[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if domainIdStr != "" {
|
|
||||||
domainId, err := uuid.Parse(domainIdStr)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("failed to parse domainId from relay state", zap.Error(err))
|
|
||||||
return nil, fmt.Errorf("failed to parse domainId from IdP response")
|
|
||||||
}
|
|
||||||
|
|
||||||
domain, err = m.GetDomain(ctx, domainId)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("failed to find domain from domainId received in IdP response", zap.Error(err))
|
|
||||||
return nil, fmt.Errorf("invalid credentials")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if domainNameStr != "" {
|
|
||||||
|
|
||||||
domainFromDB, err := m.GetDomainByName(ctx, domainNameStr)
|
|
||||||
domain = domainFromDB
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("failed to find domain from domainName received in IdP response", zap.Error(err))
|
|
||||||
return nil, fmt.Errorf("invalid credentials")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if domain != nil {
|
|
||||||
return domain, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("failed to find domain received in IdP response")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDomainByName returns org domain for a given domain name
|
|
||||||
func (m *modelDao) GetDomainByName(ctx context.Context, name string) (*types.GettableOrgDomain, basemodel.BaseApiError) {
|
|
||||||
|
|
||||||
stored := types.StorableOrgDomain{}
|
|
||||||
err := m.sqlStore.BunDB().NewSelect().
|
|
||||||
Model(&stored).
|
|
||||||
Where("name = ?", name).
|
|
||||||
Limit(1).
|
|
||||||
Scan(ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return nil, model.BadRequest(fmt.Errorf("invalid domain name"))
|
|
||||||
}
|
|
||||||
return nil, model.InternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
domain := &types.GettableOrgDomain{StorableOrgDomain: stored}
|
|
||||||
if err := domain.LoadConfig(stored.Data); err != nil {
|
|
||||||
return nil, model.InternalError(err)
|
|
||||||
}
|
|
||||||
return domain, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDomain returns org domain for a given domain id
|
|
||||||
func (m *modelDao) GetDomain(ctx context.Context, id uuid.UUID) (*types.GettableOrgDomain, basemodel.BaseApiError) {
|
|
||||||
|
|
||||||
stored := types.StorableOrgDomain{}
|
|
||||||
err := m.sqlStore.BunDB().NewSelect().
|
|
||||||
Model(&stored).
|
|
||||||
Where("id = ?", id).
|
|
||||||
Limit(1).
|
|
||||||
Scan(ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return nil, model.BadRequest(fmt.Errorf("invalid domain id"))
|
|
||||||
}
|
|
||||||
return nil, model.InternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
domain := &types.GettableOrgDomain{StorableOrgDomain: stored}
|
|
||||||
if err := domain.LoadConfig(stored.Data); err != nil {
|
|
||||||
return nil, model.InternalError(err)
|
|
||||||
}
|
|
||||||
return domain, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListDomains gets the list of auth domains by org id
|
|
||||||
func (m *modelDao) ListDomains(ctx context.Context, orgId string) ([]types.GettableOrgDomain, basemodel.BaseApiError) {
|
|
||||||
domains := []types.GettableOrgDomain{}
|
|
||||||
|
|
||||||
stored := []types.StorableOrgDomain{}
|
|
||||||
err := m.sqlStore.BunDB().NewSelect().
|
|
||||||
Model(&stored).
|
|
||||||
Where("org_id = ?", orgId).
|
|
||||||
Scan(ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return domains, nil
|
|
||||||
}
|
|
||||||
return nil, model.InternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range stored {
|
|
||||||
domain := types.GettableOrgDomain{StorableOrgDomain: s}
|
|
||||||
if err := domain.LoadConfig(s.Data); err != nil {
|
|
||||||
zap.L().Error("ListDomains() failed", zap.Error(err))
|
|
||||||
}
|
|
||||||
domains = append(domains, domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
return domains, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateDomain creates a new auth domain
|
|
||||||
func (m *modelDao) CreateDomain(ctx context.Context, domain *types.GettableOrgDomain) basemodel.BaseApiError {
|
|
||||||
|
|
||||||
if domain.ID == uuid.Nil {
|
|
||||||
domain.ID = uuid.New()
|
|
||||||
}
|
|
||||||
|
|
||||||
if domain.OrgID == "" || domain.Name == "" {
|
|
||||||
return model.BadRequest(fmt.Errorf("domain creation failed, missing fields: OrgID, Name "))
|
|
||||||
}
|
|
||||||
|
|
||||||
configJson, err := json.Marshal(domain)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("failed to unmarshal domain config", zap.Error(err))
|
|
||||||
return model.InternalError(fmt.Errorf("domain creation failed"))
|
|
||||||
}
|
|
||||||
|
|
||||||
storableDomain := types.StorableOrgDomain{
|
|
||||||
ID: domain.ID,
|
|
||||||
Name: domain.Name,
|
|
||||||
OrgID: domain.OrgID,
|
|
||||||
Data: string(configJson),
|
|
||||||
TimeAuditable: types.TimeAuditable{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = m.sqlStore.BunDB().NewInsert().
|
|
||||||
Model(&storableDomain).
|
|
||||||
Exec(ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("failed to insert domain in db", zap.Error(err))
|
|
||||||
return model.InternalError(fmt.Errorf("domain creation failed"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateDomain updates stored config params for a domain
|
|
||||||
func (m *modelDao) UpdateDomain(ctx context.Context, domain *types.GettableOrgDomain) basemodel.BaseApiError {
|
|
||||||
|
|
||||||
if domain.ID == uuid.Nil {
|
|
||||||
zap.L().Error("domain update failed", zap.Error(fmt.Errorf("OrgDomain.Id is null")))
|
|
||||||
return model.InternalError(fmt.Errorf("domain update failed"))
|
|
||||||
}
|
|
||||||
|
|
||||||
configJson, err := json.Marshal(domain)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("domain update failed", zap.Error(err))
|
|
||||||
return model.InternalError(fmt.Errorf("domain update failed"))
|
|
||||||
}
|
|
||||||
|
|
||||||
storableDomain := &types.StorableOrgDomain{
|
|
||||||
ID: domain.ID,
|
|
||||||
Name: domain.Name,
|
|
||||||
OrgID: domain.OrgID,
|
|
||||||
Data: string(configJson),
|
|
||||||
TimeAuditable: types.TimeAuditable{UpdatedAt: time.Now()},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = m.sqlStore.BunDB().NewUpdate().
|
|
||||||
Model(storableDomain).
|
|
||||||
Column("data", "updated_at").
|
|
||||||
WherePK().
|
|
||||||
Exec(ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("domain update failed", zap.Error(err))
|
|
||||||
return model.InternalError(fmt.Errorf("domain update failed"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteDomain deletes an org domain
|
|
||||||
func (m *modelDao) DeleteDomain(ctx context.Context, id uuid.UUID) basemodel.BaseApiError {
|
|
||||||
|
|
||||||
if id == uuid.Nil {
|
|
||||||
zap.L().Error("domain delete failed", zap.Error(fmt.Errorf("OrgDomain.Id is null")))
|
|
||||||
return model.InternalError(fmt.Errorf("domain delete failed"))
|
|
||||||
}
|
|
||||||
|
|
||||||
storableDomain := &types.StorableOrgDomain{ID: id}
|
|
||||||
_, err := m.sqlStore.BunDB().NewDelete().
|
|
||||||
Model(storableDomain).
|
|
||||||
WherePK().
|
|
||||||
Exec(ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("domain delete failed", zap.Error(err))
|
|
||||||
return model.InternalError(fmt.Errorf("domain delete failed"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modelDao) GetDomainByEmail(ctx context.Context, email string) (*types.GettableOrgDomain, basemodel.BaseApiError) {
|
|
||||||
|
|
||||||
if email == "" {
|
|
||||||
return nil, model.BadRequest(fmt.Errorf("could not find auth domain, missing fields: email "))
|
|
||||||
}
|
|
||||||
|
|
||||||
components := strings.Split(email, "@")
|
|
||||||
if len(components) < 2 {
|
|
||||||
return nil, model.BadRequest(fmt.Errorf("invalid email address"))
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedDomain := components[1]
|
|
||||||
|
|
||||||
stored := types.StorableOrgDomain{}
|
|
||||||
err := m.sqlStore.BunDB().NewSelect().
|
|
||||||
Model(&stored).
|
|
||||||
Where("name = ?", parsedDomain).
|
|
||||||
Limit(1).
|
|
||||||
Scan(ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, model.InternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
domain := &types.GettableOrgDomain{StorableOrgDomain: stored}
|
|
||||||
if err := domain.LoadConfig(stored.Data); err != nil {
|
|
||||||
return nil, model.InternalError(err)
|
|
||||||
}
|
|
||||||
return domain, nil
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package sqlite
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
|
||||||
)
|
|
||||||
|
|
||||||
type modelDao struct {
|
|
||||||
sqlStore sqlstore.SQLStore
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitDB creates and extends base model DB repository
|
|
||||||
func NewModelDao(sqlStore sqlstore.SQLStore) *modelDao {
|
|
||||||
return &modelDao{sqlStore: sqlStore}
|
|
||||||
}
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
package signozio
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
|
||||||
"github.com/SigNoz/signoz/pkg/zeus"
|
|
||||||
"github.com/tidwall/gjson"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ValidateLicenseV3(ctx context.Context, licenseKey string, zeus zeus.Zeus) (*model.LicenseV3, error) {
|
|
||||||
data, err := zeus.GetLicense(ctx, licenseKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var m map[string]any
|
|
||||||
if err = json.Unmarshal(data, &m); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
license, err := model.NewLicenseV3(m)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return license, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendUsage reports the usage of signoz to license server
|
|
||||||
func SendUsage(ctx context.Context, usage model.UsagePayload, zeus zeus.Zeus) error {
|
|
||||||
body, err := json.Marshal(usage)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return zeus.PutMeters(ctx, usage.LicenseKey.String(), body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckoutSession(ctx context.Context, checkoutRequest *model.CheckoutRequest, licenseKey string, zeus zeus.Zeus) (string, error) {
|
|
||||||
body, err := json.Marshal(checkoutRequest)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := zeus.GetCheckoutURL(ctx, licenseKey, body)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return gjson.GetBytes(response, "url").String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func PortalSession(ctx context.Context, portalRequest *model.PortalRequest, licenseKey string, zeus zeus.Zeus) (string, error) {
|
|
||||||
body, err := json.Marshal(portalRequest)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := zeus.GetPortalURL(ctx, licenseKey, body)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return gjson.GetBytes(response, "url").String(), nil
|
|
||||||
}
|
|
||||||
@ -1,248 +0,0 @@
|
|||||||
package license
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
"github.com/mattn/go-sqlite3"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
|
||||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Repo is license repo. stores license keys in a secured DB
|
|
||||||
type Repo struct {
|
|
||||||
db *sqlx.DB
|
|
||||||
store sqlstore.SQLStore
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLicenseRepo initiates a new license repo
|
|
||||||
func NewLicenseRepo(db *sqlx.DB, store sqlstore.SQLStore) Repo {
|
|
||||||
return Repo{
|
|
||||||
db: db,
|
|
||||||
store: store,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repo) GetLicensesV3(ctx context.Context) ([]*model.LicenseV3, error) {
|
|
||||||
licensesData := []model.LicenseDB{}
|
|
||||||
licenseV3Data := []*model.LicenseV3{}
|
|
||||||
|
|
||||||
query := "SELECT id,key,data FROM licenses_v3"
|
|
||||||
|
|
||||||
err := r.db.Select(&licensesData, query)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get licenses from db: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, l := range licensesData {
|
|
||||||
var licenseData map[string]interface{}
|
|
||||||
err := json.Unmarshal([]byte(l.Data), &licenseData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal data into licenseData : %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
license, err := model.NewLicenseV3WithIDAndKey(l.ID, l.Key, licenseData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get licenses v3 schema : %v", err)
|
|
||||||
}
|
|
||||||
licenseV3Data = append(licenseV3Data, license)
|
|
||||||
}
|
|
||||||
|
|
||||||
return licenseV3Data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetActiveLicense fetches the latest active license from DB.
|
|
||||||
// If the license is not present, expect a nil license and a nil error in the output.
|
|
||||||
func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel.ApiError) {
|
|
||||||
activeLicenseV3, err := r.GetActiveLicenseV3(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if activeLicenseV3 == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
activeLicenseV2 := model.ConvertLicenseV3ToLicenseV2(activeLicenseV3)
|
|
||||||
return activeLicenseV2, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error) {
|
|
||||||
var err error
|
|
||||||
licenses := []model.LicenseDB{}
|
|
||||||
|
|
||||||
query := "SELECT id,key,data FROM licenses_v3"
|
|
||||||
|
|
||||||
err = r.db.Select(&licenses, query)
|
|
||||||
if err != nil {
|
|
||||||
return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
var active *model.LicenseV3
|
|
||||||
for _, l := range licenses {
|
|
||||||
var licenseData map[string]interface{}
|
|
||||||
err := json.Unmarshal([]byte(l.Data), &licenseData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal data into licenseData : %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
license, err := model.NewLicenseV3WithIDAndKey(l.ID, l.Key, licenseData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get licenses v3 schema : %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if active == nil &&
|
|
||||||
(license.ValidFrom != 0) &&
|
|
||||||
(license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) {
|
|
||||||
active = license
|
|
||||||
}
|
|
||||||
if active != nil &&
|
|
||||||
license.ValidFrom > active.ValidFrom &&
|
|
||||||
(license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) {
|
|
||||||
active = license
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return active, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertLicenseV3 inserts a new license v3 in db
|
|
||||||
func (r *Repo) InsertLicenseV3(ctx context.Context, l *model.LicenseV3) *model.ApiError {
|
|
||||||
|
|
||||||
query := `INSERT INTO licenses_v3 (id, key, data) VALUES ($1, $2, $3)`
|
|
||||||
|
|
||||||
// licsense is the entity of zeus so putting the entire license here without defining schema
|
|
||||||
licenseData, err := json.Marshal(l.Data)
|
|
||||||
if err != nil {
|
|
||||||
return &model.ApiError{Typ: basemodel.ErrorBadData, Err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = r.db.ExecContext(ctx,
|
|
||||||
query,
|
|
||||||
l.ID,
|
|
||||||
l.Key,
|
|
||||||
string(licenseData),
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if sqliteErr, ok := err.(sqlite3.Error); ok {
|
|
||||||
if sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
|
|
||||||
zap.L().Error("error in inserting license data: ", zap.Error(sqliteErr))
|
|
||||||
return &model.ApiError{Typ: model.ErrorConflict, Err: sqliteErr}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
zap.L().Error("error in inserting license data: ", zap.Error(err))
|
|
||||||
return &model.ApiError{Typ: basemodel.ErrorExec, Err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateLicenseV3 updates a new license v3 in db
|
|
||||||
func (r *Repo) UpdateLicenseV3(ctx context.Context, l *model.LicenseV3) error {
|
|
||||||
|
|
||||||
// the key and id for the license can't change so only update the data here!
|
|
||||||
query := `UPDATE licenses_v3 SET data=$1 WHERE id=$2;`
|
|
||||||
|
|
||||||
license, err := json.Marshal(l.Data)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("insert license failed: license marshal error")
|
|
||||||
}
|
|
||||||
_, err = r.db.ExecContext(ctx,
|
|
||||||
query,
|
|
||||||
license,
|
|
||||||
l.ID,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("error in updating license data: ", zap.Error(err))
|
|
||||||
return fmt.Errorf("failed to update license in db: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repo) CreateFeature(req *types.FeatureStatus) *basemodel.ApiError {
|
|
||||||
|
|
||||||
_, err := r.store.BunDB().NewInsert().
|
|
||||||
Model(req).
|
|
||||||
Exec(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return &basemodel.ApiError{Typ: basemodel.ErrorInternal, Err: err}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repo) GetFeature(featureName string) (types.FeatureStatus, error) {
|
|
||||||
var feature types.FeatureStatus
|
|
||||||
|
|
||||||
err := r.store.BunDB().NewSelect().
|
|
||||||
Model(&feature).
|
|
||||||
Where("name = ?", featureName).
|
|
||||||
Scan(context.Background())
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return feature, err
|
|
||||||
}
|
|
||||||
if feature.Name == "" {
|
|
||||||
return feature, basemodel.ErrFeatureUnavailable{Key: featureName}
|
|
||||||
}
|
|
||||||
return feature, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repo) GetAllFeatures() ([]basemodel.Feature, error) {
|
|
||||||
|
|
||||||
var feature []basemodel.Feature
|
|
||||||
|
|
||||||
err := r.db.Select(&feature,
|
|
||||||
`SELECT * FROM feature_status;`)
|
|
||||||
if err != nil {
|
|
||||||
return feature, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return feature, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repo) UpdateFeature(req types.FeatureStatus) error {
|
|
||||||
|
|
||||||
_, err := r.store.BunDB().NewUpdate().
|
|
||||||
Model(&req).
|
|
||||||
Where("name = ?", req.Name).
|
|
||||||
Exec(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Repo) InitFeatures(req []types.FeatureStatus) error {
|
|
||||||
// get a feature by name, if it doesn't exist, create it. If it does exist, update it.
|
|
||||||
for _, feature := range req {
|
|
||||||
currentFeature, err := r.GetFeature(feature.Name)
|
|
||||||
if err != nil && err == sql.ErrNoRows {
|
|
||||||
err := r.CreateFeature(&feature)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
feature.Usage = int(currentFeature.Usage)
|
|
||||||
if feature.Usage >= feature.UsageLimit && feature.UsageLimit != -1 {
|
|
||||||
feature.Active = false
|
|
||||||
}
|
|
||||||
err = r.UpdateFeature(feature)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,318 +0,0 @@
|
|||||||
package license
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
baseconstants "github.com/SigNoz/signoz/pkg/query-service/constants"
|
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
|
||||||
"github.com/SigNoz/signoz/pkg/types"
|
|
||||||
"github.com/SigNoz/signoz/pkg/zeus"
|
|
||||||
|
|
||||||
validate "github.com/SigNoz/signoz/ee/query-service/integrations/signozio"
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
|
||||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
var LM *Manager
|
|
||||||
|
|
||||||
// validate and update license every 24 hours
|
|
||||||
var validationFrequency = 24 * 60 * time.Minute
|
|
||||||
|
|
||||||
type Manager struct {
|
|
||||||
repo *Repo
|
|
||||||
zeus zeus.Zeus
|
|
||||||
mutex sync.Mutex
|
|
||||||
validatorRunning bool
|
|
||||||
// end the license validation, this is important to gracefully
|
|
||||||
// stopping validation and protect in-consistent updates
|
|
||||||
done chan struct{}
|
|
||||||
// terminated waits for the validate go routine to end
|
|
||||||
terminated chan struct{}
|
|
||||||
// last time the license was validated
|
|
||||||
lastValidated int64
|
|
||||||
// keep track of validation failure attempts
|
|
||||||
failedAttempts uint64
|
|
||||||
// keep track of active license and features
|
|
||||||
activeLicenseV3 *model.LicenseV3
|
|
||||||
activeFeatures basemodel.FeatureSet
|
|
||||||
}
|
|
||||||
|
|
||||||
func StartManager(db *sqlx.DB, store sqlstore.SQLStore, zeus zeus.Zeus, features ...basemodel.Feature) (*Manager, error) {
|
|
||||||
if LM != nil {
|
|
||||||
return LM, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
repo := NewLicenseRepo(db, store)
|
|
||||||
m := &Manager{
|
|
||||||
repo: &repo,
|
|
||||||
zeus: zeus,
|
|
||||||
}
|
|
||||||
if err := m.start(features...); err != nil {
|
|
||||||
return m, err
|
|
||||||
}
|
|
||||||
|
|
||||||
LM = m
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// start loads active license in memory and initiates validator
|
|
||||||
func (lm *Manager) start(features ...basemodel.Feature) error {
|
|
||||||
return lm.LoadActiveLicenseV3(features...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lm *Manager) Stop() {
|
|
||||||
close(lm.done)
|
|
||||||
<-lm.terminated
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lm *Manager) SetActiveV3(l *model.LicenseV3, features ...basemodel.Feature) {
|
|
||||||
lm.mutex.Lock()
|
|
||||||
defer lm.mutex.Unlock()
|
|
||||||
|
|
||||||
if l == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lm.activeLicenseV3 = l
|
|
||||||
lm.activeFeatures = append(l.Features, features...)
|
|
||||||
// set default features
|
|
||||||
setDefaultFeatures(lm)
|
|
||||||
|
|
||||||
err := lm.InitFeatures(lm.activeFeatures)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Panic("Couldn't activate features", zap.Error(err))
|
|
||||||
}
|
|
||||||
if !lm.validatorRunning {
|
|
||||||
// we want to make sure only one validator runs,
|
|
||||||
// we already have lock() so good to go
|
|
||||||
lm.validatorRunning = true
|
|
||||||
go lm.ValidatorV3(context.Background())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func setDefaultFeatures(lm *Manager) {
|
|
||||||
lm.activeFeatures = append(lm.activeFeatures, baseconstants.DEFAULT_FEATURE_SET...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error {
|
|
||||||
active, err := lm.repo.GetActiveLicenseV3(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if active != nil {
|
|
||||||
lm.SetActiveV3(active, features...)
|
|
||||||
} else {
|
|
||||||
zap.L().Info("No active license found, defaulting to basic plan")
|
|
||||||
// if no active license is found, we default to basic(free) plan with all default features
|
|
||||||
lm.activeFeatures = model.BasicPlan
|
|
||||||
setDefaultFeatures(lm)
|
|
||||||
err := lm.InitFeatures(lm.activeFeatures)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("Couldn't initialize features", zap.Error(err))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lm *Manager) GetLicensesV3(ctx context.Context) (response []*model.LicenseV3, apiError *model.ApiError) {
|
|
||||||
|
|
||||||
licenses, err := lm.repo.GetLicensesV3(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, model.InternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, l := range licenses {
|
|
||||||
if lm.activeLicenseV3 != nil && l.Key == lm.activeLicenseV3.Key {
|
|
||||||
l.IsCurrent = true
|
|
||||||
}
|
|
||||||
if l.ValidUntil == -1 {
|
|
||||||
// for subscriptions, there is no end-date as such
|
|
||||||
// but for showing user some validity we default one year timespan
|
|
||||||
l.ValidUntil = l.ValidFrom + 31556926
|
|
||||||
}
|
|
||||||
response = append(response, l)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validator validates license after an epoch of time
|
|
||||||
func (lm *Manager) ValidatorV3(ctx context.Context) {
|
|
||||||
zap.L().Info("ValidatorV3 started!")
|
|
||||||
defer close(lm.terminated)
|
|
||||||
|
|
||||||
tick := time.NewTicker(validationFrequency)
|
|
||||||
defer tick.Stop()
|
|
||||||
|
|
||||||
_ = lm.ValidateV3(ctx)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-lm.done:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
select {
|
|
||||||
case <-lm.done:
|
|
||||||
return
|
|
||||||
case <-tick.C:
|
|
||||||
_ = lm.ValidateV3(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lm *Manager) RefreshLicense(ctx context.Context) error {
|
|
||||||
license, err := validate.ValidateLicenseV3(ctx, lm.activeLicenseV3.Key, lm.zeus)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = lm.repo.UpdateLicenseV3(ctx, license)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
lm.SetActiveV3(license)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) {
|
|
||||||
if lm.activeLicenseV3 == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
lm.mutex.Lock()
|
|
||||||
|
|
||||||
lm.lastValidated = time.Now().Unix()
|
|
||||||
if reterr != nil {
|
|
||||||
zap.L().Error("License validation completed with error", zap.Error(reterr))
|
|
||||||
|
|
||||||
atomic.AddUint64(&lm.failedAttempts, 1)
|
|
||||||
// default to basic plan if validation fails for three consecutive times
|
|
||||||
if atomic.LoadUint64(&lm.failedAttempts) > 3 {
|
|
||||||
zap.L().Error("License validation completed with error for three consecutive times, defaulting to basic plan", zap.String("license_id", lm.activeLicenseV3.ID), zap.Bool("license_validation", false))
|
|
||||||
lm.activeLicenseV3 = nil
|
|
||||||
lm.activeFeatures = model.BasicPlan
|
|
||||||
setDefaultFeatures(lm)
|
|
||||||
err := lm.InitFeatures(lm.activeFeatures)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("Couldn't initialize features", zap.Error(err))
|
|
||||||
}
|
|
||||||
lm.done <- struct{}{}
|
|
||||||
lm.validatorRunning = false
|
|
||||||
}
|
|
||||||
|
|
||||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED,
|
|
||||||
map[string]interface{}{"err": reterr.Error()}, "", true, false)
|
|
||||||
} else {
|
|
||||||
// reset the failed attempts counter
|
|
||||||
atomic.StoreUint64(&lm.failedAttempts, 0)
|
|
||||||
zap.L().Info("License validation completed with no errors")
|
|
||||||
}
|
|
||||||
|
|
||||||
lm.mutex.Unlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
err := lm.RefreshLicense(ctx)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (*model.LicenseV3, error) {
|
|
||||||
license, err := validate.ValidateLicenseV3(ctx, licenseKey, lm.zeus)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// insert the new license to the sqlite db
|
|
||||||
modelErr := lm.repo.InsertLicenseV3(ctx, license)
|
|
||||||
if modelErr != nil {
|
|
||||||
zap.L().Error("failed to activate license", zap.Error(modelErr))
|
|
||||||
return nil, modelErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// license is valid, activate it
|
|
||||||
lm.SetActiveV3(license)
|
|
||||||
return license, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lm *Manager) GetActiveLicense() *model.LicenseV3 {
|
|
||||||
return lm.activeLicenseV3
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckFeature will be internally used by backend routines
|
|
||||||
// for feature gating
|
|
||||||
func (lm *Manager) CheckFeature(featureKey string) error {
|
|
||||||
feature, err := lm.repo.GetFeature(featureKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if feature.Active {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return basemodel.ErrFeatureUnavailable{Key: featureKey}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFeatureFlags returns current active features
|
|
||||||
func (lm *Manager) GetFeatureFlags() (basemodel.FeatureSet, error) {
|
|
||||||
return lm.repo.GetAllFeatures()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lm *Manager) InitFeatures(features basemodel.FeatureSet) error {
|
|
||||||
featureStatus := make([]types.FeatureStatus, len(features))
|
|
||||||
for i, f := range features {
|
|
||||||
featureStatus[i] = types.FeatureStatus{
|
|
||||||
Name: f.Name,
|
|
||||||
Active: f.Active,
|
|
||||||
Usage: int(f.Usage),
|
|
||||||
UsageLimit: int(f.UsageLimit),
|
|
||||||
Route: f.Route,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lm.repo.InitFeatures(featureStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lm *Manager) UpdateFeatureFlag(feature basemodel.Feature) error {
|
|
||||||
return lm.repo.UpdateFeature(types.FeatureStatus{
|
|
||||||
Name: feature.Name,
|
|
||||||
Active: feature.Active,
|
|
||||||
Usage: int(feature.Usage),
|
|
||||||
UsageLimit: int(feature.UsageLimit),
|
|
||||||
Route: feature.Route,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lm *Manager) GetFeatureFlag(key string) (basemodel.Feature, error) {
|
|
||||||
featureStatus, err := lm.repo.GetFeature(key)
|
|
||||||
if err != nil {
|
|
||||||
return basemodel.Feature{}, err
|
|
||||||
}
|
|
||||||
return basemodel.Feature{
|
|
||||||
Name: featureStatus.Name,
|
|
||||||
Active: featureStatus.Active,
|
|
||||||
Usage: int64(featureStatus.Usage),
|
|
||||||
UsageLimit: int64(featureStatus.UsageLimit),
|
|
||||||
Route: featureStatus.Route,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepo return the license repo
|
|
||||||
func (lm *Manager) GetRepo() *Repo {
|
|
||||||
return lm.repo
|
|
||||||
}
|
|
||||||
@ -6,7 +6,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
eeuserimpl "github.com/SigNoz/signoz/ee/modules/user/impluser"
|
"github.com/SigNoz/signoz/ee/licensing"
|
||||||
|
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
||||||
"github.com/SigNoz/signoz/ee/query-service/app"
|
"github.com/SigNoz/signoz/ee/query-service/app"
|
||||||
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
|
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
|
||||||
"github.com/SigNoz/signoz/ee/zeus"
|
"github.com/SigNoz/signoz/ee/zeus"
|
||||||
@ -14,15 +15,15 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/config"
|
"github.com/SigNoz/signoz/pkg/config"
|
||||||
"github.com/SigNoz/signoz/pkg/config/envprovider"
|
"github.com/SigNoz/signoz/pkg/config/envprovider"
|
||||||
"github.com/SigNoz/signoz/pkg/config/fileprovider"
|
"github.com/SigNoz/signoz/pkg/config/fileprovider"
|
||||||
"github.com/SigNoz/signoz/pkg/emailing"
|
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
pkglicensing "github.com/SigNoz/signoz/pkg/licensing"
|
||||||
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
|
||||||
"github.com/SigNoz/signoz/pkg/signoz"
|
"github.com/SigNoz/signoz/pkg/signoz"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
|
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
"github.com/SigNoz/signoz/pkg/version"
|
"github.com/SigNoz/signoz/pkg/version"
|
||||||
|
pkgzeus "github.com/SigNoz/signoz/pkg/zeus"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
@ -90,8 +91,9 @@ func main() {
|
|||||||
loggerMgr := initZapLog()
|
loggerMgr := initZapLog()
|
||||||
zap.ReplaceGlobals(loggerMgr)
|
zap.ReplaceGlobals(loggerMgr)
|
||||||
defer loggerMgr.Sync() // flushes buffer, if any
|
defer loggerMgr.Sync() // flushes buffer, if any
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
config, err := signoz.NewConfig(context.Background(), config.ResolverConfig{
|
config, err := signoz.NewConfig(ctx, config.ResolverConfig{
|
||||||
Uris: []string{"env:"},
|
Uris: []string{"env:"},
|
||||||
ProviderFactories: []config.ProviderFactory{
|
ProviderFactories: []config.ProviderFactory{
|
||||||
envprovider.NewFactory(),
|
envprovider.NewFactory(),
|
||||||
@ -127,19 +129,18 @@ func main() {
|
|||||||
signoz, err := signoz.New(
|
signoz, err := signoz.New(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
config,
|
config,
|
||||||
|
jwt,
|
||||||
zeus.Config(),
|
zeus.Config(),
|
||||||
httpzeus.NewProviderFactory(),
|
httpzeus.NewProviderFactory(),
|
||||||
|
licensing.Config(24*time.Hour, 3),
|
||||||
|
func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] {
|
||||||
|
return httplicensing.NewProviderFactory(sqlstore, zeus)
|
||||||
|
},
|
||||||
signoz.NewEmailingProviderFactories(),
|
signoz.NewEmailingProviderFactories(),
|
||||||
signoz.NewCacheProviderFactories(),
|
signoz.NewCacheProviderFactories(),
|
||||||
signoz.NewWebProviderFactories(),
|
signoz.NewWebProviderFactories(),
|
||||||
sqlStoreFactories,
|
sqlStoreFactories,
|
||||||
signoz.NewTelemetryStoreProviderFactories(),
|
signoz.NewTelemetryStoreProviderFactories(),
|
||||||
func(sqlstore sqlstore.SQLStore, emailing emailing.Emailing, providerSettings factory.ProviderSettings) user.Module {
|
|
||||||
return eeuserimpl.NewModule(eeuserimpl.NewStore(sqlstore), jwt, emailing, providerSettings)
|
|
||||||
},
|
|
||||||
func(userModule user.Module) user.Handler {
|
|
||||||
return eeuserimpl.NewHandler(userModule)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Fatal("Failed to create signoz", zap.Error(err))
|
zap.L().Fatal("Failed to create signoz", zap.Error(err))
|
||||||
@ -163,22 +164,22 @@ func main() {
|
|||||||
zap.L().Fatal("Failed to create server", zap.Error(err))
|
zap.L().Fatal("Failed to create server", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := server.Start(context.Background()); err != nil {
|
if err := server.Start(ctx); err != nil {
|
||||||
zap.L().Fatal("Could not start server", zap.Error(err))
|
zap.L().Fatal("Could not start server", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
signoz.Start(context.Background())
|
signoz.Start(ctx)
|
||||||
|
|
||||||
if err := signoz.Wait(context.Background()); err != nil {
|
if err := signoz.Wait(ctx); err != nil {
|
||||||
zap.L().Fatal("Failed to start signoz", zap.Error(err))
|
zap.L().Fatal("Failed to start signoz", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = server.Stop()
|
err = server.Stop(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Fatal("Failed to stop server", zap.Error(err))
|
zap.L().Fatal("Failed to stop server", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = signoz.Stop(context.Background())
|
err = signoz.Stop(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Fatal("Failed to stop signoz", zap.Error(err))
|
zap.L().Fatal("Failed to stop signoz", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,244 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type License struct {
|
|
||||||
Key string `json:"key" db:"key"`
|
|
||||||
ActivationId string `json:"activationId" db:"activationId"`
|
|
||||||
CreatedAt time.Time `db:"created_at"`
|
|
||||||
|
|
||||||
// PlanDetails contains the encrypted plan info
|
|
||||||
PlanDetails string `json:"planDetails" db:"planDetails"`
|
|
||||||
|
|
||||||
// stores parsed license details
|
|
||||||
LicensePlan
|
|
||||||
|
|
||||||
FeatureSet basemodel.FeatureSet
|
|
||||||
|
|
||||||
// populated in case license has any errors
|
|
||||||
ValidationMessage string `db:"validationMessage"`
|
|
||||||
|
|
||||||
// used only for sending details to front-end
|
|
||||||
IsCurrent bool `json:"isCurrent"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *License) MarshalJSON() ([]byte, error) {
|
|
||||||
|
|
||||||
return json.Marshal(&struct {
|
|
||||||
Key string `json:"key" db:"key"`
|
|
||||||
ActivationId string `json:"activationId" db:"activationId"`
|
|
||||||
ValidationMessage string `db:"validationMessage"`
|
|
||||||
IsCurrent bool `json:"isCurrent"`
|
|
||||||
PlanKey string `json:"planKey"`
|
|
||||||
ValidFrom time.Time `json:"ValidFrom"`
|
|
||||||
ValidUntil time.Time `json:"ValidUntil"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
}{
|
|
||||||
Key: l.Key,
|
|
||||||
ActivationId: l.ActivationId,
|
|
||||||
IsCurrent: l.IsCurrent,
|
|
||||||
PlanKey: l.PlanKey,
|
|
||||||
ValidFrom: time.Unix(l.ValidFrom, 0),
|
|
||||||
ValidUntil: time.Unix(l.ValidUntil, 0),
|
|
||||||
Status: l.Status,
|
|
||||||
ValidationMessage: l.ValidationMessage,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type LicensePlan struct {
|
|
||||||
PlanKey string `json:"planKey"`
|
|
||||||
ValidFrom int64 `json:"validFrom"`
|
|
||||||
ValidUntil int64 `json:"validUntil"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Licenses struct {
|
|
||||||
TrialStart int64 `json:"trialStart"`
|
|
||||||
TrialEnd int64 `json:"trialEnd"`
|
|
||||||
OnTrial bool `json:"onTrial"`
|
|
||||||
WorkSpaceBlock bool `json:"workSpaceBlock"`
|
|
||||||
TrialConvertedToSubscription bool `json:"trialConvertedToSubscription"`
|
|
||||||
GracePeriodEnd int64 `json:"gracePeriodEnd"`
|
|
||||||
Licenses []License `json:"licenses"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubscriptionServerResp struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Data Licenses `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Plan struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LicenseDB struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Key string `json:"key"`
|
|
||||||
Data string `json:"data"`
|
|
||||||
}
|
|
||||||
type LicenseV3 struct {
|
|
||||||
ID string
|
|
||||||
Key string
|
|
||||||
Data map[string]interface{}
|
|
||||||
PlanName string
|
|
||||||
Features basemodel.FeatureSet
|
|
||||||
Status string
|
|
||||||
IsCurrent bool
|
|
||||||
ValidFrom int64
|
|
||||||
ValidUntil int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractKeyFromMapStringInterface[T any](data map[string]interface{}, key string) (T, error) {
|
|
||||||
var zeroValue T
|
|
||||||
if val, ok := data[key]; ok {
|
|
||||||
if value, ok := val.(T); ok {
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
return zeroValue, fmt.Errorf("%s key is not a valid %s", key, reflect.TypeOf(zeroValue))
|
|
||||||
}
|
|
||||||
return zeroValue, fmt.Errorf("%s key is missing", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLicenseV3(data map[string]interface{}) (*LicenseV3, error) {
|
|
||||||
var features basemodel.FeatureSet
|
|
||||||
|
|
||||||
// extract id from data
|
|
||||||
licenseID, err := extractKeyFromMapStringInterface[string](data, "id")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
delete(data, "id")
|
|
||||||
|
|
||||||
// extract key from data
|
|
||||||
licenseKey, err := extractKeyFromMapStringInterface[string](data, "key")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
delete(data, "key")
|
|
||||||
|
|
||||||
// extract status from data
|
|
||||||
status, err := extractKeyFromMapStringInterface[string](data, "status")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
planMap, err := extractKeyFromMapStringInterface[map[string]any](data, "plan")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
planName, err := extractKeyFromMapStringInterface[string](planMap, "name")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// if license status is invalid then default it to basic
|
|
||||||
if status == LicenseStatusInvalid {
|
|
||||||
planName = PlanNameBasic
|
|
||||||
}
|
|
||||||
|
|
||||||
featuresFromZeus := basemodel.FeatureSet{}
|
|
||||||
if _features, ok := data["features"]; ok {
|
|
||||||
featuresData, err := json.Marshal(_features)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to marshal features data")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := json.Unmarshal(featuresData, &featuresFromZeus); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to unmarshal features data")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch planName {
|
|
||||||
case PlanNameEnterprise:
|
|
||||||
features = append(features, EnterprisePlan...)
|
|
||||||
case PlanNameBasic:
|
|
||||||
features = append(features, BasicPlan...)
|
|
||||||
default:
|
|
||||||
features = append(features, BasicPlan...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(featuresFromZeus) > 0 {
|
|
||||||
for _, feature := range featuresFromZeus {
|
|
||||||
exists := false
|
|
||||||
for i, existingFeature := range features {
|
|
||||||
if existingFeature.Name == feature.Name {
|
|
||||||
features[i] = feature // Replace existing feature
|
|
||||||
exists = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
features = append(features, feature) // Append if it doesn't exist
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data["features"] = features
|
|
||||||
|
|
||||||
_validFrom, err := extractKeyFromMapStringInterface[float64](data, "valid_from")
|
|
||||||
if err != nil {
|
|
||||||
_validFrom = 0
|
|
||||||
}
|
|
||||||
validFrom := int64(_validFrom)
|
|
||||||
|
|
||||||
_validUntil, err := extractKeyFromMapStringInterface[float64](data, "valid_until")
|
|
||||||
if err != nil {
|
|
||||||
_validUntil = 0
|
|
||||||
}
|
|
||||||
validUntil := int64(_validUntil)
|
|
||||||
|
|
||||||
return &LicenseV3{
|
|
||||||
ID: licenseID,
|
|
||||||
Key: licenseKey,
|
|
||||||
Data: data,
|
|
||||||
PlanName: planName,
|
|
||||||
Features: features,
|
|
||||||
ValidFrom: validFrom,
|
|
||||||
ValidUntil: validUntil,
|
|
||||||
Status: status,
|
|
||||||
}, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLicenseV3WithIDAndKey(id string, key string, data map[string]interface{}) (*LicenseV3, error) {
|
|
||||||
licenseDataWithIdAndKey := data
|
|
||||||
licenseDataWithIdAndKey["id"] = id
|
|
||||||
licenseDataWithIdAndKey["key"] = key
|
|
||||||
return NewLicenseV3(licenseDataWithIdAndKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ConvertLicenseV3ToLicenseV2(l *LicenseV3) *License {
|
|
||||||
planKeyFromPlanName, ok := MapOldPlanKeyToNewPlanName[l.PlanName]
|
|
||||||
if !ok {
|
|
||||||
planKeyFromPlanName = Basic
|
|
||||||
}
|
|
||||||
return &License{
|
|
||||||
Key: l.Key,
|
|
||||||
ActivationId: "",
|
|
||||||
PlanDetails: "",
|
|
||||||
FeatureSet: l.Features,
|
|
||||||
ValidationMessage: "",
|
|
||||||
IsCurrent: l.IsCurrent,
|
|
||||||
LicensePlan: LicensePlan{
|
|
||||||
PlanKey: planKeyFromPlanName,
|
|
||||||
ValidFrom: l.ValidFrom,
|
|
||||||
ValidUntil: l.ValidUntil,
|
|
||||||
Status: l.Status},
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
type CheckoutRequest struct {
|
|
||||||
SuccessURL string `json:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PortalRequest struct {
|
|
||||||
SuccessURL string `json:"url"`
|
|
||||||
}
|
|
||||||
@ -1,170 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewLicenseV3(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
data []byte
|
|
||||||
pass bool
|
|
||||||
expected *LicenseV3
|
|
||||||
error error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Error for missing license id",
|
|
||||||
data: []byte(`{}`),
|
|
||||||
pass: false,
|
|
||||||
error: errors.New("id key is missing"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Error for license id not being a valid string",
|
|
||||||
data: []byte(`{"id": 10}`),
|
|
||||||
pass: false,
|
|
||||||
error: errors.New("id key is not a valid string"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Error for missing license key",
|
|
||||||
data: []byte(`{"id":"does-not-matter"}`),
|
|
||||||
pass: false,
|
|
||||||
error: errors.New("key key is missing"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Error for invalid string license key",
|
|
||||||
data: []byte(`{"id":"does-not-matter","key":10}`),
|
|
||||||
pass: false,
|
|
||||||
error: errors.New("key key is not a valid string"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Error for missing license status",
|
|
||||||
data: []byte(`{"id":"does-not-matter", "key": "does-not-matter","category":"FREE"}`),
|
|
||||||
pass: false,
|
|
||||||
error: errors.New("status key is missing"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Error for invalid string license status",
|
|
||||||
data: []byte(`{"id":"does-not-matter","key": "does-not-matter", "category":"FREE", "status":10}`),
|
|
||||||
pass: false,
|
|
||||||
error: errors.New("status key is not a valid string"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Error for missing license plan",
|
|
||||||
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE"}`),
|
|
||||||
pass: false,
|
|
||||||
error: errors.New("plan key is missing"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Error for invalid json license plan",
|
|
||||||
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":10}`),
|
|
||||||
pass: false,
|
|
||||||
error: errors.New("plan key is not a valid map[string]interface {}"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Error for invalid license plan",
|
|
||||||
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{}}`),
|
|
||||||
pass: false,
|
|
||||||
error: errors.New("name key is missing"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Parse the entire license properly",
|
|
||||||
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"ENTERPRISE"},"valid_from": 1730899309,"valid_until": -1}`),
|
|
||||||
pass: true,
|
|
||||||
expected: &LicenseV3{
|
|
||||||
ID: "does-not-matter",
|
|
||||||
Key: "does-not-matter-key",
|
|
||||||
Data: map[string]interface{}{
|
|
||||||
"plan": map[string]interface{}{
|
|
||||||
"name": "ENTERPRISE",
|
|
||||||
},
|
|
||||||
"category": "FREE",
|
|
||||||
"status": "ACTIVE",
|
|
||||||
"valid_from": float64(1730899309),
|
|
||||||
"valid_until": float64(-1),
|
|
||||||
},
|
|
||||||
PlanName: PlanNameEnterprise,
|
|
||||||
ValidFrom: 1730899309,
|
|
||||||
ValidUntil: -1,
|
|
||||||
Status: "ACTIVE",
|
|
||||||
IsCurrent: false,
|
|
||||||
Features: model.FeatureSet{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Fallback to basic plan if license status is invalid",
|
|
||||||
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"INVALID","plan":{"name":"ENTERPRISE"},"valid_from": 1730899309,"valid_until": -1}`),
|
|
||||||
pass: true,
|
|
||||||
expected: &LicenseV3{
|
|
||||||
ID: "does-not-matter",
|
|
||||||
Key: "does-not-matter-key",
|
|
||||||
Data: map[string]interface{}{
|
|
||||||
"plan": map[string]interface{}{
|
|
||||||
"name": "ENTERPRISE",
|
|
||||||
},
|
|
||||||
"category": "FREE",
|
|
||||||
"status": "INVALID",
|
|
||||||
"valid_from": float64(1730899309),
|
|
||||||
"valid_until": float64(-1),
|
|
||||||
},
|
|
||||||
PlanName: PlanNameBasic,
|
|
||||||
ValidFrom: 1730899309,
|
|
||||||
ValidUntil: -1,
|
|
||||||
Status: "INVALID",
|
|
||||||
IsCurrent: false,
|
|
||||||
Features: model.FeatureSet{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "fallback states for validFrom and validUntil",
|
|
||||||
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"ENTERPRISE"},"valid_from":1234.456,"valid_until":5678.567}`),
|
|
||||||
pass: true,
|
|
||||||
expected: &LicenseV3{
|
|
||||||
ID: "does-not-matter",
|
|
||||||
Key: "does-not-matter-key",
|
|
||||||
Data: map[string]interface{}{
|
|
||||||
"plan": map[string]interface{}{
|
|
||||||
"name": "ENTERPRISE",
|
|
||||||
},
|
|
||||||
"valid_from": 1234.456,
|
|
||||||
"valid_until": 5678.567,
|
|
||||||
"category": "FREE",
|
|
||||||
"status": "ACTIVE",
|
|
||||||
},
|
|
||||||
PlanName: PlanNameEnterprise,
|
|
||||||
ValidFrom: 1234,
|
|
||||||
ValidUntil: 5678,
|
|
||||||
Status: "ACTIVE",
|
|
||||||
IsCurrent: false,
|
|
||||||
Features: model.FeatureSet{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
var licensePayload map[string]interface{}
|
|
||||||
err := json.Unmarshal(tc.data, &licensePayload)
|
|
||||||
require.NoError(t, err)
|
|
||||||
license, err := NewLicenseV3(licensePayload)
|
|
||||||
if license != nil {
|
|
||||||
license.Features = make(model.FeatureSet, 0)
|
|
||||||
delete(license.Data, "features")
|
|
||||||
}
|
|
||||||
|
|
||||||
if tc.pass {
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, license)
|
|
||||||
assert.Equal(t, tc.expected, license)
|
|
||||||
} else {
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.EqualError(t, err, tc.error.Error())
|
|
||||||
require.Nil(t, license)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -14,9 +14,9 @@ import (
|
|||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/dao"
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/license"
|
|
||||||
"github.com/SigNoz/signoz/ee/query-service/model"
|
"github.com/SigNoz/signoz/ee/query-service/model"
|
||||||
|
"github.com/SigNoz/signoz/pkg/licensing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/utils/encryption"
|
"github.com/SigNoz/signoz/pkg/query-service/utils/encryption"
|
||||||
"github.com/SigNoz/signoz/pkg/zeus"
|
"github.com/SigNoz/signoz/pkg/zeus"
|
||||||
)
|
)
|
||||||
@ -35,49 +35,54 @@ var (
|
|||||||
type Manager struct {
|
type Manager struct {
|
||||||
clickhouseConn clickhouse.Conn
|
clickhouseConn clickhouse.Conn
|
||||||
|
|
||||||
licenseRepo *license.Repo
|
licenseService licensing.Licensing
|
||||||
|
|
||||||
scheduler *gocron.Scheduler
|
scheduler *gocron.Scheduler
|
||||||
|
|
||||||
modelDao dao.ModelDao
|
|
||||||
|
|
||||||
zeus zeus.Zeus
|
zeus zeus.Zeus
|
||||||
|
|
||||||
|
organizationModule organization.Module
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(modelDao dao.ModelDao, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn, zeus zeus.Zeus) (*Manager, error) {
|
func New(licenseService licensing.Licensing, clickhouseConn clickhouse.Conn, zeus zeus.Zeus, organizationModule organization.Module) (*Manager, error) {
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
clickhouseConn: clickhouseConn,
|
clickhouseConn: clickhouseConn,
|
||||||
licenseRepo: licenseRepo,
|
licenseService: licenseService,
|
||||||
scheduler: gocron.NewScheduler(time.UTC).Every(1).Day().At("00:00"), // send usage every at 00:00 UTC
|
scheduler: gocron.NewScheduler(time.UTC).Every(1).Day().At("00:00"), // send usage every at 00:00 UTC
|
||||||
modelDao: modelDao,
|
|
||||||
zeus: zeus,
|
zeus: zeus,
|
||||||
|
organizationModule: organizationModule,
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// start loads collects and exports any exported snapshot and starts the exporter
|
// start loads collects and exports any exported snapshot and starts the exporter
|
||||||
func (lm *Manager) Start() error {
|
func (lm *Manager) Start(ctx context.Context) error {
|
||||||
// compares the locker and stateUnlocked if both are same lock is applied else returns error
|
// compares the locker and stateUnlocked if both are same lock is applied else returns error
|
||||||
if !atomic.CompareAndSwapUint32(&locker, stateUnlocked, stateLocked) {
|
if !atomic.CompareAndSwapUint32(&locker, stateUnlocked, stateLocked) {
|
||||||
return fmt.Errorf("usage exporter is locked")
|
return fmt.Errorf("usage exporter is locked")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := lm.scheduler.Do(func() { lm.UploadUsage() })
|
// upload usage once when starting the service
|
||||||
|
|
||||||
|
_, err := lm.scheduler.Do(func() { lm.UploadUsage(ctx) })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// upload usage once when starting the service
|
lm.UploadUsage(ctx)
|
||||||
lm.UploadUsage()
|
|
||||||
|
|
||||||
lm.scheduler.StartAsync()
|
lm.scheduler.StartAsync()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (lm *Manager) UploadUsage() {
|
func (lm *Manager) UploadUsage(ctx context.Context) {
|
||||||
ctx := context.Background()
|
|
||||||
|
organizations, err := lm.organizationModule.GetAll(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
zap.L().Error("failed to get organizations", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, organization := range organizations {
|
||||||
// check if license is present or not
|
// check if license is present or not
|
||||||
license, err := lm.licenseRepo.GetActiveLicense(ctx)
|
license, err := lm.licenseService.GetActive(ctx, organization.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zap.L().Error("failed to get active license", zap.Error(err))
|
zap.L().Error("failed to get active license", zap.Error(err))
|
||||||
return
|
return
|
||||||
@ -169,14 +174,14 @@ func (lm *Manager) UploadUsage() {
|
|||||||
// not returning error here since it is captured in the failed count
|
// not returning error here since it is captured in the failed count
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lm *Manager) Stop() {
|
func (lm *Manager) Stop(ctx context.Context) {
|
||||||
lm.scheduler.Stop()
|
lm.scheduler.Stop()
|
||||||
|
|
||||||
zap.L().Info("sending usage data before shutting down")
|
zap.L().Info("sending usage data before shutting down")
|
||||||
// send usage before shutting down
|
// send usage before shutting down
|
||||||
lm.UploadUsage()
|
lm.UploadUsage(ctx)
|
||||||
|
|
||||||
atomic.StoreUint32(&locker, stateUnlocked)
|
atomic.StoreUint32(&locker, stateUnlocked)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,8 +36,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
user,
|
user,
|
||||||
isLoggedIn: isLoggedInState,
|
isLoggedIn: isLoggedInState,
|
||||||
isFetchingOrgPreferences,
|
isFetchingOrgPreferences,
|
||||||
activeLicenseV3,
|
activeLicense,
|
||||||
isFetchingActiveLicenseV3,
|
isFetchingActiveLicense,
|
||||||
trialInfo,
|
trialInfo,
|
||||||
featureFlags,
|
featureFlags,
|
||||||
} = useAppContext();
|
} = useAppContext();
|
||||||
@ -145,16 +145,16 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isFetchingActiveLicenseV3 && activeLicenseV3) {
|
if (!isFetchingActiveLicense && activeLicense) {
|
||||||
const currentRoute = mapRoutes.get('current');
|
const currentRoute = mapRoutes.get('current');
|
||||||
|
|
||||||
const isTerminated = activeLicenseV3.state === LicenseState.TERMINATED;
|
const isTerminated = activeLicense.state === LicenseState.TERMINATED;
|
||||||
const isExpired = activeLicenseV3.state === LicenseState.EXPIRED;
|
const isExpired = activeLicense.state === LicenseState.EXPIRED;
|
||||||
const isCancelled = activeLicenseV3.state === LicenseState.CANCELLED;
|
const isCancelled = activeLicense.state === LicenseState.CANCELLED;
|
||||||
|
|
||||||
const isWorkspaceAccessRestricted = isTerminated || isExpired || isCancelled;
|
const isWorkspaceAccessRestricted = isTerminated || isExpired || isCancelled;
|
||||||
|
|
||||||
const { platform } = activeLicenseV3;
|
const { platform } = activeLicense;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isWorkspaceAccessRestricted &&
|
isWorkspaceAccessRestricted &&
|
||||||
@ -164,26 +164,26 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
navigateToWorkSpaceAccessRestricted(currentRoute);
|
navigateToWorkSpaceAccessRestricted(currentRoute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isFetchingActiveLicenseV3, activeLicenseV3, mapRoutes, pathname]);
|
}, [isFetchingActiveLicense, activeLicense, mapRoutes, pathname]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isFetchingActiveLicenseV3) {
|
if (!isFetchingActiveLicense) {
|
||||||
const currentRoute = mapRoutes.get('current');
|
const currentRoute = mapRoutes.get('current');
|
||||||
const shouldBlockWorkspace = trialInfo?.workSpaceBlock;
|
const shouldBlockWorkspace = trialInfo?.workSpaceBlock;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
shouldBlockWorkspace &&
|
shouldBlockWorkspace &&
|
||||||
currentRoute &&
|
currentRoute &&
|
||||||
activeLicenseV3?.platform === LicensePlatform.CLOUD
|
activeLicense?.platform === LicensePlatform.CLOUD
|
||||||
) {
|
) {
|
||||||
navigateToWorkSpaceBlocked(currentRoute);
|
navigateToWorkSpaceBlocked(currentRoute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [
|
}, [
|
||||||
isFetchingActiveLicenseV3,
|
isFetchingActiveLicense,
|
||||||
trialInfo?.workSpaceBlock,
|
trialInfo?.workSpaceBlock,
|
||||||
activeLicenseV3?.platform,
|
activeLicense?.platform,
|
||||||
mapRoutes,
|
mapRoutes,
|
||||||
pathname,
|
pathname,
|
||||||
]);
|
]);
|
||||||
@ -197,20 +197,20 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isFetchingActiveLicenseV3 && activeLicenseV3) {
|
if (!isFetchingActiveLicense && activeLicense) {
|
||||||
const currentRoute = mapRoutes.get('current');
|
const currentRoute = mapRoutes.get('current');
|
||||||
const shouldSuspendWorkspace =
|
const shouldSuspendWorkspace =
|
||||||
activeLicenseV3.state === LicenseState.DEFAULTED;
|
activeLicense.state === LicenseState.DEFAULTED;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
shouldSuspendWorkspace &&
|
shouldSuspendWorkspace &&
|
||||||
currentRoute &&
|
currentRoute &&
|
||||||
activeLicenseV3.platform === LicensePlatform.CLOUD
|
activeLicense.platform === LicensePlatform.CLOUD
|
||||||
) {
|
) {
|
||||||
navigateToWorkSpaceSuspended(currentRoute);
|
navigateToWorkSpaceSuspended(currentRoute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isFetchingActiveLicenseV3, activeLicenseV3, mapRoutes, pathname]);
|
}, [isFetchingActiveLicense, activeLicense, mapRoutes, pathname]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (org && org.length > 0 && org[0].id !== undefined) {
|
if (org && org.length > 0 && org[0].id !== undefined) {
|
||||||
|
|||||||
@ -13,9 +13,9 @@ import AppLayout from 'container/AppLayout';
|
|||||||
import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys';
|
import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||||
import { useThemeConfig } from 'hooks/useDarkMode';
|
import { useThemeConfig } from 'hooks/useDarkMode';
|
||||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||||
import { LICENSE_PLAN_KEY } from 'hooks/useLicense';
|
|
||||||
import { NotificationProvider } from 'hooks/useNotifications';
|
import { NotificationProvider } from 'hooks/useNotifications';
|
||||||
import { ResourceProvider } from 'hooks/useResourceAttribute';
|
import { ResourceProvider } from 'hooks/useResourceAttribute';
|
||||||
|
import { StatusCodes } from 'http-status-codes';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
import posthog from 'posthog-js';
|
import posthog from 'posthog-js';
|
||||||
@ -23,6 +23,7 @@ import AlertRuleProvider from 'providers/Alert';
|
|||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { IUser } from 'providers/App/types';
|
import { IUser } from 'providers/App/types';
|
||||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||||
|
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
|
||||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||||
import { Suspense, useCallback, useEffect, useState } from 'react';
|
import { Suspense, useCallback, useEffect, useState } from 'react';
|
||||||
import { Route, Router, Switch } from 'react-router-dom';
|
import { Route, Router, Switch } from 'react-router-dom';
|
||||||
@ -41,14 +42,13 @@ import defaultRoutes, {
|
|||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
const themeConfig = useThemeConfig();
|
const themeConfig = useThemeConfig();
|
||||||
const {
|
const {
|
||||||
licenses,
|
|
||||||
user,
|
user,
|
||||||
isFetchingUser,
|
isFetchingUser,
|
||||||
isFetchingLicenses,
|
|
||||||
isFetchingFeatureFlags,
|
isFetchingFeatureFlags,
|
||||||
trialInfo,
|
trialInfo,
|
||||||
activeLicenseV3,
|
activeLicense,
|
||||||
isFetchingActiveLicenseV3,
|
isFetchingActiveLicense,
|
||||||
|
activeLicenseFetchError,
|
||||||
userFetchError,
|
userFetchError,
|
||||||
featureFlagsFetchError,
|
featureFlagsFetchError,
|
||||||
isLoggedIn: isLoggedInState,
|
isLoggedIn: isLoggedInState,
|
||||||
@ -66,7 +66,7 @@ function App(): JSX.Element {
|
|||||||
const enableAnalytics = useCallback(
|
const enableAnalytics = useCallback(
|
||||||
(user: IUser): void => {
|
(user: IUser): void => {
|
||||||
// wait for the required data to be loaded before doing init for anything!
|
// wait for the required data to be loaded before doing init for anything!
|
||||||
if (!isFetchingActiveLicenseV3 && activeLicenseV3 && org) {
|
if (!isFetchingActiveLicense && activeLicense && org) {
|
||||||
const orgName =
|
const orgName =
|
||||||
org && Array.isArray(org) && org.length > 0 ? org[0].displayName : '';
|
org && Array.isArray(org) && org.length > 0 ? org[0].displayName : '';
|
||||||
|
|
||||||
@ -153,8 +153,8 @@ function App(): JSX.Element {
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
hostname,
|
hostname,
|
||||||
isFetchingActiveLicenseV3,
|
isFetchingActiveLicense,
|
||||||
activeLicenseV3,
|
activeLicense,
|
||||||
org,
|
org,
|
||||||
trialInfo?.trialConvertedToSubscription,
|
trialInfo?.trialConvertedToSubscription,
|
||||||
],
|
],
|
||||||
@ -163,18 +163,17 @@ function App(): JSX.Element {
|
|||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!isFetchingLicenses &&
|
!isFetchingActiveLicense &&
|
||||||
licenses &&
|
(activeLicense || activeLicenseFetchError) &&
|
||||||
!isFetchingUser &&
|
!isFetchingUser &&
|
||||||
user &&
|
user &&
|
||||||
!!user.email
|
!!user.email
|
||||||
) {
|
) {
|
||||||
const isOnBasicPlan =
|
const isOnBasicPlan =
|
||||||
licenses.licenses?.some(
|
activeLicenseFetchError &&
|
||||||
(license) =>
|
[StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes(
|
||||||
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
|
activeLicenseFetchError?.getHttpStatusCode(),
|
||||||
) || licenses.licenses === null;
|
);
|
||||||
|
|
||||||
const isIdentifiedUser = getLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER);
|
const isIdentifiedUser = getLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER);
|
||||||
|
|
||||||
if (isLoggedInState && user && user.id && user.email && !isIdentifiedUser) {
|
if (isLoggedInState && user && user.id && user.email && !isIdentifiedUser) {
|
||||||
@ -204,11 +203,12 @@ function App(): JSX.Element {
|
|||||||
}, [
|
}, [
|
||||||
isLoggedInState,
|
isLoggedInState,
|
||||||
user,
|
user,
|
||||||
licenses,
|
|
||||||
isCloudUser,
|
isCloudUser,
|
||||||
isEnterpriseSelfHostedUser,
|
isEnterpriseSelfHostedUser,
|
||||||
isFetchingLicenses,
|
isFetchingActiveLicense,
|
||||||
isFetchingUser,
|
isFetchingUser,
|
||||||
|
activeLicense,
|
||||||
|
activeLicenseFetchError,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -231,8 +231,7 @@ function App(): JSX.Element {
|
|||||||
if (
|
if (
|
||||||
!isFetchingFeatureFlags &&
|
!isFetchingFeatureFlags &&
|
||||||
(featureFlags || featureFlagsFetchError) &&
|
(featureFlags || featureFlagsFetchError) &&
|
||||||
licenses &&
|
activeLicense &&
|
||||||
activeLicenseV3 &&
|
|
||||||
trialInfo
|
trialInfo
|
||||||
) {
|
) {
|
||||||
let isChatSupportEnabled = false;
|
let isChatSupportEnabled = false;
|
||||||
@ -270,8 +269,7 @@ function App(): JSX.Element {
|
|||||||
featureFlags,
|
featureFlags,
|
||||||
isFetchingFeatureFlags,
|
isFetchingFeatureFlags,
|
||||||
featureFlagsFetchError,
|
featureFlagsFetchError,
|
||||||
licenses,
|
activeLicense,
|
||||||
activeLicenseV3,
|
|
||||||
trialInfo,
|
trialInfo,
|
||||||
isCloudUser,
|
isCloudUser,
|
||||||
isEnterpriseSelfHostedUser,
|
isEnterpriseSelfHostedUser,
|
||||||
@ -333,7 +331,7 @@ function App(): JSX.Element {
|
|||||||
// if the user is in logged in state
|
// if the user is in logged in state
|
||||||
if (isLoggedInState) {
|
if (isLoggedInState) {
|
||||||
// if the setup calls are loading then return a spinner
|
// if the setup calls are loading then return a spinner
|
||||||
if (isFetchingLicenses || isFetchingUser || isFetchingFeatureFlags) {
|
if (isFetchingActiveLicense || isFetchingUser || isFetchingFeatureFlags) {
|
||||||
return <Spinner tip="Loading..." />;
|
return <Spinner tip="Loading..." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,7 +343,11 @@ function App(): JSX.Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if all of the data is not set then return a spinner, this is required because there is some gap between loading states and data setting
|
// if all of the data is not set then return a spinner, this is required because there is some gap between loading states and data setting
|
||||||
if ((!licenses || !user.email || !featureFlags) && !userFetchError) {
|
if (
|
||||||
|
(!activeLicense || !user.email || !featureFlags) &&
|
||||||
|
!userFetchError &&
|
||||||
|
!activeLicenseFetchError
|
||||||
|
) {
|
||||||
return <Spinner tip="Loading..." />;
|
return <Spinner tip="Loading..." />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -357,6 +359,7 @@ function App(): JSX.Element {
|
|||||||
<CompatRouter>
|
<CompatRouter>
|
||||||
<UserpilotRouteTracker />
|
<UserpilotRouteTracker />
|
||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
|
<ErrorModalProvider>
|
||||||
<PrivateRoute>
|
<PrivateRoute>
|
||||||
<ResourceProvider>
|
<ResourceProvider>
|
||||||
<QueryBuilderProvider>
|
<QueryBuilderProvider>
|
||||||
@ -385,6 +388,7 @@ function App(): JSX.Element {
|
|||||||
</QueryBuilderProvider>
|
</QueryBuilderProvider>
|
||||||
</ResourceProvider>
|
</ResourceProvider>
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
|
</ErrorModalProvider>
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</CompatRouter>
|
</CompatRouter>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@ -1,24 +0,0 @@
|
|||||||
import axios from 'api';
|
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
|
||||||
import { PayloadProps, Props } from 'types/api/SAML/deleteDomain';
|
|
||||||
|
|
||||||
const deleteDomain = async (
|
|
||||||
props: Props,
|
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.delete(`/domains/${props.id}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
error: null,
|
|
||||||
message: response.data.status,
|
|
||||||
payload: response.data.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default deleteDomain;
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import axios from 'api';
|
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
|
||||||
import { PayloadProps, Props } from 'types/api/SAML/listDomain';
|
|
||||||
|
|
||||||
const listAllDomain = async (
|
|
||||||
props: Props,
|
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`/orgs/${props.orgId}/domains`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
error: null,
|
|
||||||
message: response.data.status,
|
|
||||||
payload: response.data.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default listAllDomain;
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import axios from 'api';
|
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
|
||||||
import { PayloadProps, Props } from 'types/api/SAML/postDomain';
|
|
||||||
|
|
||||||
const postDomain = async (
|
|
||||||
props: Props,
|
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post(`/domains`, props);
|
|
||||||
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
error: null,
|
|
||||||
message: response.data.status,
|
|
||||||
payload: response.data.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default postDomain;
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import axios from 'api';
|
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
|
||||||
import { PayloadProps, Props } from 'types/api/SAML/updateDomain';
|
|
||||||
|
|
||||||
const updateDomain = async (
|
|
||||||
props: Props,
|
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.put(`/domains/${props.id}`, props);
|
|
||||||
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
error: null,
|
|
||||||
message: response.data.status,
|
|
||||||
payload: response.data.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default updateDomain;
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
import axios from 'api';
|
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
|
||||||
import {
|
|
||||||
CheckoutRequestPayloadProps,
|
|
||||||
CheckoutSuccessPayloadProps,
|
|
||||||
} from 'types/api/billing/checkout';
|
|
||||||
|
|
||||||
const updateCreditCardApi = async (
|
|
||||||
props: CheckoutRequestPayloadProps,
|
|
||||||
): Promise<SuccessResponse<CheckoutSuccessPayloadProps> | ErrorResponse> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('/checkout', {
|
|
||||||
url: props.url,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
error: null,
|
|
||||||
message: response.data.status,
|
|
||||||
payload: response.data.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default updateCreditCardApi;
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
import axios from 'api';
|
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
|
||||||
import {
|
|
||||||
CheckoutRequestPayloadProps,
|
|
||||||
CheckoutSuccessPayloadProps,
|
|
||||||
} from 'types/api/billing/checkout';
|
|
||||||
|
|
||||||
const manageCreditCardApi = async (
|
|
||||||
props: CheckoutRequestPayloadProps,
|
|
||||||
): Promise<SuccessResponse<CheckoutSuccessPayloadProps> | ErrorResponse> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('/portal', {
|
|
||||||
url: props.url,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
error: null,
|
|
||||||
message: response.data.status,
|
|
||||||
payload: response.data.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default manageCreditCardApi;
|
|
||||||
@ -4,7 +4,11 @@
|
|||||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||||
import loginApi from 'api/v1/login/login';
|
import loginApi from 'api/v1/login/login';
|
||||||
import afterLogin from 'AppRoutes/utils';
|
import afterLogin from 'AppRoutes/utils';
|
||||||
import axios, { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
import axios, {
|
||||||
|
AxiosError,
|
||||||
|
AxiosResponse,
|
||||||
|
InternalAxiosRequestConfig,
|
||||||
|
} from 'axios';
|
||||||
import { ENVIRONMENT } from 'constants/env';
|
import { ENVIRONMENT } from 'constants/env';
|
||||||
import { Events } from 'constants/events';
|
import { Events } from 'constants/events';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
@ -83,6 +87,7 @@ const interceptorRejected = async (
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
const reResponse = await axios(
|
const reResponse = await axios(
|
||||||
`${value.config.baseURL}${value.config.url?.substring(1)}`,
|
`${value.config.baseURL}${value.config.url?.substring(1)}`,
|
||||||
{
|
{
|
||||||
@ -96,11 +101,13 @@ const interceptorRejected = async (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (reResponse.status === 200) {
|
|
||||||
return await Promise.resolve(reResponse);
|
return await Promise.resolve(reResponse);
|
||||||
}
|
} catch (error) {
|
||||||
|
if ((error as AxiosError)?.response?.status === 401) {
|
||||||
Logout();
|
Logout();
|
||||||
return await Promise.reject(reResponse);
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logout();
|
Logout();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
import { ApiV3Instance as axios } from 'api';
|
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
|
||||||
import { PayloadProps, Props } from 'types/api/licenses/apply';
|
|
||||||
|
|
||||||
const apply = async (
|
|
||||||
props: Props,
|
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post('/licenses', {
|
|
||||||
key: props.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
error: null,
|
|
||||||
message: response.data.status,
|
|
||||||
payload: response.data.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default apply;
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import { ApiV3Instance as axios } from 'api';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
|
||||||
import { PayloadProps } from 'types/api/licenses/getAll';
|
|
||||||
|
|
||||||
const getAll = async (): Promise<
|
|
||||||
SuccessResponse<PayloadProps> | ErrorResponse
|
|
||||||
> => {
|
|
||||||
const response = await axios.get('/licenses');
|
|
||||||
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
error: null,
|
|
||||||
message: response.data.status,
|
|
||||||
payload: response.data.data,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getAll;
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import { ApiV3Instance as axios } from 'api';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
|
||||||
import { LicenseV3EventQueueResModel } from 'types/api/licensesV3/getActive';
|
|
||||||
|
|
||||||
const getActive = async (): Promise<
|
|
||||||
SuccessResponse<LicenseV3EventQueueResModel> | ErrorResponse
|
|
||||||
> => {
|
|
||||||
const response = await axios.get('/licenses/active');
|
|
||||||
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
error: null,
|
|
||||||
message: response.data.status,
|
|
||||||
payload: response.data.data,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getActive;
|
|
||||||
28
frontend/src/api/v1/checkout/create.ts
Normal file
28
frontend/src/api/v1/checkout/create.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||||
|
import {
|
||||||
|
CheckoutRequestPayloadProps,
|
||||||
|
CheckoutSuccessPayloadProps,
|
||||||
|
PayloadProps,
|
||||||
|
} from 'types/api/billing/checkout';
|
||||||
|
|
||||||
|
const updateCreditCardApi = async (
|
||||||
|
props: CheckoutRequestPayloadProps,
|
||||||
|
): Promise<SuccessResponseV2<CheckoutSuccessPayloadProps>> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post<PayloadProps>('/checkout', {
|
||||||
|
url: props.url,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default updateCreditCardApi;
|
||||||
21
frontend/src/api/v1/domains/create.ts
Normal file
21
frontend/src/api/v1/domains/create.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||||
|
import { AuthDomain } from 'types/api/SAML/listDomain';
|
||||||
|
import { PayloadProps, Props } from 'types/api/SAML/postDomain';
|
||||||
|
|
||||||
|
const create = async (props: Props): Promise<SuccessResponseV2<AuthDomain>> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post<PayloadProps>(`/domains`, props);
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default create;
|
||||||
20
frontend/src/api/v1/domains/delete.ts
Normal file
20
frontend/src/api/v1/domains/delete.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||||
|
import { PayloadProps, Props } from 'types/api/SAML/deleteDomain';
|
||||||
|
|
||||||
|
const deleteDomain = async (props: Props): Promise<SuccessResponseV2<null>> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete<PayloadProps>(`/domains/${props.id}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default deleteDomain;
|
||||||
20
frontend/src/api/v1/domains/list.ts
Normal file
20
frontend/src/api/v1/domains/list.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||||
|
import { AuthDomain, PayloadProps } from 'types/api/SAML/listDomain';
|
||||||
|
|
||||||
|
const listAllDomain = async (): Promise<SuccessResponseV2<AuthDomain[]>> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<PayloadProps>(`/domains`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default listAllDomain;
|
||||||
23
frontend/src/api/v1/domains/update.ts
Normal file
23
frontend/src/api/v1/domains/update.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||||
|
import { AuthDomain } from 'types/api/SAML/listDomain';
|
||||||
|
import { PayloadProps, Props } from 'types/api/SAML/updateDomain';
|
||||||
|
|
||||||
|
const updateDomain = async (
|
||||||
|
props: Props,
|
||||||
|
): Promise<SuccessResponseV2<AuthDomain>> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.put<PayloadProps>(`/domains/${props.id}`, props);
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default updateDomain;
|
||||||
28
frontend/src/api/v1/portal/create.ts
Normal file
28
frontend/src/api/v1/portal/create.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||||
|
import {
|
||||||
|
CheckoutRequestPayloadProps,
|
||||||
|
CheckoutSuccessPayloadProps,
|
||||||
|
PayloadProps,
|
||||||
|
} from 'types/api/billing/checkout';
|
||||||
|
|
||||||
|
const manageCreditCardApi = async (
|
||||||
|
props: CheckoutRequestPayloadProps,
|
||||||
|
): Promise<SuccessResponseV2<CheckoutSuccessPayloadProps>> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post<PayloadProps>('/portal', {
|
||||||
|
url: props.url,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default manageCreditCardApi;
|
||||||
25
frontend/src/api/v3/licenses/active/get.ts
Normal file
25
frontend/src/api/v3/licenses/active/get.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { ApiV3Instance as axios } from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||||
|
import {
|
||||||
|
LicenseEventQueueResModel,
|
||||||
|
PayloadProps,
|
||||||
|
} from 'types/api/licensesV3/getActive';
|
||||||
|
|
||||||
|
const getActive = async (): Promise<
|
||||||
|
SuccessResponseV2<LicenseEventQueueResModel>
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<PayloadProps>('/licenses/active');
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getActive;
|
||||||
24
frontend/src/api/v3/licenses/put.ts
Normal file
24
frontend/src/api/v3/licenses/put.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { ApiV3Instance as axios } from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||||
|
import { PayloadProps, Props } from 'types/api/licenses/apply';
|
||||||
|
|
||||||
|
const apply = async (
|
||||||
|
props: Props,
|
||||||
|
): Promise<SuccessResponseV2<PayloadProps>> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post<PayloadProps>('/licenses', {
|
||||||
|
key: props.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default apply;
|
||||||
191
frontend/src/assets/Error.tsx
Normal file
191
frontend/src/assets/Error.tsx
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type ErrorIconProps = React.SVGProps<SVGSVGElement>;
|
||||||
|
|
||||||
|
function ErrorIcon({ ...props }: ErrorIconProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#C62828"
|
||||||
|
d="M1.281 5.78a.922.922 0 0 0-.92.921v4.265a.922.922 0 0 0 .92.92h.617V5.775l-.617.005ZM12.747 5.78c.508 0 .92.413.92.92v4.264a.923.923 0 0 1-.92.922h-.617V5.775l.617.004Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#90A4AE"
|
||||||
|
d="M12.463 5.931 12.45 4.82a.867.867 0 0 0-.87-.861H7.34v-1.49a1.083 1.083 0 1 0-.68 0v1.496H2.42a.867.867 0 0 0-.864.86v7.976c.003.475.389.86.865.862h9.16a.868.868 0 0 0 .869-.862v-.82h.013v-6.05Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#C62828"
|
||||||
|
d="M7 1.885a.444.444 0 1 1 0-.888.444.444 0 0 1 0 .888Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="url(#a)"
|
||||||
|
d="M4.795 10.379h4.412c.384 0 .696.312.696.697v.063a.697.697 0 0 1-.696.697H4.795a.697.697 0 0 1-.697-.697v-.063c0-.385.312-.697.697-.697Z"
|
||||||
|
/>
|
||||||
|
<path fill="url(#b)" d="M6.115 10.38h-.262v1.455h.262V10.38Z" />
|
||||||
|
<path fill="url(#c)" d="M7.138 10.38h-.262v1.455h.262V10.38Z" />
|
||||||
|
<path fill="url(#d)" d="M8.147 10.38h-.262v1.455h.262V10.38Z" />
|
||||||
|
<path fill="url(#e)" d="M9.22 10.38h-.262v1.455h.262V10.38Z" />
|
||||||
|
<path fill="url(#f)" d="M5.042 10.379H4.78v1.454h.262V10.38Z" />
|
||||||
|
<path
|
||||||
|
fill="#C62828"
|
||||||
|
d="M7 9.367h-.593a.111.111 0 0 1-.098-.162l.304-.6.288-.532a.11.11 0 0 1 .195 0l.29.556.301.576a.11.11 0 0 1-.098.162H7Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="url(#g)"
|
||||||
|
d="M4.627 8.587a1.278 1.278 0 1 0 0-2.556 1.278 1.278 0 0 0 0 2.556Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="url(#h)"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M4.627 6.142a1.167 1.167 0 1 0 0 2.333 1.167 1.167 0 0 0 0-2.333ZM3.237 7.31a1.389 1.389 0 1 1 2.778 0 1.389 1.389 0 0 1-2.777 0Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="url(#i)"
|
||||||
|
d="M9.333 6.028a1.278 1.278 0 1 0 0 2.556 1.278 1.278 0 0 0 0-2.556Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="url(#j)"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M7.944 7.306a1.39 1.39 0 0 1 2.778 0 1.389 1.389 0 0 1-2.778 0Zm1.39-1.167a1.167 1.167 0 1 0 0 2.334 1.167 1.167 0 0 0 0-2.334Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id="a"
|
||||||
|
x1="7.001"
|
||||||
|
x2="7.001"
|
||||||
|
y1="11.836"
|
||||||
|
y2="10.379"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset=".12" stopColor="#E0E0E0" />
|
||||||
|
<stop offset=".52" stopColor="#fff" />
|
||||||
|
<stop offset="1" stopColor="#EAEAEA" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="b"
|
||||||
|
x1="5.984"
|
||||||
|
x2="5.984"
|
||||||
|
y1="11.835"
|
||||||
|
y2="10.381"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stopColor="#333" />
|
||||||
|
<stop offset=".55" stopColor="#666" />
|
||||||
|
<stop offset="1" stopColor="#333" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="c"
|
||||||
|
x1="7.007"
|
||||||
|
x2="7.007"
|
||||||
|
y1="11.835"
|
||||||
|
y2="10.381"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stopColor="#333" />
|
||||||
|
<stop offset=".55" stopColor="#666" />
|
||||||
|
<stop offset="1" stopColor="#333" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="d"
|
||||||
|
x1="8.016"
|
||||||
|
x2="8.016"
|
||||||
|
y1="11.835"
|
||||||
|
y2="10.381"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stopColor="#333" />
|
||||||
|
<stop offset=".55" stopColor="#666" />
|
||||||
|
<stop offset="1" stopColor="#333" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="e"
|
||||||
|
x1="9.089"
|
||||||
|
x2="9.089"
|
||||||
|
y1="11.835"
|
||||||
|
y2="10.381"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stopColor="#333" />
|
||||||
|
<stop offset=".55" stopColor="#666" />
|
||||||
|
<stop offset="1" stopColor="#333" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="f"
|
||||||
|
x1="4.911"
|
||||||
|
x2="4.911"
|
||||||
|
y1="11.833"
|
||||||
|
y2="10.379"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stopColor="#333" />
|
||||||
|
<stop offset=".55" stopColor="#666" />
|
||||||
|
<stop offset="1" stopColor="#333" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="h"
|
||||||
|
x1="3.238"
|
||||||
|
x2="6.015"
|
||||||
|
y1="7.309"
|
||||||
|
y2="7.309"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stopColor="#333" />
|
||||||
|
<stop offset=".55" stopColor="#666" />
|
||||||
|
<stop offset="1" stopColor="#333" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="j"
|
||||||
|
x1="7.939"
|
||||||
|
x2="10.716"
|
||||||
|
y1="7.306"
|
||||||
|
y2="7.306"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stopColor="#333" />
|
||||||
|
<stop offset=".55" stopColor="#666" />
|
||||||
|
<stop offset="1" stopColor="#333" />
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient
|
||||||
|
id="g"
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="1"
|
||||||
|
gradientTransform="matrix(1.27771 0 0 1.2777 4.627 7.309)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset=".48" stopColor="#fff" />
|
||||||
|
<stop offset=".77" stopColor="#FDFDFD" />
|
||||||
|
<stop offset=".88" stopColor="#F6F6F6" />
|
||||||
|
<stop offset=".96" stopColor="#EBEBEB" />
|
||||||
|
<stop offset="1" stopColor="#E0E0E0" />
|
||||||
|
</radialGradient>
|
||||||
|
<radialGradient
|
||||||
|
id="i"
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="1"
|
||||||
|
gradientTransform="matrix(1.27771 0 0 1.2777 9.328 7.306)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset=".48" stopColor="#fff" />
|
||||||
|
<stop offset=".77" stopColor="#FDFDFD" />
|
||||||
|
<stop offset=".88" stopColor="#F6F6F6" />
|
||||||
|
<stop offset=".96" stopColor="#EBEBEB" />
|
||||||
|
<stop offset="1" stopColor="#E0E0E0" />
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorIcon;
|
||||||
@ -1,14 +1,14 @@
|
|||||||
import { Button, Modal, Typography } from 'antd';
|
import { Button, Modal, Typography } from 'antd';
|
||||||
import updateCreditCardApi from 'api/billing/checkout';
|
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
import updateCreditCardApi from 'api/v1/checkout/create';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { CreditCard, X } from 'lucide-react';
|
import { CreditCard, X } from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useMutation } from 'react-query';
|
import { useMutation } from 'react-query';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { SuccessResponseV2 } from 'types/api';
|
||||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
|
|
||||||
export default function ChatSupportGateway(): JSX.Element {
|
export default function ChatSupportGateway(): JSX.Element {
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
@ -18,20 +18,21 @@ export default function ChatSupportGateway(): JSX.Element {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleBillingOnSuccess = (
|
const handleBillingOnSuccess = (
|
||||||
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
|
data: SuccessResponseV2<CheckoutSuccessPayloadProps>,
|
||||||
): void => {
|
): void => {
|
||||||
if (data?.payload?.redirectURL) {
|
if (data?.data?.redirectURL) {
|
||||||
const newTab = document.createElement('a');
|
const newTab = document.createElement('a');
|
||||||
newTab.href = data.payload.redirectURL;
|
newTab.href = data.data.redirectURL;
|
||||||
newTab.target = '_blank';
|
newTab.target = '_blank';
|
||||||
newTab.rel = 'noopener noreferrer';
|
newTab.rel = 'noopener noreferrer';
|
||||||
newTab.click();
|
newTab.click();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBillingOnError = (): void => {
|
const handleBillingOnError = (error: APIError): void => {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: SOMETHING_WENT_WRONG,
|
message: error.getErrorCode(),
|
||||||
|
description: error.getErrorMessage(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
118
frontend/src/components/ErrorModal/ErrorModal.styles.scss
Normal file
118
frontend/src/components/ErrorModal/ErrorModal.styles.scss
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
.error-modal {
|
||||||
|
&__trigger {
|
||||||
|
width: fit-content;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: rgba(229, 72, 77, 0.2);
|
||||||
|
padding-left: 3px;
|
||||||
|
padding-right: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
span {
|
||||||
|
color: var(--bg-cherry-500);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px; /* 200% */
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__wrap {
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(11, 12, 14, 0.12) 0.07%,
|
||||||
|
rgba(39, 8, 14, 0.24) 50.04%,
|
||||||
|
rgba(106, 29, 44, 0.36) 75.02%,
|
||||||
|
rgba(197, 57, 85, 0.48) 87.51%,
|
||||||
|
rgba(242, 71, 105, 0.6) 100%
|
||||||
|
);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
.ant-modal {
|
||||||
|
bottom: 40px;
|
||||||
|
top: unset;
|
||||||
|
position: absolute;
|
||||||
|
width: 520px;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__body {
|
||||||
|
padding: 0;
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
overflow: hidden;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}
|
||||||
|
&__header {
|
||||||
|
background: none !important;
|
||||||
|
.ant-modal-title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.key-value-label {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
&__key,
|
||||||
|
&__value {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 16px;
|
||||||
|
letter-spacing: 0.48px;
|
||||||
|
}
|
||||||
|
&__key {
|
||||||
|
text-transform: uppercase;
|
||||||
|
&,
|
||||||
|
&:hover {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__value {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.close-button {
|
||||||
|
padding: 3px 7px;
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--bg-slate-500);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__footer {
|
||||||
|
margin: 0 !important;
|
||||||
|
height: 6px;
|
||||||
|
background: var(--bg-sakura-500);
|
||||||
|
}
|
||||||
|
&__content {
|
||||||
|
padding: 0 !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.error-modal {
|
||||||
|
&__body,
|
||||||
|
&__header .close-button {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
&__header .close-button {
|
||||||
|
svg {
|
||||||
|
fill: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
195
frontend/src/components/ErrorModal/ErrorModal.test.tsx
Normal file
195
frontend/src/components/ErrorModal/ErrorModal.test.tsx
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
|
|
||||||
|
import ErrorModal from './ErrorModal';
|
||||||
|
|
||||||
|
// Mock the query client to return version data
|
||||||
|
const mockVersionData = {
|
||||||
|
payload: {
|
||||||
|
ee: 'Y',
|
||||||
|
version: '1.0.0',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
jest.mock('react-query', () => ({
|
||||||
|
...jest.requireActual('react-query'),
|
||||||
|
useQueryClient: (): { getQueryData: () => typeof mockVersionData } => ({
|
||||||
|
getQueryData: jest.fn(() => mockVersionData),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
const mockError: APIError = new APIError({
|
||||||
|
httpStatusCode: 400,
|
||||||
|
error: {
|
||||||
|
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||||
|
message: 'Something went wrong while processing your request.',
|
||||||
|
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||||
|
code: 'An error occurred',
|
||||||
|
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||||
|
url: 'https://example.com/docs',
|
||||||
|
errors: [
|
||||||
|
{ message: 'First error detail' },
|
||||||
|
{ message: 'Second error detail' },
|
||||||
|
{ message: 'Third error detail' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
describe('ErrorModal Component', () => {
|
||||||
|
it('should render the modal when open is true', () => {
|
||||||
|
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
|
||||||
|
|
||||||
|
// Check if the error message is displayed
|
||||||
|
expect(screen.getByText('An error occurred')).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText('Something went wrong while processing your request.'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render the modal when open is false', () => {
|
||||||
|
render(<ErrorModal error={mockError} open={false} onClose={jest.fn()} />);
|
||||||
|
|
||||||
|
// Check that the modal content is not in the document
|
||||||
|
expect(screen.queryByText('An error occurred')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onClose when the close button is clicked', async () => {
|
||||||
|
const onCloseMock = jest.fn();
|
||||||
|
render(<ErrorModal error={mockError} open onClose={onCloseMock} />);
|
||||||
|
|
||||||
|
// Click the close button
|
||||||
|
const closeButton = screen.getByTestId('close-button');
|
||||||
|
act(() => {
|
||||||
|
fireEvent.click(closeButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if onClose was called
|
||||||
|
expect(onCloseMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display version data if available', async () => {
|
||||||
|
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
|
||||||
|
|
||||||
|
// Check if the version data is displayed
|
||||||
|
expect(screen.getByText('ENTERPRISE')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('1.0.0')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('should render the messages count badge when there are multiple errors', () => {
|
||||||
|
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
|
||||||
|
|
||||||
|
// Check if the messages count badge is displayed
|
||||||
|
expect(screen.getByText('MESSAGES')).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText('3')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Check if the individual error messages are displayed
|
||||||
|
expect(screen.getByText('First error detail')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Second error detail')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Third error detail')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the open docs button when URL is provided', async () => {
|
||||||
|
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
|
||||||
|
|
||||||
|
// Check if the open docs button is displayed
|
||||||
|
const openDocsButton = screen.getByTestId('error-docs-button');
|
||||||
|
|
||||||
|
expect(openDocsButton).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(openDocsButton).toHaveAttribute('href', 'https://example.com/docs');
|
||||||
|
|
||||||
|
expect(openDocsButton).toHaveAttribute('target', '_blank');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display scroll for more if there are less than 10 messages', () => {
|
||||||
|
render(<ErrorModal error={mockError} open onClose={jest.fn()} />);
|
||||||
|
|
||||||
|
expect(screen.queryByText('Scroll for more')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('should display scroll for more if there are more than 10 messages', async () => {
|
||||||
|
const longError = new APIError({
|
||||||
|
httpStatusCode: 400,
|
||||||
|
error: {
|
||||||
|
...mockError.error,
|
||||||
|
code: 'An error occurred',
|
||||||
|
message: 'Something went wrong while processing your request.',
|
||||||
|
url: 'https://example.com/docs',
|
||||||
|
errors: Array.from({ length: 15 }, (_, i) => ({
|
||||||
|
message: `Error detail ${i + 1}`,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<ErrorModal error={longError} open onClose={jest.fn()} />);
|
||||||
|
|
||||||
|
// Check if the scroll hint is displayed
|
||||||
|
expect(screen.getByText('Scroll for more')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should render the trigger component if provided', () => {
|
||||||
|
const mockTrigger = <button type="button">Open Error Modal</button>;
|
||||||
|
render(
|
||||||
|
<ErrorModal
|
||||||
|
error={mockError}
|
||||||
|
triggerComponent={mockTrigger}
|
||||||
|
onClose={jest.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if the trigger component is rendered
|
||||||
|
expect(screen.getByText('Open Error Modal')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open the modal when the trigger component is clicked', async () => {
|
||||||
|
const mockTrigger = <button type="button">Open Error Modal</button>;
|
||||||
|
render(
|
||||||
|
<ErrorModal
|
||||||
|
error={mockError}
|
||||||
|
triggerComponent={mockTrigger}
|
||||||
|
onClose={jest.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Click the trigger component
|
||||||
|
const triggerButton = screen.getByText('Open Error Modal');
|
||||||
|
act(() => {
|
||||||
|
fireEvent.click(triggerButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if the modal is displayed
|
||||||
|
expect(screen.getByText('An error occurred')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the default trigger tag if no trigger component is provided', () => {
|
||||||
|
render(<ErrorModal error={mockError} onClose={jest.fn()} />);
|
||||||
|
|
||||||
|
// Check if the default trigger tag is rendered
|
||||||
|
expect(screen.getByText('error')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close the modal when the onCancel event is triggered', async () => {
|
||||||
|
const onCloseMock = jest.fn();
|
||||||
|
render(<ErrorModal error={mockError} onClose={onCloseMock} />);
|
||||||
|
|
||||||
|
// Click the trigger component
|
||||||
|
const triggerButton = screen.getByText('error');
|
||||||
|
act(() => {
|
||||||
|
fireEvent.click(triggerButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('An error occurred')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger the onCancel event
|
||||||
|
act(() => {
|
||||||
|
fireEvent.click(screen.getByTestId('close-button'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if the modal is closed
|
||||||
|
expect(onCloseMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// check if the modal is not visible
|
||||||
|
const modal = document.getElementsByClassName('ant-modal');
|
||||||
|
const style = window.getComputedStyle(modal[0]);
|
||||||
|
expect(style.display).toBe('none');
|
||||||
|
});
|
||||||
|
});
|
||||||
102
frontend/src/components/ErrorModal/ErrorModal.tsx
Normal file
102
frontend/src/components/ErrorModal/ErrorModal.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import './ErrorModal.styles.scss';
|
||||||
|
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Button, Modal, Tag } from 'antd';
|
||||||
|
import { CircleAlert, X } from 'lucide-react';
|
||||||
|
import KeyValueLabel from 'periscope/components/KeyValueLabel';
|
||||||
|
import { useAppContext } from 'providers/App/App';
|
||||||
|
import React from 'react';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
|
|
||||||
|
import ErrorContent from './components/ErrorContent';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
error: APIError;
|
||||||
|
triggerComponent?: React.ReactElement;
|
||||||
|
onClose?: () => void;
|
||||||
|
open?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const classNames = {
|
||||||
|
body: 'error-modal__body',
|
||||||
|
mask: 'error-modal__mask',
|
||||||
|
header: 'error-modal__header',
|
||||||
|
footer: 'error-modal__footer',
|
||||||
|
content: 'error-modal__content',
|
||||||
|
};
|
||||||
|
|
||||||
|
function ErrorModal({
|
||||||
|
open,
|
||||||
|
error,
|
||||||
|
triggerComponent,
|
||||||
|
onClose,
|
||||||
|
}: Props): JSX.Element {
|
||||||
|
const [visible, setVisible] = React.useState(open);
|
||||||
|
|
||||||
|
const handleClose = (): void => {
|
||||||
|
setVisible(false);
|
||||||
|
onClose?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const { versionData } = useAppContext();
|
||||||
|
|
||||||
|
const versionDataPayload = versionData;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!triggerComponent ? (
|
||||||
|
<Tag
|
||||||
|
className="error-modal__trigger"
|
||||||
|
icon={<CircleAlert size={14} color={Color.BG_CHERRY_500} />}
|
||||||
|
color="error"
|
||||||
|
onClick={(): void => setVisible(true)}
|
||||||
|
>
|
||||||
|
error
|
||||||
|
</Tag>
|
||||||
|
) : (
|
||||||
|
React.cloneElement(triggerComponent, {
|
||||||
|
onClick: () => setVisible(true),
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={visible}
|
||||||
|
footer={<div className="error-modal__footer" />}
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
{versionDataPayload ? (
|
||||||
|
<KeyValueLabel
|
||||||
|
badgeKey={versionDataPayload.ee === 'Y' ? 'ENTERPRISE' : 'COMMUNITY'}
|
||||||
|
badgeValue={versionDataPayload.version}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="error-modal__version-placeholder" />
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
className="close-button"
|
||||||
|
onClick={handleClose}
|
||||||
|
data-testid="close-button"
|
||||||
|
>
|
||||||
|
<X size={16} color={Color.BG_VANILLA_400} />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onCancel={handleClose}
|
||||||
|
closeIcon={false}
|
||||||
|
classNames={classNames}
|
||||||
|
wrapClassName="error-modal__wrap"
|
||||||
|
>
|
||||||
|
<ErrorContent error={error} />
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorModal.defaultProps = {
|
||||||
|
onClose: undefined,
|
||||||
|
triggerComponent: null,
|
||||||
|
open: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ErrorModal;
|
||||||
@ -0,0 +1,208 @@
|
|||||||
|
.error-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
// === SECTION: Summary (Top)
|
||||||
|
&__summary-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__summary {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__summary-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__summary-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error-code {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 24px; /* 150% */
|
||||||
|
letter-spacing: -0.08px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__error-message {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__docs-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 9px 12.5px;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 150% */
|
||||||
|
letter-spacing: 0.12px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__message-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0px 16px 16px;
|
||||||
|
|
||||||
|
.key-value-label {
|
||||||
|
width: fit-content;
|
||||||
|
border-color: var(--bg-slate-400);
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
&__key {
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
&__value {
|
||||||
|
padding-right: 10px;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 18px; /* 150% */
|
||||||
|
letter-spacing: 0.48px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
&-dot {
|
||||||
|
height: 6px;
|
||||||
|
width: 6px;
|
||||||
|
background: var(--bg-sakura-500);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
&-text {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 18px; /* 180% */
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-line {
|
||||||
|
flex: 1;
|
||||||
|
height: 8px;
|
||||||
|
background-image: radial-gradient(circle, #444c63 1px, transparent 2px);
|
||||||
|
background-size: 8px 11px;
|
||||||
|
background-position: top left;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === SECTION: Message List (Bottom)
|
||||||
|
|
||||||
|
&__message-list-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__message-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
max-height: 275px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__message-item {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Geist Mono;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
padding: 3px 12px;
|
||||||
|
padding-left: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__message-item::before {
|
||||||
|
font-family: unset;
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 2px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 50px;
|
||||||
|
background: var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__scroll-hint {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
margin: auto;
|
||||||
|
width: fit-content;
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 4px 12px 4px 10px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
background: var(--bg-slate-200);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0px 103px 12px 0px rgba(0, 0, 0, 0.01),
|
||||||
|
0px 66px 18px 0px rgba(0, 0, 0, 0.01), 0px 37px 22px 0px rgba(0, 0, 0, 0.03),
|
||||||
|
0px 17px 17px 0px rgba(0, 0, 0, 0.04), 0px 4px 9px 0px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__scroll-hint-text {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px;
|
||||||
|
letter-spacing: -0.06px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.error-content {
|
||||||
|
&__error-code {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
&__error-message {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
&__message-item {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
&__message-badge {
|
||||||
|
&-label-text {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
.key-value-label__value {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__docs-button {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
import './ErrorContent.styles.scss';
|
||||||
|
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import ErrorIcon from 'assets/Error';
|
||||||
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
|
import { BookOpenText, ChevronsDown } from 'lucide-react';
|
||||||
|
import KeyValueLabel from 'periscope/components/KeyValueLabel';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
|
|
||||||
|
interface ErrorContentProps {
|
||||||
|
error: APIError;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ErrorContent({ error }: ErrorContentProps): JSX.Element {
|
||||||
|
const {
|
||||||
|
url: errorUrl,
|
||||||
|
errors: errorMessages,
|
||||||
|
code: errorCode,
|
||||||
|
message: errorMessage,
|
||||||
|
} = error.error.error;
|
||||||
|
return (
|
||||||
|
<section className="error-content">
|
||||||
|
{/* Summary Header */}
|
||||||
|
<section className="error-content__summary-section">
|
||||||
|
<header className="error-content__summary">
|
||||||
|
<div className="error-content__summary-left">
|
||||||
|
<div className="error-content__icon-wrapper">
|
||||||
|
<ErrorIcon />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="error-content__summary-text">
|
||||||
|
<h2 className="error-content__error-code">{errorCode}</h2>
|
||||||
|
<p className="error-content__error-message">{errorMessage}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{errorUrl && (
|
||||||
|
<div className="error-content__summary-right">
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
className="error-content__docs-button"
|
||||||
|
href={errorUrl}
|
||||||
|
target="_blank"
|
||||||
|
data-testid="error-docs-button"
|
||||||
|
>
|
||||||
|
<BookOpenText size={14} />
|
||||||
|
Open Docs
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{errorMessages?.length > 0 && (
|
||||||
|
<div className="error-content__message-badge">
|
||||||
|
<KeyValueLabel
|
||||||
|
badgeKey={
|
||||||
|
<div className="error-content__message-badge-label">
|
||||||
|
<div className="error-content__message-badge-label-dot" />
|
||||||
|
<div className="error-content__message-badge-label-text">MESSAGES</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
badgeValue={errorMessages.length.toString()}
|
||||||
|
/>
|
||||||
|
<div className="error-content__message-badge-line" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Detailed Messages */}
|
||||||
|
<section className="error-content__messages-section">
|
||||||
|
<div className="error-content__message-list-container">
|
||||||
|
<OverlayScrollbar>
|
||||||
|
<ul className="error-content__message-list">
|
||||||
|
{errorMessages?.map((error) => (
|
||||||
|
<li className="error-content__message-item" key={error.message}>
|
||||||
|
{error.message}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</OverlayScrollbar>
|
||||||
|
{errorMessages?.length > 10 && (
|
||||||
|
<div className="error-content__scroll-hint">
|
||||||
|
<ChevronsDown
|
||||||
|
size={16}
|
||||||
|
color={Color.BG_VANILLA_100}
|
||||||
|
className="error-content__scroll-hint-icon"
|
||||||
|
/>
|
||||||
|
<span className="error-content__scroll-hint-text">Scroll for more</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorContent;
|
||||||
@ -37,6 +37,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import {
|
import {
|
||||||
@ -67,6 +68,7 @@ function HostMetricsDetails({
|
|||||||
AppState,
|
AppState,
|
||||||
GlobalReducer
|
GlobalReducer
|
||||||
>((state) => state.globalTime);
|
>((state) => state.globalTime);
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const startMs = useMemo(() => Math.floor(Number(minTime) / 1000000000), [
|
const startMs = useMemo(() => Math.floor(Number(minTime) / 1000000000), [
|
||||||
minTime,
|
minTime,
|
||||||
@ -86,7 +88,9 @@ function HostMetricsDetails({
|
|||||||
selectedTime as Time,
|
selectedTime as Time,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [selectedView, setSelectedView] = useState<VIEWS>(VIEWS.METRICS);
|
const [selectedView, setSelectedView] = useState<VIEWS>(
|
||||||
|
(searchParams.get('view') as VIEWS) || VIEWS.METRICS,
|
||||||
|
);
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const initialFilters = useMemo(
|
const initialFilters = useMemo(
|
||||||
@ -149,6 +153,9 @@ function HostMetricsDetails({
|
|||||||
|
|
||||||
const handleTabChange = (e: RadioChangeEvent): void => {
|
const handleTabChange = (e: RadioChangeEvent): void => {
|
||||||
setSelectedView(e.target.value);
|
setSelectedView(e.target.value);
|
||||||
|
if (host?.hostName) {
|
||||||
|
setSearchParams({ hostName: host?.hostName, view: e.target.value });
|
||||||
|
}
|
||||||
logEvent(InfraMonitoringEvents.TabChanged, {
|
logEvent(InfraMonitoringEvents.TabChanged, {
|
||||||
entity: InfraMonitoringEvents.HostEntity,
|
entity: InfraMonitoringEvents.HostEntity,
|
||||||
view: e.target.value,
|
view: e.target.value,
|
||||||
@ -313,6 +320,7 @@ function HostMetricsDetails({
|
|||||||
|
|
||||||
const handleClose = (): void => {
|
const handleClose = (): void => {
|
||||||
setSelectedInterval(selectedTime as Time);
|
setSelectedInterval(selectedTime as Time);
|
||||||
|
setSearchParams({});
|
||||||
|
|
||||||
if (selectedTime !== 'custom') {
|
if (selectedTime !== 'custom') {
|
||||||
const { maxTime, minTime } = GetMinMax(selectedTime);
|
const { maxTime, minTime } = GetMinMax(selectedTime);
|
||||||
|
|||||||
@ -75,7 +75,6 @@ function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
|
|||||||
const getItemContent = useCallback(
|
const getItemContent = useCallback(
|
||||||
(_: number, logToRender: ILog): JSX.Element => (
|
(_: number, logToRender: ILog): JSX.Element => (
|
||||||
<RawLogView
|
<RawLogView
|
||||||
isReadOnly
|
|
||||||
isTextOverflowEllipsisDisabled
|
isTextOverflowEllipsisDisabled
|
||||||
key={logToRender.id}
|
key={logToRender.id}
|
||||||
data={logToRender}
|
data={logToRender}
|
||||||
|
|||||||
@ -45,7 +45,7 @@ jest.mock(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
describe('HostMetricsLogs', () => {
|
describe.skip('HostMetricsLogs', () => {
|
||||||
let capturedQueryRangePayloads: QueryRangePayload[] = [];
|
let capturedQueryRangePayloads: QueryRangePayload[] = [];
|
||||||
const itemHeight = 100;
|
const itemHeight = 100;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@ -80,6 +80,7 @@ function Metrics({
|
|||||||
softMin: null,
|
softMin: null,
|
||||||
minTimeScale: timeRange.startTime,
|
minTimeScale: timeRange.startTime,
|
||||||
maxTimeScale: timeRange.endTime,
|
maxTimeScale: timeRange.endTime,
|
||||||
|
enableZoom: true,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
[queries, isDarkMode, dimensions, timeRange.startTime, timeRange.endTime],
|
[queries, isDarkMode, dimensions, timeRange.startTime, timeRange.endTime],
|
||||||
@ -115,7 +116,7 @@ function Metrics({
|
|||||||
<div className="metrics-header">
|
<div className="metrics-header">
|
||||||
<div className="metrics-datetime-section">
|
<div className="metrics-datetime-section">
|
||||||
<DateTimeSelectionV2
|
<DateTimeSelectionV2
|
||||||
showAutoRefresh={false}
|
showAutoRefresh
|
||||||
showRefreshText={false}
|
showRefreshText={false}
|
||||||
hideShareModal
|
hideShareModal
|
||||||
onTimeChange={handleTimeChange}
|
onTimeChange={handleTimeChange}
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import './LaunchChatSupport.styles.scss';
|
import './LaunchChatSupport.styles.scss';
|
||||||
|
|
||||||
import { Button, Modal, Tooltip, Typography } from 'antd';
|
import { Button, Modal, Tooltip, Typography } from 'antd';
|
||||||
import updateCreditCardApi from 'api/billing/checkout';
|
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import updateCreditCardApi from 'api/v1/checkout/create';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
@ -14,8 +13,9 @@ import { useAppContext } from 'providers/App/App';
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useMutation } from 'react-query';
|
import { useMutation } from 'react-query';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { SuccessResponseV2 } from 'types/api';
|
||||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
|
|
||||||
export interface LaunchChatSupportProps {
|
export interface LaunchChatSupportProps {
|
||||||
eventName: string;
|
eventName: string;
|
||||||
@ -118,20 +118,21 @@ function LaunchChatSupport({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleBillingOnSuccess = (
|
const handleBillingOnSuccess = (
|
||||||
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
|
data: SuccessResponseV2<CheckoutSuccessPayloadProps>,
|
||||||
): void => {
|
): void => {
|
||||||
if (data?.payload?.redirectURL) {
|
if (data?.data?.redirectURL) {
|
||||||
const newTab = document.createElement('a');
|
const newTab = document.createElement('a');
|
||||||
newTab.href = data.payload.redirectURL;
|
newTab.href = data.data.redirectURL;
|
||||||
newTab.target = '_blank';
|
newTab.target = '_blank';
|
||||||
newTab.rel = 'noopener noreferrer';
|
newTab.rel = 'noopener noreferrer';
|
||||||
newTab.click();
|
newTab.click();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBillingOnError = (): void => {
|
const handleBillingOnError = (error: APIError): void => {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: SOMETHING_WENT_WRONG,
|
message: error.getErrorCode(),
|
||||||
|
description: error.getErrorMessage(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,25 @@
|
|||||||
background: var(--bg-ink-400);
|
background: var(--bg-ink-400);
|
||||||
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
.log-detail-drawer__title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.log-detail-drawer__title-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-detail-drawer__title-right {
|
||||||
|
.ant-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ant-drawer-header {
|
.ant-drawer-header {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import { RadioChangeEvent } from 'antd/lib';
|
|||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import { QueryParams } from 'constants/query';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
|
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
|
||||||
import InfraMetrics from 'container/LogDetailedView/InfraMetrics/InfraMetrics';
|
import InfraMetrics from 'container/LogDetailedView/InfraMetrics/InfraMetrics';
|
||||||
import JSONView from 'container/LogDetailedView/JsonView';
|
import JSONView from 'container/LogDetailedView/JsonView';
|
||||||
@ -22,9 +24,12 @@ import dompurify from 'dompurify';
|
|||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import {
|
import {
|
||||||
BarChart2,
|
BarChart2,
|
||||||
Braces,
|
Braces,
|
||||||
|
Compass,
|
||||||
Copy,
|
Copy,
|
||||||
Filter,
|
Filter,
|
||||||
HardHat,
|
HardHat,
|
||||||
@ -33,9 +38,12 @@ import {
|
|||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useCopyToClipboard } from 'react-use';
|
import { useSelector } from 'react-redux';
|
||||||
|
import { useCopyToClipboard, useLocation } from 'react-use';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
import { FORBID_DOM_PURIFY_TAGS } from 'utils/app';
|
||||||
|
|
||||||
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
|
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
|
||||||
@ -77,6 +85,12 @@ function LogDetail({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
const location = useLocation();
|
||||||
|
const { safeNavigate } = useSafeNavigate();
|
||||||
|
const urlQuery = useUrlQuery();
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
@ -119,6 +133,21 @@ function LogDetail({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Go to logs explorer page with the log data
|
||||||
|
const handleOpenInExplorer = (): void => {
|
||||||
|
urlQuery.set(QueryParams.activeLogId, `"${log?.id}"`);
|
||||||
|
urlQuery.set(QueryParams.startTime, minTime?.toString() || '');
|
||||||
|
urlQuery.set(QueryParams.endTime, maxTime?.toString() || '');
|
||||||
|
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only show when opened from infra monitoring page
|
||||||
|
const showOpenInExplorerBtn = useMemo(
|
||||||
|
() => location.pathname?.includes('/infrastructure-monitoring'),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
if (!log) {
|
if (!log) {
|
||||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
return <></>;
|
return <></>;
|
||||||
@ -131,10 +160,23 @@ function LogDetail({
|
|||||||
width="60%"
|
width="60%"
|
||||||
maskStyle={{ background: 'none' }}
|
maskStyle={{ background: 'none' }}
|
||||||
title={
|
title={
|
||||||
<>
|
<div className="log-detail-drawer__title">
|
||||||
|
<div className="log-detail-drawer__title-left">
|
||||||
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
|
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
|
||||||
<Typography.Text className="title">Log details</Typography.Text>
|
<Typography.Text className="title">Log details</Typography.Text>
|
||||||
</>
|
</div>
|
||||||
|
{showOpenInExplorerBtn && (
|
||||||
|
<div className="log-detail-drawer__title-right">
|
||||||
|
<Button
|
||||||
|
className="open-in-explorer-btn"
|
||||||
|
icon={<Compass size={16} />}
|
||||||
|
onClick={handleOpenInExplorer}
|
||||||
|
>
|
||||||
|
Open in Explorer
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
placement="right"
|
placement="right"
|
||||||
// closable
|
// closable
|
||||||
|
|||||||
@ -30,6 +30,7 @@
|
|||||||
.right-action {
|
.right-action {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
min-width: 48px;
|
||||||
|
|
||||||
.clear-all {
|
.clear-all {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@ -52,10 +53,14 @@
|
|||||||
.checkbox-value-section {
|
.checkbox-value-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: 4px;
|
||||||
width: calc(100% - 24px);
|
width: calc(100% - 24px);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
.value-string {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
&.filter-disabled {
|
&.filter-disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|
||||||
@ -74,9 +79,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.value-string {
|
|
||||||
}
|
|
||||||
|
|
||||||
.only-btn {
|
.only-btn {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -177,3 +179,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label-false {
|
||||||
|
width: 2px;
|
||||||
|
height: 11px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--bg-cherry-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-true {
|
||||||
|
width: 2px;
|
||||||
|
height: 11px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--bg-forest-500);
|
||||||
|
}
|
||||||
|
|||||||
@ -504,6 +504,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
|||||||
onChange(value, currentFilterState[value], true);
|
onChange(value, currentFilterState[value], true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div className={`${filter.title} label-${value}`} />
|
||||||
{filter.customRendererForValue ? (
|
{filter.customRendererForValue ? (
|
||||||
filter.customRendererForValue(value)
|
filter.customRendererForValue(value)
|
||||||
) : (
|
) : (
|
||||||
@ -511,7 +512,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
|||||||
className="value-string"
|
className="value-string"
|
||||||
ellipsis={{ tooltip: { placement: 'right' } }}
|
ellipsis={{ tooltip: { placement: 'right' } }}
|
||||||
>
|
>
|
||||||
{value}
|
{String(value)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
)}
|
||||||
<Button type="text" className="only-btn">
|
<Button type="text" className="only-btn">
|
||||||
|
|||||||
@ -0,0 +1,174 @@
|
|||||||
|
.collapseContainer {
|
||||||
|
background-color: var(--bg-ink-500);
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
.ant-collapse-header {
|
||||||
|
padding: 12px !important;
|
||||||
|
align-items: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-collapse-expand-icon {
|
||||||
|
padding-right: 9px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-collapse-header-text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-inputs {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.min-max-input {
|
||||||
|
.ant-input-group-addon {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: 'Space Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px;
|
||||||
|
letter-spacing: 0.48px;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input {
|
||||||
|
padding: 4px 6px;
|
||||||
|
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: 'Space Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px;
|
||||||
|
letter-spacing: 0.48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
background-color: var(--bg-slate-400);
|
||||||
|
margin: 0;
|
||||||
|
border-color: var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-header {
|
||||||
|
padding: 16px 8px 16px 12px;
|
||||||
|
.filter-title {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.ant-typography {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-icon {
|
||||||
|
background-color: var(--bg-ink-500);
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
background-color: var(--bg-ink-500);
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
padding-top: 8px;
|
||||||
|
|
||||||
|
.anticon-vertical-align-top {
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-body-header {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
> button {
|
||||||
|
position: absolute;
|
||||||
|
right: 4px;
|
||||||
|
padding-top: 13px;
|
||||||
|
}
|
||||||
|
.ant-collapse {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card {
|
||||||
|
background-color: var(--bg-ink-500);
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
.lightMode {
|
||||||
|
.collapseContainer {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.ant-collapse-header-text {
|
||||||
|
color: var(--bg-slate-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-inputs {
|
||||||
|
.min-max-input {
|
||||||
|
.ant-input-group-addon {
|
||||||
|
color: var(--bg-slate-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input {
|
||||||
|
color: var(--bg-slate-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
border-color: var(--bg-vanilla-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-header {
|
||||||
|
.filter-title {
|
||||||
|
.ant-typography {
|
||||||
|
color: var(--bg-slate-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-icon {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,281 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
import './Duration.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Collapse } from 'antd';
|
||||||
|
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
|
||||||
|
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
|
||||||
|
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
|
||||||
|
import { DurationSection } from 'pages/TracesExplorer/Filter/DurationSection';
|
||||||
|
import {
|
||||||
|
AllTraceFilterKeys,
|
||||||
|
AllTraceFilterKeyValue,
|
||||||
|
HandleRunProps,
|
||||||
|
unionTagFilterItems,
|
||||||
|
} from 'pages/TracesExplorer/Filter/filterUtils';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
export type FilterType = Record<
|
||||||
|
AllTraceFilterKeys,
|
||||||
|
{ values: string[] | string; keys: BaseAutocompleteData }
|
||||||
|
>;
|
||||||
|
|
||||||
|
function Duration({
|
||||||
|
filter,
|
||||||
|
onFilterChange,
|
||||||
|
}: {
|
||||||
|
filter: IQuickFiltersConfig;
|
||||||
|
onFilterChange?: (query: Query) => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
const [selectedFilters, setSelectedFilters] = useState<
|
||||||
|
Record<
|
||||||
|
AllTraceFilterKeys,
|
||||||
|
{ values: string[] | string; keys: BaseAutocompleteData }
|
||||||
|
>
|
||||||
|
>();
|
||||||
|
const [activeKeys, setActiveKeys] = useState<string[]>([
|
||||||
|
filter.defaultOpen ? 'durationNano' : '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||||
|
|
||||||
|
const compositeQuery = useGetCompositeQueryParam();
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
const syncSelectedFilters = useMemo((): FilterType => {
|
||||||
|
const filters = compositeQuery?.builder.queryData?.[0].filters;
|
||||||
|
if (!filters) {
|
||||||
|
return {} as FilterType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (filters.items || [])
|
||||||
|
.filter((item) =>
|
||||||
|
Object.keys(AllTraceFilterKeyValue).includes(item.key?.key as string),
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
(item) =>
|
||||||
|
(item.op === 'in' && item.key?.key !== 'durationNano') ||
|
||||||
|
(item.key?.key === 'durationNano' && ['>=', '<='].includes(item.op)),
|
||||||
|
)
|
||||||
|
.reduce((acc, item) => {
|
||||||
|
const keys = item.key as BaseAutocompleteData;
|
||||||
|
const attributeName = item.key?.key || '';
|
||||||
|
const values = item.value as string[];
|
||||||
|
|
||||||
|
if ((attributeName as AllTraceFilterKeys) === 'durationNano') {
|
||||||
|
if (item.op === '>=') {
|
||||||
|
acc.durationNanoMin = {
|
||||||
|
values: getMs(String(values)),
|
||||||
|
keys,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
acc.durationNanoMax = {
|
||||||
|
values: getMs(String(values)),
|
||||||
|
keys,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributeName) {
|
||||||
|
if (acc[attributeName as AllTraceFilterKeys]) {
|
||||||
|
const existingValue = acc[attributeName as AllTraceFilterKeys];
|
||||||
|
acc[attributeName as AllTraceFilterKeys] = {
|
||||||
|
values: [...existingValue.values, ...values],
|
||||||
|
keys,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
acc[attributeName as AllTraceFilterKeys] = { values, keys };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {} as FilterType);
|
||||||
|
}, [compositeQuery]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isEqual(syncSelectedFilters, selectedFilters)) {
|
||||||
|
setSelectedFilters(syncSelectedFilters);
|
||||||
|
}
|
||||||
|
}, [syncSelectedFilters]);
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
const preparePostData = (): TagFilterItem[] => {
|
||||||
|
if (!selectedFilters) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = Object.keys(selectedFilters)?.flatMap((attribute) => {
|
||||||
|
const { keys, values } = selectedFilters[attribute as AllTraceFilterKeys];
|
||||||
|
if (
|
||||||
|
['durationNanoMax', 'durationNanoMin', 'durationNano'].includes(
|
||||||
|
attribute as AllTraceFilterKeys,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (!values || !values.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
let minValue = '';
|
||||||
|
let maxValue = '';
|
||||||
|
|
||||||
|
const durationItems: TagFilterItem[] = [];
|
||||||
|
|
||||||
|
if (isArray(values)) {
|
||||||
|
minValue = values?.[0];
|
||||||
|
maxValue = values?.[1];
|
||||||
|
|
||||||
|
const minItems: TagFilterItem = {
|
||||||
|
id: uuid().slice(0, 8),
|
||||||
|
op: '>=',
|
||||||
|
key: keys,
|
||||||
|
value: Number(minValue) * 1000000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const maxItems: TagFilterItem = {
|
||||||
|
id: uuid().slice(0, 8),
|
||||||
|
op: '<=',
|
||||||
|
key: keys,
|
||||||
|
value: Number(maxValue) * 1000000,
|
||||||
|
};
|
||||||
|
return maxValue ? [minItems, maxItems] : [minItems];
|
||||||
|
}
|
||||||
|
if (attribute === 'durationNanoMin') {
|
||||||
|
durationItems.push({
|
||||||
|
id: uuid().slice(0, 8),
|
||||||
|
op: '>=',
|
||||||
|
key: keys,
|
||||||
|
value: Number(values) * 1000000,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
durationItems.push({
|
||||||
|
id: uuid().slice(0, 8),
|
||||||
|
op: '<=',
|
||||||
|
key: keys,
|
||||||
|
value: Number(values) * 1000000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return durationItems;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: uuid().slice(0, 8),
|
||||||
|
key: keys,
|
||||||
|
op: 'in',
|
||||||
|
value: values,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return items as TagFilterItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFilterItemIds = (query: Query): Query => {
|
||||||
|
const clonedQuery = cloneDeep(query);
|
||||||
|
clonedQuery.builder.queryData = clonedQuery.builder.queryData.map((data) => ({
|
||||||
|
...data,
|
||||||
|
filters: {
|
||||||
|
...data.filters,
|
||||||
|
items: data.filters?.items?.map((item) => ({
|
||||||
|
...item,
|
||||||
|
id: '',
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
return clonedQuery;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRun = useCallback(
|
||||||
|
(props?: HandleRunProps): void => {
|
||||||
|
const preparedQuery: Query = {
|
||||||
|
...currentQuery,
|
||||||
|
builder: {
|
||||||
|
...currentQuery.builder,
|
||||||
|
queryData: currentQuery.builder.queryData.map((item) => ({
|
||||||
|
...item,
|
||||||
|
filters: {
|
||||||
|
...item.filters,
|
||||||
|
items: props?.resetAll
|
||||||
|
? []
|
||||||
|
: (unionTagFilterItems(item.filters?.items, preparePostData())
|
||||||
|
.map((item) =>
|
||||||
|
item.key?.key === props?.clearByType ? undefined : item,
|
||||||
|
)
|
||||||
|
.filter((i) => i) as TagFilterItem[]),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentQueryWithoutIds = removeFilterItemIds(currentQuery);
|
||||||
|
const preparedQueryWithoutIds = removeFilterItemIds(preparedQuery);
|
||||||
|
|
||||||
|
if (
|
||||||
|
isEqual(currentQueryWithoutIds, preparedQueryWithoutIds) &&
|
||||||
|
!props?.resetAll
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onFilterChange && isFunction(onFilterChange)) {
|
||||||
|
onFilterChange(preparedQuery);
|
||||||
|
} else {
|
||||||
|
redirectWithQueryBuilderData(preparedQuery);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[currentQuery, redirectWithQueryBuilderData, selectedFilters],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleRun();
|
||||||
|
}, [selectedFilters]);
|
||||||
|
|
||||||
|
const onClearHandler = (e: React.MouseEvent): void => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (selectedFilters?.durationNanoMin || selectedFilters?.durationNanoMax) {
|
||||||
|
handleRun({ clearByType: 'durationNano' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="section-body-header" data-testid="collapse-duration">
|
||||||
|
<Collapse
|
||||||
|
bordered={false}
|
||||||
|
className="collapseContainer"
|
||||||
|
activeKey={activeKeys}
|
||||||
|
onChange={(keys): void => setActiveKeys(keys as string[])}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'durationNano',
|
||||||
|
children: (
|
||||||
|
<DurationSection
|
||||||
|
setSelectedFilters={setSelectedFilters}
|
||||||
|
selectedFilters={selectedFilters}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
label: 'Duration',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{activeKeys.includes('durationNano') && (
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
onClick={onClearHandler}
|
||||||
|
data-testid="collapse-duration-clearBtn"
|
||||||
|
>
|
||||||
|
Clear All
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Duration.defaultProps = {
|
||||||
|
onFilterChange: (): void => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Duration;
|
||||||
@ -5,19 +5,24 @@ import {
|
|||||||
SyncOutlined,
|
SyncOutlined,
|
||||||
VerticalAlignTopOutlined,
|
VerticalAlignTopOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Skeleton, Tooltip, Typography } from 'antd';
|
import { Skeleton, Switch, Tooltip, Typography } from 'antd';
|
||||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import { useApiMonitoringParams } from 'container/ApiMonitoring/queryParams';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { cloneDeep, isFunction, isNull } from 'lodash-es';
|
import { cloneDeep, isFunction, isNull } from 'lodash-es';
|
||||||
import { Settings2 as SettingsIcon } from 'lucide-react';
|
import { Settings2 as SettingsIcon } from 'lucide-react';
|
||||||
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { USER_ROLES } from 'types/roles';
|
||||||
|
|
||||||
import Checkbox from './FilterRenderers/Checkbox/Checkbox';
|
import Checkbox from './FilterRenderers/Checkbox/Checkbox';
|
||||||
|
import Duration from './FilterRenderers/Duration/Duration';
|
||||||
import Slider from './FilterRenderers/Slider/Slider';
|
import Slider from './FilterRenderers/Slider/Slider';
|
||||||
import useFilterConfig from './hooks/useFilterConfig';
|
import useFilterConfig from './hooks/useFilterConfig';
|
||||||
import AnnouncementTooltip from './QuickFiltersSettings/AnnouncementTooltip';
|
import AnnouncementTooltip from './QuickFiltersSettings/AnnouncementTooltip';
|
||||||
@ -32,8 +37,14 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
|||||||
source,
|
source,
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
signal,
|
signal,
|
||||||
|
showFilterCollapse = true,
|
||||||
|
showQueryName = true,
|
||||||
} = props;
|
} = props;
|
||||||
|
const { user } = useAppContext();
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
|
const isAdmin = user.role === USER_ROLES.ADMIN;
|
||||||
|
const [params, setParams] = useApiMonitoringParams();
|
||||||
|
const showIP = params.showIP ?? true;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
filterConfig,
|
filterConfig,
|
||||||
@ -95,13 +106,13 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const lastQueryName =
|
const lastQueryName =
|
||||||
|
showQueryName &&
|
||||||
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
|
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="quick-filters-container">
|
<div className="quick-filters-container">
|
||||||
<div className="quick-filters">
|
<div className="quick-filters">
|
||||||
{source !== QuickFiltersSource.INFRA_MONITORING &&
|
{source !== QuickFiltersSource.INFRA_MONITORING && (
|
||||||
source !== QuickFiltersSource.API_MONITORING && (
|
|
||||||
<section className="header">
|
<section className="header">
|
||||||
<section className="left-actions">
|
<section className="left-actions">
|
||||||
<FilterOutlined />
|
<FilterOutlined />
|
||||||
@ -109,12 +120,8 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
|||||||
{lastQueryName ? 'Filters for' : 'Filters'}
|
{lastQueryName ? 'Filters for' : 'Filters'}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
{lastQueryName && (
|
{lastQueryName && (
|
||||||
<Tooltip
|
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
||||||
title={`Filter currently in sync with query ${lastQueryName}`}
|
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
||||||
>
|
|
||||||
<Typography.Text className="sync-tag">
|
|
||||||
{lastQueryName}
|
|
||||||
</Typography.Text>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
@ -125,6 +132,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
|||||||
<SyncOutlined className="sync-icon" onClick={handleReset} />
|
<SyncOutlined className="sync-icon" onClick={handleReset} />
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{showFilterCollapse && (
|
||||||
<Tooltip title="Collapse Filters">
|
<Tooltip title="Collapse Filters">
|
||||||
<div className="right-action-icon-container">
|
<div className="right-action-icon-container">
|
||||||
<VerticalAlignTopOutlined
|
<VerticalAlignTopOutlined
|
||||||
@ -133,7 +141,8 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{isDynamicFilters && (
|
)}
|
||||||
|
{isDynamicFilters && isAdmin && (
|
||||||
<Tooltip title="Settings">
|
<Tooltip title="Settings">
|
||||||
<div
|
<div
|
||||||
className={classNames('right-action-icon-container', {
|
className={classNames('right-action-icon-container', {
|
||||||
@ -179,6 +188,23 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<OverlayScrollbar>
|
<OverlayScrollbar>
|
||||||
|
<>
|
||||||
|
{source === QuickFiltersSource.API_MONITORING && (
|
||||||
|
<div className="api-quick-filters-header">
|
||||||
|
<Typography.Text>Show IP addresses</Typography.Text>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
style={{ marginLeft: 'auto' }}
|
||||||
|
checked={showIP ?? true}
|
||||||
|
onClick={(): void => {
|
||||||
|
logEvent('API Monitoring: Show IP addresses clicked', {
|
||||||
|
showIP: !(showIP ?? true),
|
||||||
|
});
|
||||||
|
setParams({ showIP });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<section className="filters">
|
<section className="filters">
|
||||||
{filterConfig.map((filter) => {
|
{filterConfig.map((filter) => {
|
||||||
switch (filter.type) {
|
switch (filter.type) {
|
||||||
@ -190,6 +216,8 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
|||||||
onFilterChange={onFilterChange}
|
onFilterChange={onFilterChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case FiltersType.DURATION:
|
||||||
|
return <Duration filter={filter} onFilterChange={onFilterChange} />;
|
||||||
case FiltersType.SLIDER:
|
case FiltersType.SLIDER:
|
||||||
return <Slider filter={filter} />;
|
return <Slider filter={filter} />;
|
||||||
// eslint-disable-next-line sonarjs/no-duplicated-branches
|
// eslint-disable-next-line sonarjs/no-duplicated-branches
|
||||||
@ -204,6 +232,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</section>
|
</section>
|
||||||
|
</>
|
||||||
</OverlayScrollbar>
|
</OverlayScrollbar>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -235,4 +264,6 @@ QuickFilters.defaultProps = {
|
|||||||
onFilterChange: null,
|
onFilterChange: null,
|
||||||
signal: '',
|
signal: '',
|
||||||
config: [],
|
config: [],
|
||||||
|
showFilterCollapse: true,
|
||||||
|
showQueryName: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,10 +3,12 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
|||||||
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
|
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
|
||||||
import { SignalType } from 'components/QuickFilters/types';
|
import { SignalType } from 'components/QuickFilters/types';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
|
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||||
import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions';
|
import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
function OtherFiltersSkeleton(): JSX.Element {
|
function OtherFiltersSkeleton(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
@ -34,6 +36,11 @@ function OtherFilters({
|
|||||||
addedFilters: FilterType[];
|
addedFilters: FilterType[];
|
||||||
setAddedFilters: React.Dispatch<React.SetStateAction<FilterType[]>>;
|
setAddedFilters: React.Dispatch<React.SetStateAction<FilterType[]>>;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
|
const isLogDataSource = useMemo(
|
||||||
|
() => SIGNAL_DATA_SOURCE_MAP[signal as SignalType] === DataSource.LOGS,
|
||||||
|
[signal],
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: suggestionsData,
|
data: suggestionsData,
|
||||||
isFetching: isFetchingSuggestions,
|
isFetching: isFetchingSuggestions,
|
||||||
@ -45,18 +52,39 @@ function OtherFilters({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
|
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
|
||||||
enabled: !!signal,
|
enabled: !!signal && isLogDataSource,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const otherFilters = useMemo(
|
const {
|
||||||
() =>
|
data: aggregateKeysData,
|
||||||
suggestionsData?.payload?.attributes?.filter(
|
isFetching: isFetchingAggregateKeys,
|
||||||
(attr) => !addedFilters.some((filter) => filter.key === attr.key),
|
} = useGetAggregateKeys(
|
||||||
),
|
{
|
||||||
[suggestionsData, addedFilters],
|
searchText: inputValue,
|
||||||
|
dataSource: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
aggregateAttribute: '',
|
||||||
|
tagType: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
|
||||||
|
enabled: !!signal && !isLogDataSource,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const otherFilters = useMemo(() => {
|
||||||
|
let filterAttributes;
|
||||||
|
if (isLogDataSource) {
|
||||||
|
filterAttributes = suggestionsData?.payload?.attributes || [];
|
||||||
|
} else {
|
||||||
|
filterAttributes = aggregateKeysData?.payload?.attributeKeys || [];
|
||||||
|
}
|
||||||
|
return filterAttributes?.filter(
|
||||||
|
(attr) => !addedFilters.some((filter) => filter.key === attr.key),
|
||||||
|
);
|
||||||
|
}, [suggestionsData, aggregateKeysData, addedFilters, isLogDataSource]);
|
||||||
|
|
||||||
const handleAddFilter = (filter: FilterType): void => {
|
const handleAddFilter = (filter: FilterType): void => {
|
||||||
setAddedFilters((prev) => [
|
setAddedFilters((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
@ -71,7 +99,8 @@ function OtherFilters({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderFilters = (): React.ReactNode => {
|
const renderFilters = (): React.ReactNode => {
|
||||||
if (isFetchingSuggestions) return <OtherFiltersSkeleton />;
|
const isLoading = isFetchingSuggestions || isFetchingAggregateKeys;
|
||||||
|
if (isLoading) return <OtherFiltersSkeleton />;
|
||||||
if (!otherFilters?.length)
|
if (!otherFilters?.length)
|
||||||
return <div className="no-values-found">No values found</div>;
|
return <div className="no-values-found">No values found</div>;
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
background: var(--bg-slate-500);
|
background: var(--bg-slate-500);
|
||||||
transition: width 0.05s ease-in-out;
|
transition: width 0.05s ease-in-out;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
|
||||||
&.qf-logs-explorer {
|
&.qf-logs-explorer {
|
||||||
height: calc(100vh - 45px);
|
height: calc(100vh - 45px);
|
||||||
@ -16,6 +17,14 @@
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.qf-api-monitoring {
|
||||||
|
height: calc(100vh - 45px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.qf-traces-explorer {
|
||||||
|
height: calc(100vh - 45px);
|
||||||
|
}
|
||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
@ -172,6 +181,7 @@
|
|||||||
.lightMode {
|
.lightMode {
|
||||||
.quick-filters-settings {
|
.quick-filters-settings {
|
||||||
background: var(--bg-vanilla-100);
|
background: var(--bg-vanilla-100);
|
||||||
|
color: var(--bg-slate-500);
|
||||||
.search {
|
.search {
|
||||||
.ant-input {
|
.ant-input {
|
||||||
background-color: var(--bg-vanilla-100);
|
background-color: var(--bg-vanilla-100);
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import logEvent from 'api/common/logEvent';
|
||||||
import updateCustomFiltersAPI from 'api/quickFilters/updateCustomFilters';
|
import updateCustomFiltersAPI from 'api/quickFilters/updateCustomFilters';
|
||||||
import axios, { AxiosError } from 'axios';
|
import axios, { AxiosError } from 'axios';
|
||||||
import { SignalType } from 'components/QuickFilters/types';
|
import { SignalType } from 'components/QuickFilters/types';
|
||||||
@ -46,6 +47,9 @@ const useQuickFilterSettings = ({
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setIsSettingsOpen(false);
|
setIsSettingsOpen(false);
|
||||||
setIsStale(true);
|
setIsStale(true);
|
||||||
|
logEvent('Quick Filters Settings: changes saved', {
|
||||||
|
addedFilters,
|
||||||
|
});
|
||||||
notifications.success({
|
notifications.success({
|
||||||
message: 'Quick filters updated successfully',
|
message: 'Quick filters updated successfully',
|
||||||
placement: 'bottomRight',
|
placement: 'bottomRight',
|
||||||
|
|||||||
@ -33,7 +33,7 @@ const useFilterConfig = ({
|
|||||||
const isDynamicFilters = useMemo(() => customFilters.length > 0, [
|
const isDynamicFilters = useMemo(() => customFilters.length > 0, [
|
||||||
customFilters,
|
customFilters,
|
||||||
]);
|
]);
|
||||||
const { isLoading: isCustomFiltersLoading } = useQuery<
|
const { isFetching: isCustomFiltersLoading } = useQuery<
|
||||||
SuccessResponse<PayloadProps> | ErrorResponse,
|
SuccessResponse<PayloadProps> | ErrorResponse,
|
||||||
Error
|
Error
|
||||||
>(
|
>(
|
||||||
@ -49,10 +49,10 @@ const useFilterConfig = ({
|
|||||||
enabled: !!signal && isStale,
|
enabled: !!signal && isStale,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const filterConfig = useMemo(() => getFilterConfig(customFilters, config), [
|
const filterConfig = useMemo(
|
||||||
config,
|
() => getFilterConfig(signal, customFilters, config),
|
||||||
customFilters,
|
[config, customFilters, signal],
|
||||||
]);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filterConfig,
|
filterConfig,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
act,
|
||||||
cleanup,
|
cleanup,
|
||||||
fireEvent,
|
fireEvent,
|
||||||
render,
|
render,
|
||||||
@ -8,6 +9,7 @@ import {
|
|||||||
waitFor,
|
waitFor,
|
||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
import { ENVIRONMENT } from 'constants/env';
|
import { ENVIRONMENT } from 'constants/env';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import {
|
import {
|
||||||
otherFiltersResponse,
|
otherFiltersResponse,
|
||||||
@ -17,6 +19,7 @@ import {
|
|||||||
import { server } from 'mocks-server/server';
|
import { server } from 'mocks-server/server';
|
||||||
import { rest } from 'msw';
|
import { rest } from 'msw';
|
||||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||||
|
import { USER_ROLES } from 'types/roles';
|
||||||
|
|
||||||
import QuickFilters from '../QuickFilters';
|
import QuickFilters from '../QuickFilters';
|
||||||
import { IQuickFiltersConfig, QuickFiltersSource, SignalType } from '../types';
|
import { IQuickFiltersConfig, QuickFiltersSource, SignalType } from '../types';
|
||||||
@ -26,6 +29,21 @@ jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
|||||||
useQueryBuilder: jest.fn(),
|
useQueryBuilder: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useLocation: (): { pathname: string } => ({
|
||||||
|
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.TRACES_EXPLORER}/`,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const userRole = USER_ROLES.ADMIN;
|
||||||
|
|
||||||
|
// mock useAppContext
|
||||||
|
jest.mock('providers/App/App', () => ({
|
||||||
|
useAppContext: jest.fn(() => ({ user: { role: userRole } })),
|
||||||
|
}));
|
||||||
|
|
||||||
const handleFilterVisibilityChange = jest.fn();
|
const handleFilterVisibilityChange = jest.fn();
|
||||||
const redirectWithQueryBuilderData = jest.fn();
|
const redirectWithQueryBuilderData = jest.fn();
|
||||||
const putHandler = jest.fn();
|
const putHandler = jest.fn();
|
||||||
@ -163,7 +181,9 @@ describe('Quick Filters with custom filters', () => {
|
|||||||
expect(screen.getByText('Filters for')).toBeInTheDocument();
|
expect(screen.getByText('Filters for')).toBeInTheDocument();
|
||||||
expect(screen.getByText(QUERY_NAME)).toBeInTheDocument();
|
expect(screen.getByText(QUERY_NAME)).toBeInTheDocument();
|
||||||
await screen.findByText(FILTER_SERVICE_NAME);
|
await screen.findByText(FILTER_SERVICE_NAME);
|
||||||
await screen.findByText('otel-demo');
|
const allByText = await screen.findAllByText('otel-demo');
|
||||||
|
// since 2 filter collapse are open, there are 2 filter items visible
|
||||||
|
expect(allByText).toHaveLength(2);
|
||||||
|
|
||||||
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
|
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
|
||||||
fireEvent.click(icon);
|
fireEvent.click(icon);
|
||||||
@ -285,4 +305,59 @@ describe('Quick Filters with custom filters', () => {
|
|||||||
);
|
);
|
||||||
expect(requestBody.signal).toBe(SIGNAL);
|
expect(requestBody.signal).toBe(SIGNAL);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// render duration filter
|
||||||
|
it('should render duration slider for duration_nono filter', async () => {
|
||||||
|
// Set up fake timers **before rendering**
|
||||||
|
jest.useFakeTimers();
|
||||||
|
|
||||||
|
const { getByTestId } = render(<TestQuickFilters signal={SIGNAL} />);
|
||||||
|
await screen.findByText(FILTER_SERVICE_NAME);
|
||||||
|
expect(screen.getByText('Duration')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// click to open the duration filter
|
||||||
|
fireEvent.click(screen.getByText('Duration'));
|
||||||
|
|
||||||
|
const minDuration = getByTestId('min-input') as HTMLInputElement;
|
||||||
|
const maxDuration = getByTestId('max-input') as HTMLInputElement;
|
||||||
|
expect(minDuration).toHaveValue(null);
|
||||||
|
expect(minDuration).toHaveProperty('placeholder', '0');
|
||||||
|
expect(maxDuration).toHaveValue(null);
|
||||||
|
expect(maxDuration).toHaveProperty('placeholder', '100000000');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
// set values
|
||||||
|
fireEvent.change(minDuration, { target: { value: '10000' } });
|
||||||
|
fireEvent.change(maxDuration, { target: { value: '20000' } });
|
||||||
|
jest.advanceTimersByTime(2000);
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(redirectWithQueryBuilderData).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
builder: {
|
||||||
|
queryData: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
filters: expect.objectContaining({
|
||||||
|
items: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
key: expect.objectContaining({ key: 'durationNano' }),
|
||||||
|
op: '>=',
|
||||||
|
value: 10000000000,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
key: expect.objectContaining({ key: 'durationNano' }),
|
||||||
|
op: '<=',
|
||||||
|
value: 20000000000,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.useRealTimers(); // Clean up
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
|||||||
export enum FiltersType {
|
export enum FiltersType {
|
||||||
SLIDER = 'SLIDER',
|
SLIDER = 'SLIDER',
|
||||||
CHECKBOX = 'CHECKBOX',
|
CHECKBOX = 'CHECKBOX',
|
||||||
|
DURATION = 'DURATION', // ALIAS FOR DURATION_NANO
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MinMax {
|
export enum MinMax {
|
||||||
@ -42,6 +43,8 @@ export interface IQuickFiltersProps {
|
|||||||
onFilterChange?: (query: Query) => void;
|
onFilterChange?: (query: Query) => void;
|
||||||
signal?: SignalType;
|
signal?: SignalType;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
showFilterCollapse?: boolean;
|
||||||
|
showQueryName?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum QuickFiltersSource {
|
export enum QuickFiltersSource {
|
||||||
|
|||||||
@ -1,30 +1,53 @@
|
|||||||
|
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
|
||||||
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
|
||||||
|
|
||||||
import { FiltersType, IQuickFiltersConfig } from './types';
|
import { FiltersType, IQuickFiltersConfig, SignalType } from './types';
|
||||||
|
|
||||||
const getFilterName = (str: string): string =>
|
const FILTER_TITLE_MAP: Record<string, string> = {
|
||||||
|
duration_nano: 'Duration',
|
||||||
|
hasError: 'Has Error (Status)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const FILTER_TYPE_MAP: Record<string, FiltersType> = {
|
||||||
|
duration_nano: FiltersType.DURATION,
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFilterName = (str: string): string => {
|
||||||
|
if (FILTER_TITLE_MAP[str]) {
|
||||||
|
return FILTER_TITLE_MAP[str];
|
||||||
|
}
|
||||||
// replace . and _ with space
|
// replace . and _ with space
|
||||||
// capitalize the first letter of each word
|
// capitalize the first letter of each word
|
||||||
str
|
return str
|
||||||
.replace(/\./g, ' ')
|
.replace(/\./g, ' ')
|
||||||
.replace(/_/g, ' ')
|
.replace(/_/g, ' ')
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
.join(' ');
|
.join(' ');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFilterType = (att: FilterType): FiltersType => {
|
||||||
|
if (FILTER_TYPE_MAP[att.key]) {
|
||||||
|
return FILTER_TYPE_MAP[att.key];
|
||||||
|
}
|
||||||
|
return FiltersType.CHECKBOX;
|
||||||
|
};
|
||||||
|
|
||||||
export const getFilterConfig = (
|
export const getFilterConfig = (
|
||||||
|
signal?: SignalType,
|
||||||
customFilters?: FilterType[],
|
customFilters?: FilterType[],
|
||||||
config?: IQuickFiltersConfig[],
|
config?: IQuickFiltersConfig[],
|
||||||
): IQuickFiltersConfig[] => {
|
): IQuickFiltersConfig[] => {
|
||||||
if (!customFilters?.length) {
|
if (!customFilters?.length || !signal) {
|
||||||
return config || [];
|
return config || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return customFilters.map(
|
return customFilters.map(
|
||||||
(att, index) =>
|
(att, index) =>
|
||||||
({
|
({
|
||||||
type: FiltersType.CHECKBOX,
|
type: getFilterType(att),
|
||||||
title: getFilterName(att.key),
|
title: getFilterName(att.key),
|
||||||
|
dataSource: SIGNAL_DATA_SOURCE_MAP[signal],
|
||||||
attributeKey: {
|
attributeKey: {
|
||||||
id: att.key,
|
id: att.key,
|
||||||
key: att.key,
|
key: att.key,
|
||||||
@ -33,7 +56,7 @@ export const getFilterConfig = (
|
|||||||
isColumn: att.isColumn,
|
isColumn: att.isColumn,
|
||||||
isJSON: att.isJSON,
|
isJSON: att.isJSON,
|
||||||
},
|
},
|
||||||
defaultOpen: index === 0,
|
defaultOpen: index < 2,
|
||||||
} as IQuickFiltersConfig),
|
} as IQuickFiltersConfig),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import {
|
|||||||
} from 'mocks-server/__mockdata__/alerts';
|
} from 'mocks-server/__mockdata__/alerts';
|
||||||
import { server } from 'mocks-server/server';
|
import { server } from 'mocks-server/server';
|
||||||
import { rest } from 'msw';
|
import { rest } from 'msw';
|
||||||
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||||
|
|
||||||
import { testLabelInputAndHelpValue } from './testUtils';
|
import { testLabelInputAndHelpValue } from './testUtils';
|
||||||
|
|
||||||
@ -30,6 +30,14 @@ jest.mock('hooks/useNotifications', () => ({
|
|||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
const showErrorModal = jest.fn();
|
||||||
|
jest.mock('providers/ErrorModalProvider', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
...jest.requireActual('providers/ErrorModalProvider'),
|
||||||
|
useErrorModal: jest.fn(() => ({
|
||||||
|
showErrorModal,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('components/MarkdownRenderer/MarkdownRenderer', () => ({
|
jest.mock('components/MarkdownRenderer/MarkdownRenderer', () => ({
|
||||||
MarkdownRenderer: jest.fn(() => <div>Mocked MarkdownRenderer</div>),
|
MarkdownRenderer: jest.fn(() => <div>Mocked MarkdownRenderer</div>),
|
||||||
@ -119,7 +127,7 @@ describe('Create Alert Channel', () => {
|
|||||||
|
|
||||||
fireEvent.click(saveButton);
|
fireEvent.click(saveButton);
|
||||||
|
|
||||||
await waitFor(() => expect(errorNotification).toHaveBeenCalled());
|
await waitFor(() => expect(showErrorModal).toHaveBeenCalled());
|
||||||
});
|
});
|
||||||
it('Should check if clicking on Test button shows "An alert has been sent to this channel" success message if testing passes', async () => {
|
it('Should check if clicking on Test button shows "An alert has been sent to this channel" success message if testing passes', async () => {
|
||||||
server.use(
|
server.use(
|
||||||
@ -151,9 +159,11 @@ describe('Create Alert Channel', () => {
|
|||||||
name: 'button_test_channel',
|
name: 'button_test_channel',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
act(() => {
|
||||||
fireEvent.click(testButton);
|
fireEvent.click(testButton);
|
||||||
|
});
|
||||||
|
|
||||||
await waitFor(() => expect(errorNotification).toHaveBeenCalled());
|
await waitFor(() => expect(showErrorModal).toHaveBeenCalled());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('New Alert Channel Cascading Fields Based on Channel Type', () => {
|
describe('New Alert Channel Cascading Fields Based on Channel Type', () => {
|
||||||
|
|||||||
@ -1,23 +1,16 @@
|
|||||||
import './Explorer.styles.scss';
|
import './Explorer.styles.scss';
|
||||||
|
|
||||||
import { FilterOutlined } from '@ant-design/icons';
|
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import { Switch, Typography } from 'antd';
|
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { useApiMonitoringParams } from '../queryParams';
|
|
||||||
import { ApiMonitoringQuickFiltersConfig } from '../utils';
|
|
||||||
import DomainList from './Domains/DomainList';
|
import DomainList from './Domains/DomainList';
|
||||||
|
|
||||||
function Explorer(): JSX.Element {
|
function Explorer(): JSX.Element {
|
||||||
const [params, setParams] = useApiMonitoringParams();
|
|
||||||
const showIP = params.showIP ?? true;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logEvent('API Monitoring: Landing page visited', {});
|
logEvent('API Monitoring: Landing page visited', {});
|
||||||
}, []);
|
}, []);
|
||||||
@ -26,29 +19,12 @@ function Explorer(): JSX.Element {
|
|||||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||||
<div className={cx('api-monitoring-page', 'filter-visible')}>
|
<div className={cx('api-monitoring-page', 'filter-visible')}>
|
||||||
<section className="api-quick-filter-left-section">
|
<section className="api-quick-filter-left-section">
|
||||||
<div className="api-quick-filters-header">
|
|
||||||
<FilterOutlined />
|
|
||||||
<Typography.Text>Filters</Typography.Text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="api-quick-filters-header">
|
|
||||||
<Typography.Text>Show IP addresses</Typography.Text>
|
|
||||||
<Switch
|
|
||||||
size="small"
|
|
||||||
style={{ marginLeft: 'auto' }}
|
|
||||||
checked={showIP ?? true}
|
|
||||||
onClick={(): void => {
|
|
||||||
logEvent('API Monitoring: Show IP addresses clicked', {
|
|
||||||
showIP: !(showIP ?? true),
|
|
||||||
});
|
|
||||||
setParams({ showIP });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<QuickFilters
|
<QuickFilters
|
||||||
|
className="qf-api-monitoring"
|
||||||
source={QuickFiltersSource.API_MONITORING}
|
source={QuickFiltersSource.API_MONITORING}
|
||||||
config={ApiMonitoringQuickFiltersConfig}
|
signal={SignalType.API_MONITORING}
|
||||||
|
showFilterCollapse={false}
|
||||||
|
showQueryName={false}
|
||||||
handleFilterVisibilityChange={(): void => {}}
|
handleFilterVisibilityChange={(): void => {}}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -5,16 +5,15 @@ import './AppLayout.styles.scss';
|
|||||||
|
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import { Flex } from 'antd';
|
import { Flex } from 'antd';
|
||||||
import manageCreditCardApi from 'api/billing/manage';
|
|
||||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import manageCreditCardApi from 'api/v1/portal/create';
|
||||||
import getUserLatestVersion from 'api/v1/version/getLatestVersion';
|
import getUserLatestVersion from 'api/v1/version/getLatestVersion';
|
||||||
import getUserVersion from 'api/v1/version/getVersion';
|
import getUserVersion from 'api/v1/version/getVersion';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
|
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
|
||||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
|
||||||
import { Events } from 'constants/events';
|
import { Events } from 'constants/events';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
@ -51,8 +50,9 @@ import {
|
|||||||
UPDATE_LATEST_VERSION,
|
UPDATE_LATEST_VERSION,
|
||||||
UPDATE_LATEST_VERSION_ERROR,
|
UPDATE_LATEST_VERSION_ERROR,
|
||||||
} from 'types/actions/app';
|
} from 'types/actions/app';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { SuccessResponseV2 } from 'types/api';
|
||||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
import {
|
import {
|
||||||
LicenseEvent,
|
LicenseEvent,
|
||||||
LicensePlatform,
|
LicensePlatform,
|
||||||
@ -75,8 +75,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
user,
|
user,
|
||||||
trialInfo,
|
trialInfo,
|
||||||
activeLicenseV3,
|
activeLicense,
|
||||||
isFetchingActiveLicenseV3,
|
isFetchingActiveLicense,
|
||||||
featureFlags,
|
featureFlags,
|
||||||
isFetchingFeatureFlags,
|
isFetchingFeatureFlags,
|
||||||
featureFlagsFetchError,
|
featureFlagsFetchError,
|
||||||
@ -93,20 +93,21 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
const [slowApiWarningShown, setSlowApiWarningShown] = useState(false);
|
const [slowApiWarningShown, setSlowApiWarningShown] = useState(false);
|
||||||
|
|
||||||
const handleBillingOnSuccess = (
|
const handleBillingOnSuccess = (
|
||||||
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
|
data: SuccessResponseV2<CheckoutSuccessPayloadProps>,
|
||||||
): void => {
|
): void => {
|
||||||
if (data?.payload?.redirectURL) {
|
if (data?.data?.redirectURL) {
|
||||||
const newTab = document.createElement('a');
|
const newTab = document.createElement('a');
|
||||||
newTab.href = data.payload.redirectURL;
|
newTab.href = data.data.redirectURL;
|
||||||
newTab.target = '_blank';
|
newTab.target = '_blank';
|
||||||
newTab.rel = 'noopener noreferrer';
|
newTab.rel = 'noopener noreferrer';
|
||||||
newTab.click();
|
newTab.click();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBillingOnError = (): void => {
|
const handleBillingOnError = (error: APIError): void => {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: SOMETHING_WENT_WRONG,
|
message: error.getErrorCode(),
|
||||||
|
description: error.getErrorMessage(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -260,8 +261,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!isFetchingActiveLicenseV3 &&
|
!isFetchingActiveLicense &&
|
||||||
activeLicenseV3 &&
|
activeLicense &&
|
||||||
trialInfo?.onTrial &&
|
trialInfo?.onTrial &&
|
||||||
!trialInfo?.trialConvertedToSubscription &&
|
!trialInfo?.trialConvertedToSubscription &&
|
||||||
!trialInfo?.workSpaceBlock &&
|
!trialInfo?.workSpaceBlock &&
|
||||||
@ -269,16 +270,16 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
) {
|
) {
|
||||||
setShowTrialExpiryBanner(true);
|
setShowTrialExpiryBanner(true);
|
||||||
}
|
}
|
||||||
}, [isFetchingActiveLicenseV3, activeLicenseV3, trialInfo]);
|
}, [isFetchingActiveLicense, activeLicense, trialInfo]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isFetchingActiveLicenseV3 && activeLicenseV3) {
|
if (!isFetchingActiveLicense && activeLicense) {
|
||||||
const isTerminated = activeLicenseV3.state === LicenseState.TERMINATED;
|
const isTerminated = activeLicense.state === LicenseState.TERMINATED;
|
||||||
const isExpired = activeLicenseV3.state === LicenseState.EXPIRED;
|
const isExpired = activeLicense.state === LicenseState.EXPIRED;
|
||||||
const isCancelled = activeLicenseV3.state === LicenseState.CANCELLED;
|
const isCancelled = activeLicense.state === LicenseState.CANCELLED;
|
||||||
const isDefaulted = activeLicenseV3.state === LicenseState.DEFAULTED;
|
const isDefaulted = activeLicense.state === LicenseState.DEFAULTED;
|
||||||
const isEvaluationExpired =
|
const isEvaluationExpired =
|
||||||
activeLicenseV3.state === LicenseState.EVALUATION_EXPIRED;
|
activeLicense.state === LicenseState.EVALUATION_EXPIRED;
|
||||||
|
|
||||||
const isWorkspaceAccessRestricted =
|
const isWorkspaceAccessRestricted =
|
||||||
isTerminated ||
|
isTerminated ||
|
||||||
@ -287,7 +288,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
isDefaulted ||
|
isDefaulted ||
|
||||||
isEvaluationExpired;
|
isEvaluationExpired;
|
||||||
|
|
||||||
const { platform } = activeLicenseV3;
|
const { platform } = activeLicense;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
isWorkspaceAccessRestricted &&
|
isWorkspaceAccessRestricted &&
|
||||||
@ -296,17 +297,17 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
setShowWorkspaceRestricted(true);
|
setShowWorkspaceRestricted(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isFetchingActiveLicenseV3, activeLicenseV3]);
|
}, [isFetchingActiveLicense, activeLicense]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!isFetchingActiveLicenseV3 &&
|
!isFetchingActiveLicense &&
|
||||||
!isNull(activeLicenseV3) &&
|
!isNull(activeLicense) &&
|
||||||
activeLicenseV3?.event_queue?.event === LicenseEvent.DEFAULT
|
activeLicense?.event_queue?.event === LicenseEvent.DEFAULT
|
||||||
) {
|
) {
|
||||||
setShowPaymentFailedWarning(true);
|
setShowPaymentFailedWarning(true);
|
||||||
}
|
}
|
||||||
}, [activeLicenseV3, isFetchingActiveLicenseV3]);
|
}, [activeLicense, isFetchingActiveLicense]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// after logging out hide the trial expiry banner
|
// after logging out hide the trial expiry banner
|
||||||
@ -392,7 +393,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
if (
|
if (
|
||||||
!isFetchingFeatureFlags &&
|
!isFetchingFeatureFlags &&
|
||||||
(featureFlags || featureFlagsFetchError) &&
|
(featureFlags || featureFlagsFetchError) &&
|
||||||
activeLicenseV3 &&
|
activeLicense &&
|
||||||
trialInfo
|
trialInfo
|
||||||
) {
|
) {
|
||||||
let isChatSupportEnabled = false;
|
let isChatSupportEnabled = false;
|
||||||
@ -421,7 +422,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
isCloudUserVal,
|
isCloudUserVal,
|
||||||
isFetchingFeatureFlags,
|
isFetchingFeatureFlags,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
activeLicenseV3,
|
activeLicense,
|
||||||
trialInfo,
|
trialInfo,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -523,14 +524,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
const renderWorkspaceRestrictedBanner = (): JSX.Element => (
|
const renderWorkspaceRestrictedBanner = (): JSX.Element => (
|
||||||
<div className="workspace-restricted-banner">
|
<div className="workspace-restricted-banner">
|
||||||
{activeLicenseV3?.state === LicenseState.TERMINATED && (
|
{activeLicense?.state === LicenseState.TERMINATED && (
|
||||||
<>
|
<>
|
||||||
Your SigNoz license is terminated, enterprise features have been disabled.
|
Your SigNoz license is terminated, enterprise features have been disabled.
|
||||||
Please contact support at{' '}
|
Please contact support at{' '}
|
||||||
<a href="mailto:support@signoz.io">support@signoz.io</a> for new license
|
<a href="mailto:support@signoz.io">support@signoz.io</a> for new license
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{activeLicenseV3?.state === LicenseState.EXPIRED && (
|
{activeLicense?.state === LicenseState.EXPIRED && (
|
||||||
<>
|
<>
|
||||||
Your SigNoz license has expired. Please contact support at{' '}
|
Your SigNoz license has expired. Please contact support at{' '}
|
||||||
<a href="mailto:support@signoz.io">support@signoz.io</a> for renewal to
|
<a href="mailto:support@signoz.io">support@signoz.io</a> for renewal to
|
||||||
@ -544,7 +545,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{activeLicenseV3?.state === LicenseState.CANCELLED && (
|
{activeLicense?.state === LicenseState.CANCELLED && (
|
||||||
<>
|
<>
|
||||||
Your SigNoz license is cancelled. Please contact support at{' '}
|
Your SigNoz license is cancelled. Please contact support at{' '}
|
||||||
<a href="mailto:support@signoz.io">support@signoz.io</a> for reactivation
|
<a href="mailto:support@signoz.io">support@signoz.io</a> for reactivation
|
||||||
@ -559,7 +560,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeLicenseV3?.state === LicenseState.DEFAULTED && (
|
{activeLicense?.state === LicenseState.DEFAULTED && (
|
||||||
<>
|
<>
|
||||||
Your SigNoz license is defaulted. Please clear the bill to continue using
|
Your SigNoz license is defaulted. Please clear the bill to continue using
|
||||||
the enterprise features. Contact support at{' '}
|
the enterprise features. Contact support at{' '}
|
||||||
@ -575,7 +576,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeLicenseV3?.state === LicenseState.EVALUATION_EXPIRED && (
|
{activeLicense?.state === LicenseState.EVALUATION_EXPIRED && (
|
||||||
<>
|
<>
|
||||||
Your SigNoz trial has ended. Please contact support at{' '}
|
Your SigNoz trial has ended. Please contact support at{' '}
|
||||||
<a href="mailto:support@signoz.io">support@signoz.io</a> for next steps to
|
<a href="mailto:support@signoz.io">support@signoz.io</a> for next steps to
|
||||||
@ -624,7 +625,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
Your bill payment has failed. Your workspace will get suspended on{' '}
|
Your bill payment has failed. Your workspace will get suspended on{' '}
|
||||||
<span>
|
<span>
|
||||||
{getFormattedDateWithMinutes(
|
{getFormattedDateWithMinutes(
|
||||||
dayjs(activeLicenseV3?.event_queue?.scheduled_at).unix() || Date.now(),
|
dayjs(activeLicense?.event_queue?.scheduled_at).unix() || Date.now(),
|
||||||
)}
|
)}
|
||||||
.
|
.
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -16,10 +16,10 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
import updateCreditCardApi from 'api/billing/checkout';
|
|
||||||
import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
|
import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
|
||||||
import manageCreditCardApi from 'api/billing/manage';
|
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
|
import updateCreditCardApi from 'api/v1/checkout/create';
|
||||||
|
import manageCreditCardApi from 'api/v1/portal/create';
|
||||||
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 { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
@ -31,9 +31,8 @@ import { useAppContext } from 'providers/App/App';
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useMutation, useQuery } from 'react-query';
|
import { useMutation, useQuery } from 'react-query';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { SuccessResponseV2 } from 'types/api';
|
||||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||||
import { License } from 'types/api/licenses/def';
|
|
||||||
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
||||||
|
|
||||||
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
|
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
|
||||||
@ -126,7 +125,6 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
const daysRemainingStr = t('days_remaining');
|
const daysRemainingStr = t('days_remaining');
|
||||||
const [headerText, setHeaderText] = useState('');
|
const [headerText, setHeaderText] = useState('');
|
||||||
const [billAmount, setBillAmount] = useState(0);
|
const [billAmount, setBillAmount] = useState(0);
|
||||||
const [activeLicense, setActiveLicense] = useState<License | null>(null);
|
|
||||||
const [daysRemaining, setDaysRemaining] = useState(0);
|
const [daysRemaining, setDaysRemaining] = useState(0);
|
||||||
const [isFreeTrial, setIsFreeTrial] = useState(false);
|
const [isFreeTrial, setIsFreeTrial] = useState(false);
|
||||||
const [data, setData] = useState<any[]>([]);
|
const [data, setData] = useState<any[]>([]);
|
||||||
@ -137,11 +135,10 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
const {
|
const {
|
||||||
user,
|
user,
|
||||||
org,
|
org,
|
||||||
licenses,
|
|
||||||
trialInfo,
|
trialInfo,
|
||||||
isFetchingActiveLicenseV3,
|
isFetchingActiveLicense,
|
||||||
activeLicenseV3,
|
activeLicense,
|
||||||
activeLicenseV3FetchError,
|
activeLicenseFetchError,
|
||||||
} = useAppContext();
|
} = useAppContext();
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
@ -216,14 +213,9 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const activeValidLicense =
|
|
||||||
licenses?.licenses?.find((license) => license.isCurrent === true) || null;
|
|
||||||
|
|
||||||
setActiveLicense(activeValidLicense);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isFetchingActiveLicenseV3 &&
|
!isFetchingActiveLicense &&
|
||||||
!activeLicenseV3FetchError &&
|
!activeLicenseFetchError &&
|
||||||
trialInfo?.onTrial
|
trialInfo?.onTrial
|
||||||
) {
|
) {
|
||||||
const remainingDays = getRemainingDays(trialInfo?.trialEnd);
|
const remainingDays = getRemainingDays(trialInfo?.trialEnd);
|
||||||
@ -238,12 +230,11 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
licenses?.licenses,
|
activeLicense,
|
||||||
activeLicenseV3,
|
|
||||||
trialInfo?.onTrial,
|
trialInfo?.onTrial,
|
||||||
trialInfo?.trialEnd,
|
trialInfo?.trialEnd,
|
||||||
isFetchingActiveLicenseV3,
|
isFetchingActiveLicense,
|
||||||
activeLicenseV3FetchError,
|
activeLicenseFetchError,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const columns: ColumnsType<DataType> = [
|
const columns: ColumnsType<DataType> = [
|
||||||
@ -288,11 +279,11 @@ export default function BillingContainer(): JSX.Element {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleBillingOnSuccess = (
|
const handleBillingOnSuccess = (
|
||||||
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
|
data: SuccessResponseV2<CheckoutSuccessPayloadProps>,
|
||||||
): void => {
|
): void => {
|
||||||
if (data?.payload?.redirectURL) {
|
if (data?.data?.redirectURL) {
|
||||||
const newTab = document.createElement('a');
|
const newTab = document.createElement('a');
|
||||||
newTab.href = data.payload.redirectURL;
|
newTab.href = data.data.redirectURL;
|
||||||
newTab.target = '_blank';
|
newTab.target = '_blank';
|
||||||
newTab.rel = 'noopener noreferrer';
|
newTab.rel = 'noopener noreferrer';
|
||||||
newTab.click();
|
newTab.click();
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import ROUTES from 'constants/routes';
|
|||||||
import FormAlertChannels from 'container/FormAlertChannels';
|
import FormAlertChannels from 'container/FormAlertChannels';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
|
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import APIError from 'types/api/error';
|
import APIError from 'types/api/error';
|
||||||
@ -42,6 +43,7 @@ function CreateAlertChannels({
|
|||||||
}: CreateAlertChannelsProps): JSX.Element {
|
}: CreateAlertChannelsProps): JSX.Element {
|
||||||
// init namespace for translations
|
// init namespace for translations
|
||||||
const { t } = useTranslation('channels');
|
const { t } = useTranslation('channels');
|
||||||
|
const { showErrorModal } = useErrorModal();
|
||||||
|
|
||||||
const [formInstance] = Form.useForm();
|
const [formInstance] = Form.useForm();
|
||||||
|
|
||||||
@ -145,15 +147,12 @@ function CreateAlertChannels({
|
|||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
return { status: 'success', statusMessage: t('channel_creation_done') };
|
return { status: 'success', statusMessage: t('channel_creation_done') };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
showErrorModal(error as APIError);
|
||||||
message: (error as APIError).error.error.code,
|
|
||||||
description: (error as APIError).error.error.message,
|
|
||||||
});
|
|
||||||
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
} finally {
|
} finally {
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
}
|
}
|
||||||
}, [prepareSlackRequest, t, notifications]);
|
}, [prepareSlackRequest, notifications, t, showErrorModal]);
|
||||||
|
|
||||||
const prepareWebhookRequest = useCallback(() => {
|
const prepareWebhookRequest = useCallback(() => {
|
||||||
// initial api request without auth params
|
// initial api request without auth params
|
||||||
@ -202,15 +201,12 @@ function CreateAlertChannels({
|
|||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
return { status: 'success', statusMessage: t('channel_creation_done') };
|
return { status: 'success', statusMessage: t('channel_creation_done') };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
showErrorModal(error as APIError);
|
||||||
message: (error as APIError).getErrorCode(),
|
|
||||||
description: (error as APIError).getErrorMessage(),
|
|
||||||
});
|
|
||||||
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
} finally {
|
} finally {
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
}
|
}
|
||||||
}, [prepareWebhookRequest, t, notifications]);
|
}, [prepareWebhookRequest, notifications, t, showErrorModal]);
|
||||||
|
|
||||||
const preparePagerRequest = useCallback(() => {
|
const preparePagerRequest = useCallback(() => {
|
||||||
const validationError = ValidatePagerChannel(selectedConfig as PagerChannel);
|
const validationError = ValidatePagerChannel(selectedConfig as PagerChannel);
|
||||||
@ -254,15 +250,12 @@ function CreateAlertChannels({
|
|||||||
}
|
}
|
||||||
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
showErrorModal(error as APIError);
|
||||||
message: (error as APIError).getErrorCode(),
|
|
||||||
description: (error as APIError).getErrorMessage(),
|
|
||||||
});
|
|
||||||
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
} finally {
|
} finally {
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
}
|
}
|
||||||
}, [t, notifications, preparePagerRequest]);
|
}, [preparePagerRequest, t, notifications, showErrorModal]);
|
||||||
|
|
||||||
const prepareOpsgenieRequest = useCallback(
|
const prepareOpsgenieRequest = useCallback(
|
||||||
() => ({
|
() => ({
|
||||||
@ -287,15 +280,12 @@ function CreateAlertChannels({
|
|||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
return { status: 'success', statusMessage: t('channel_creation_done') };
|
return { status: 'success', statusMessage: t('channel_creation_done') };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
showErrorModal(error as APIError);
|
||||||
message: (error as APIError).getErrorCode(),
|
|
||||||
description: (error as APIError).getErrorMessage(),
|
|
||||||
});
|
|
||||||
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
} finally {
|
} finally {
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
}
|
}
|
||||||
}, [prepareOpsgenieRequest, t, notifications]);
|
}, [prepareOpsgenieRequest, notifications, t, showErrorModal]);
|
||||||
|
|
||||||
const prepareEmailRequest = useCallback(
|
const prepareEmailRequest = useCallback(
|
||||||
() => ({
|
() => ({
|
||||||
@ -320,15 +310,12 @@ function CreateAlertChannels({
|
|||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
return { status: 'success', statusMessage: t('channel_creation_done') };
|
return { status: 'success', statusMessage: t('channel_creation_done') };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
showErrorModal(error as APIError);
|
||||||
message: (error as APIError).getErrorCode(),
|
|
||||||
description: (error as APIError).getErrorMessage(),
|
|
||||||
});
|
|
||||||
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
} finally {
|
} finally {
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
}
|
}
|
||||||
}, [prepareEmailRequest, t, notifications]);
|
}, [prepareEmailRequest, notifications, t, showErrorModal]);
|
||||||
|
|
||||||
const prepareMsTeamsRequest = useCallback(
|
const prepareMsTeamsRequest = useCallback(
|
||||||
() => ({
|
() => ({
|
||||||
@ -353,15 +340,12 @@ function CreateAlertChannels({
|
|||||||
history.replace(ROUTES.ALL_CHANNELS);
|
history.replace(ROUTES.ALL_CHANNELS);
|
||||||
return { status: 'success', statusMessage: t('channel_creation_done') };
|
return { status: 'success', statusMessage: t('channel_creation_done') };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
showErrorModal(error as APIError);
|
||||||
message: (error as APIError).getErrorCode(),
|
|
||||||
description: (error as APIError).getErrorMessage(),
|
|
||||||
});
|
|
||||||
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
return { status: 'failed', statusMessage: t('channel_creation_failed') };
|
||||||
} finally {
|
} finally {
|
||||||
setSavingState(false);
|
setSavingState(false);
|
||||||
}
|
}
|
||||||
}, [prepareMsTeamsRequest, t, notifications]);
|
}, [prepareMsTeamsRequest, notifications, t, showErrorModal]);
|
||||||
|
|
||||||
const onSaveHandler = useCallback(
|
const onSaveHandler = useCallback(
|
||||||
async (value: ChannelType) => {
|
async (value: ChannelType) => {
|
||||||
@ -459,10 +443,8 @@ function CreateAlertChannels({
|
|||||||
status: 'Test success',
|
status: 'Test success',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
showErrorModal(error as APIError);
|
||||||
message: (error as APIError).error.error.code,
|
|
||||||
description: (error as APIError).error.error.message,
|
|
||||||
});
|
|
||||||
logEvent('Alert Channel: Test notification', {
|
logEvent('Alert Channel: Test notification', {
|
||||||
type: channelType,
|
type: channelType,
|
||||||
sendResolvedAlert: selectedConfig?.send_resolved,
|
sendResolvedAlert: selectedConfig?.send_resolved,
|
||||||
|
|||||||
@ -14,6 +14,8 @@ function ExplorerOptionWrapper({
|
|||||||
isLoading,
|
isLoading,
|
||||||
onExport,
|
onExport,
|
||||||
sourcepage,
|
sourcepage,
|
||||||
|
isOneChartPerQuery,
|
||||||
|
splitedQueries,
|
||||||
}: ExplorerOptionsWrapperProps): JSX.Element {
|
}: ExplorerOptionsWrapperProps): JSX.Element {
|
||||||
const [isExplorerOptionHidden, setIsExplorerOptionHidden] = useState(false);
|
const [isExplorerOptionHidden, setIsExplorerOptionHidden] = useState(false);
|
||||||
|
|
||||||
@ -32,6 +34,8 @@ function ExplorerOptionWrapper({
|
|||||||
sourcepage={sourcepage}
|
sourcepage={sourcepage}
|
||||||
isExplorerOptionHidden={isExplorerOptionHidden}
|
isExplorerOptionHidden={isExplorerOptionHidden}
|
||||||
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
|
setIsExplorerOptionHidden={setIsExplorerOptionHidden}
|
||||||
|
isOneChartPerQuery={isOneChartPerQuery}
|
||||||
|
splitedQueries={splitedQueries}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,21 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
|
.multi-alert-button,
|
||||||
|
.multi-dashboard-button {
|
||||||
|
min-width: 130px;
|
||||||
|
|
||||||
|
.ant-select-selector {
|
||||||
|
.ant-select-selection-placeholder {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-arrow {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-update {
|
.hide-update {
|
||||||
|
|||||||
@ -90,6 +90,8 @@ function ExplorerOptions({
|
|||||||
sourcepage,
|
sourcepage,
|
||||||
isExplorerOptionHidden = false,
|
isExplorerOptionHidden = false,
|
||||||
setIsExplorerOptionHidden,
|
setIsExplorerOptionHidden,
|
||||||
|
isOneChartPerQuery = false,
|
||||||
|
splitedQueries = [],
|
||||||
}: ExplorerOptionsProps): JSX.Element {
|
}: ExplorerOptionsProps): JSX.Element {
|
||||||
const [isExport, setIsExport] = useState<boolean>(false);
|
const [isExport, setIsExport] = useState<boolean>(false);
|
||||||
const [isSaveModalOpen, setIsSaveModalOpen] = useState(false);
|
const [isSaveModalOpen, setIsSaveModalOpen] = useState(false);
|
||||||
@ -99,6 +101,8 @@ function ExplorerOptions({
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const ref = useRef<RefSelectProps>(null);
|
const ref = useRef<RefSelectProps>(null);
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
const [queryToExport, setQueryToExport] = useState<Query | null>(null);
|
||||||
|
|
||||||
const isLogsExplorer = sourcepage === DataSource.LOGS;
|
const isLogsExplorer = sourcepage === DataSource.LOGS;
|
||||||
const isMetricsExplorer = sourcepage === DataSource.METRICS;
|
const isMetricsExplorer = sourcepage === DataSource.METRICS;
|
||||||
|
|
||||||
@ -149,22 +153,28 @@ function ExplorerOptions({
|
|||||||
|
|
||||||
const { user } = useAppContext();
|
const { user } = useAppContext();
|
||||||
|
|
||||||
const handleConditionalQueryModification = useCallback((): string => {
|
const handleConditionalQueryModification = useCallback(
|
||||||
|
(defaultQuery: Query | null): string => {
|
||||||
|
const queryToUse = defaultQuery || query;
|
||||||
if (
|
if (
|
||||||
query?.builder?.queryData?.[0]?.aggregateOperator !== StringOperators.NOOP
|
queryToUse?.builder?.queryData?.[0]?.aggregateOperator !==
|
||||||
|
StringOperators.NOOP
|
||||||
) {
|
) {
|
||||||
return JSON.stringify(query);
|
return JSON.stringify(queryToUse);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modify aggregateOperator to count, as noop is not supported in alerts
|
// Modify aggregateOperator to count, as noop is not supported in alerts
|
||||||
const modifiedQuery = cloneDeep(query);
|
const modifiedQuery = cloneDeep(queryToUse);
|
||||||
|
|
||||||
modifiedQuery.builder.queryData[0].aggregateOperator = StringOperators.COUNT;
|
modifiedQuery.builder.queryData[0].aggregateOperator = StringOperators.COUNT;
|
||||||
|
|
||||||
return JSON.stringify(modifiedQuery);
|
return JSON.stringify(modifiedQuery);
|
||||||
}, [query]);
|
},
|
||||||
|
[query],
|
||||||
|
);
|
||||||
|
|
||||||
const onCreateAlertsHandler = useCallback(() => {
|
const onCreateAlertsHandler = useCallback(
|
||||||
|
(defaultQuery: Query | null) => {
|
||||||
if (sourcepage === DataSource.TRACES) {
|
if (sourcepage === DataSource.TRACES) {
|
||||||
logEvent('Traces Explorer: Create alert', {
|
logEvent('Traces Explorer: Create alert', {
|
||||||
panelType,
|
panelType,
|
||||||
@ -179,21 +189,26 @@ function ExplorerOptions({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const stringifiedQuery = handleConditionalQueryModification();
|
const stringifiedQuery = handleConditionalQueryModification(defaultQuery);
|
||||||
|
|
||||||
history.push(
|
history.push(
|
||||||
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
|
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
|
||||||
stringifiedQuery,
|
stringifiedQuery,
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [handleConditionalQueryModification, history]);
|
[handleConditionalQueryModification, history],
|
||||||
|
);
|
||||||
|
|
||||||
const onCancel = (value: boolean) => (): void => {
|
const onCancel = (value: boolean) => (): void => {
|
||||||
onModalToggle(value);
|
onModalToggle(value);
|
||||||
|
if (isOneChartPerQuery) {
|
||||||
|
setQueryToExport(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAddToDashboard = (): void => {
|
const onAddToDashboard = useCallback((): void => {
|
||||||
if (sourcepage === DataSource.TRACES) {
|
if (sourcepage === DataSource.TRACES) {
|
||||||
logEvent('Traces Explorer: Add to dashboard clicked', {
|
logEvent('Traces Explorer: Add to dashboard clicked', {
|
||||||
panelType,
|
panelType,
|
||||||
@ -208,7 +223,7 @@ function ExplorerOptions({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
setIsExport(true);
|
setIsExport(true);
|
||||||
};
|
}, [isLogsExplorer, isMetricsExplorer, panelType, setIsExport, sourcepage]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: viewsData,
|
data: viewsData,
|
||||||
@ -616,6 +631,120 @@ function ExplorerOptions({
|
|||||||
return 'https://signoz.io/docs/product-features/trace-explorer/?utm_source=product&utm_medium=trace-explorer-toolbar';
|
return 'https://signoz.io/docs/product-features/trace-explorer/?utm_source=product&utm_medium=trace-explorer-toolbar';
|
||||||
}, [isLogsExplorer, isMetricsExplorer]);
|
}, [isLogsExplorer, isMetricsExplorer]);
|
||||||
|
|
||||||
|
const getQueryName = (query: Query): string => {
|
||||||
|
if (query.builder.queryFormulas.length > 0) {
|
||||||
|
return `Formula ${query.builder.queryFormulas[0].queryName}`;
|
||||||
|
}
|
||||||
|
return `Query ${query.builder.queryData[0].queryName}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const alertButton = useMemo(() => {
|
||||||
|
if (isOneChartPerQuery) {
|
||||||
|
const selectLabel = (
|
||||||
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
|
shape="round"
|
||||||
|
icon={<ConciergeBell size={16} />}
|
||||||
|
>
|
||||||
|
Create an Alert
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
disabled={disabled}
|
||||||
|
className="multi-alert-button"
|
||||||
|
placeholder={selectLabel}
|
||||||
|
value={selectLabel}
|
||||||
|
suffixIcon={null}
|
||||||
|
onSelect={(e): void => {
|
||||||
|
const selectedQuery = splitedQueries.find(
|
||||||
|
(query) => query.id === ((e as unknown) as string),
|
||||||
|
);
|
||||||
|
if (selectedQuery) {
|
||||||
|
onCreateAlertsHandler(selectedQuery);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{splitedQueries.map((splittedQuery) => (
|
||||||
|
<Select.Option key={splittedQuery.id} value={splittedQuery.id}>
|
||||||
|
{getQueryName(splittedQuery)}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
|
shape="round"
|
||||||
|
onClick={(): void => onCreateAlertsHandler(query)}
|
||||||
|
icon={<ConciergeBell size={16} />}
|
||||||
|
>
|
||||||
|
Create an Alert
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
disabled,
|
||||||
|
isOneChartPerQuery,
|
||||||
|
onCreateAlertsHandler,
|
||||||
|
query,
|
||||||
|
splitedQueries,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dashboardButton = useMemo(() => {
|
||||||
|
if (isOneChartPerQuery) {
|
||||||
|
const selectLabel = (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
disabled={disabled}
|
||||||
|
shape="round"
|
||||||
|
onClick={onAddToDashboard}
|
||||||
|
icon={<Plus size={16} />}
|
||||||
|
>
|
||||||
|
Add to Dashboard
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
disabled={disabled}
|
||||||
|
className="multi-dashboard-button"
|
||||||
|
placeholder={selectLabel}
|
||||||
|
value={selectLabel}
|
||||||
|
suffixIcon={null}
|
||||||
|
onSelect={(e): void => {
|
||||||
|
const selectedQuery = splitedQueries.find(
|
||||||
|
(query) => query.id === ((e as unknown) as string),
|
||||||
|
);
|
||||||
|
if (selectedQuery) {
|
||||||
|
setQueryToExport(() => {
|
||||||
|
onAddToDashboard();
|
||||||
|
return selectedQuery;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* eslint-disable-next-line sonarjs/no-identical-functions */}
|
||||||
|
{splitedQueries.map((splittedQuery) => (
|
||||||
|
<Select.Option key={splittedQuery.id} value={splittedQuery.id}>
|
||||||
|
{getQueryName(splittedQuery)}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
disabled={disabled}
|
||||||
|
shape="round"
|
||||||
|
onClick={onAddToDashboard}
|
||||||
|
icon={<Plus size={16} />}
|
||||||
|
>
|
||||||
|
Add to Dashboard
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}, [disabled, isOneChartPerQuery, onAddToDashboard, splitedQueries]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="explorer-options-container">
|
<div className="explorer-options-container">
|
||||||
{
|
{
|
||||||
@ -719,24 +848,8 @@ function ExplorerOptions({
|
|||||||
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
|
<hr className={isEditDeleteSupported ? '' : 'hidden'} />
|
||||||
|
|
||||||
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
|
<div className={cx('actions', isEditDeleteSupported ? '' : 'hidden')}>
|
||||||
<Button
|
{alertButton}
|
||||||
disabled={disabled}
|
{dashboardButton}
|
||||||
shape="round"
|
|
||||||
onClick={onCreateAlertsHandler}
|
|
||||||
icon={<ConciergeBell size={16} />}
|
|
||||||
>
|
|
||||||
Create an Alert
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
disabled={disabled}
|
|
||||||
shape="round"
|
|
||||||
onClick={onAddToDashboard}
|
|
||||||
icon={<Plus size={16} />}
|
|
||||||
>
|
|
||||||
Add to Dashboard
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
{/* Hide the info icon for metrics explorer until we get the docs link */}
|
{/* Hide the info icon for metrics explorer until we get the docs link */}
|
||||||
@ -818,9 +931,15 @@ function ExplorerOptions({
|
|||||||
destroyOnClose
|
destroyOnClose
|
||||||
>
|
>
|
||||||
<ExportPanelContainer
|
<ExportPanelContainer
|
||||||
query={query}
|
query={isOneChartPerQuery ? queryToExport : query}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onExport={onExport}
|
onExport={(dashboard, isNewDashboard): void => {
|
||||||
|
if (isOneChartPerQuery && queryToExport) {
|
||||||
|
onExport(dashboard, isNewDashboard, queryToExport);
|
||||||
|
} else {
|
||||||
|
onExport(dashboard, isNewDashboard);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
@ -829,18 +948,26 @@ function ExplorerOptions({
|
|||||||
|
|
||||||
export interface ExplorerOptionsProps {
|
export interface ExplorerOptionsProps {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
onExport: (dashboard: Dashboard | null, isNewDashboard?: boolean) => void;
|
onExport: (
|
||||||
|
dashboard: Dashboard | null,
|
||||||
|
isNewDashboard?: boolean,
|
||||||
|
queryToExport?: Query,
|
||||||
|
) => void;
|
||||||
query: Query | null;
|
query: Query | null;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
sourcepage: DataSource;
|
sourcepage: DataSource;
|
||||||
isExplorerOptionHidden?: boolean;
|
isExplorerOptionHidden?: boolean;
|
||||||
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
|
setIsExplorerOptionHidden?: Dispatch<SetStateAction<boolean>>;
|
||||||
|
isOneChartPerQuery?: boolean;
|
||||||
|
splitedQueries?: Query[];
|
||||||
}
|
}
|
||||||
|
|
||||||
ExplorerOptions.defaultProps = {
|
ExplorerOptions.defaultProps = {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isExplorerOptionHidden: false,
|
isExplorerOptionHidden: false,
|
||||||
setIsExplorerOptionHidden: undefined,
|
setIsExplorerOptionHidden: undefined,
|
||||||
|
isOneChartPerQuery: false,
|
||||||
|
splitedQueries: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ExplorerOptions;
|
export default ExplorerOptions;
|
||||||
|
|||||||
@ -142,6 +142,7 @@ function ChartPreview({
|
|||||||
params: {
|
params: {
|
||||||
allowSelectedIntervalForStepGen,
|
allowSelectedIntervalForStepGen,
|
||||||
},
|
},
|
||||||
|
originalGraphType: graphType,
|
||||||
},
|
},
|
||||||
alertDef?.version || DEFAULT_ENTITY_VERSION,
|
alertDef?.version || DEFAULT_ENTITY_VERSION,
|
||||||
{
|
{
|
||||||
|
|||||||
@ -94,6 +94,7 @@ function FullView({
|
|||||||
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
variables: getDashboardVariables(selectedDashboard?.data.variables),
|
||||||
fillGaps: widget.fillSpans,
|
fillGaps: widget.fillSpans,
|
||||||
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
|
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
|
||||||
|
originalGraphType: widget?.panelTypes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
updatedQuery.builder.queryData[0].pageSize = 10;
|
updatedQuery.builder.queryData[0].pageSize = 10;
|
||||||
|
|||||||
@ -208,6 +208,7 @@ function GridCardGraph({
|
|||||||
: globalSelectedInterval,
|
: globalSelectedInterval,
|
||||||
start: customTimeRange?.startTime || start,
|
start: customTimeRange?.startTime || start,
|
||||||
end: customTimeRange?.endTime || end,
|
end: customTimeRange?.endTime || end,
|
||||||
|
originalGraphType: widget?.panelTypes,
|
||||||
},
|
},
|
||||||
version || DEFAULT_ENTITY_VERSION,
|
version || DEFAULT_ENTITY_VERSION,
|
||||||
{
|
{
|
||||||
|
|||||||
228
frontend/src/container/GridCardLayout/__tests__/utils.test.ts
Normal file
228
frontend/src/container/GridCardLayout/__tests__/utils.test.ts
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import { getStepIntervalPoints, updateStepInterval } from '../utils';
|
||||||
|
|
||||||
|
describe('GridCardLayout Utils', () => {
|
||||||
|
describe('getStepIntervalPoints', () => {
|
||||||
|
it('should return 60 points for duration <= 1 hour', () => {
|
||||||
|
// 30 minutes in milliseconds
|
||||||
|
const start = Date.now();
|
||||||
|
const end = start + 30 * 60 * 1000;
|
||||||
|
|
||||||
|
expect(getStepIntervalPoints(start, end)).toBe(60);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 60 points for exactly 1 hour', () => {
|
||||||
|
// 1 hour in milliseconds
|
||||||
|
const start = Date.now();
|
||||||
|
const end = start + 60 * 60 * 1000;
|
||||||
|
|
||||||
|
expect(getStepIntervalPoints(start, end)).toBe(60);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 120 points for duration <= 3 hours', () => {
|
||||||
|
// 2 hours in milliseconds
|
||||||
|
const start = Date.now();
|
||||||
|
const end = start + 2 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
expect(getStepIntervalPoints(start, end)).toBe(120);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 120 points for exactly 3 hours', () => {
|
||||||
|
// 3 hours in milliseconds
|
||||||
|
const start = Date.now();
|
||||||
|
const end = start + 3 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
expect(getStepIntervalPoints(start, end)).toBe(120);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 180 points for duration <= 5 hours', () => {
|
||||||
|
// 4 hours in milliseconds
|
||||||
|
const start = Date.now();
|
||||||
|
const end = start + 4 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
expect(getStepIntervalPoints(start, end)).toBe(180);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 180 points for exactly 5 hours', () => {
|
||||||
|
// 5 hours in milliseconds
|
||||||
|
const start = Date.now();
|
||||||
|
const end = start + 5 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
expect(getStepIntervalPoints(start, end)).toBe(180);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate dynamic interval for duration > 5 hours', () => {
|
||||||
|
// 10 hours in milliseconds
|
||||||
|
const start = Date.now();
|
||||||
|
const end = start + 10 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
const result = getStepIntervalPoints(start, end);
|
||||||
|
|
||||||
|
// For 10 hours (600 minutes), interval should be ceil(600/80) = 8, rounded to 10, then * 60 = 600
|
||||||
|
expect(result).toBe(600);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle very long durations correctly', () => {
|
||||||
|
// 7 days in milliseconds
|
||||||
|
const start = Date.now();
|
||||||
|
const end = start + 7 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
const result = getStepIntervalPoints(start, end);
|
||||||
|
|
||||||
|
// For 7 days (10080 minutes), interval should be ceil(10080/80) = 126, rounded to 130, then * 60 = 7800
|
||||||
|
expect(result).toBe(7800);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should round up to nearest multiple of 5 minutes', () => {
|
||||||
|
// 12 hours in milliseconds
|
||||||
|
const start = Date.now();
|
||||||
|
const end = start + 12 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
const result = getStepIntervalPoints(start, end);
|
||||||
|
|
||||||
|
// For 12 hours (720 minutes), interval should be ceil(720/80) = 9, rounded to 10, then * 60 = 600
|
||||||
|
expect(result).toBe(600);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle edge case with very small duration', () => {
|
||||||
|
// 1 minute in milliseconds
|
||||||
|
const start = Date.now();
|
||||||
|
const end = start + 1 * 60 * 1000;
|
||||||
|
|
||||||
|
expect(getStepIntervalPoints(start, end)).toBe(60);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle zero duration', () => {
|
||||||
|
const start = Date.now();
|
||||||
|
const end = start;
|
||||||
|
|
||||||
|
expect(getStepIntervalPoints(start, end)).toBe(60);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateStepInterval', () => {
|
||||||
|
const mockQuery: Query = {
|
||||||
|
queryType: EQueryType.QUERY_BUILDER,
|
||||||
|
builder: {
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
stepInterval: 30,
|
||||||
|
aggregateOperator: 'avg',
|
||||||
|
dataSource: DataSource.METRICS,
|
||||||
|
queryName: 'A',
|
||||||
|
aggregateAttribute: { key: 'cpu_usage', type: 'Gauge' },
|
||||||
|
timeAggregation: 'avg',
|
||||||
|
spaceAggregation: 'avg',
|
||||||
|
functions: [],
|
||||||
|
filters: { items: [], op: 'AND' },
|
||||||
|
expression: 'A',
|
||||||
|
disabled: false,
|
||||||
|
having: [],
|
||||||
|
groupBy: [],
|
||||||
|
orderBy: [],
|
||||||
|
limit: null,
|
||||||
|
offset: 0,
|
||||||
|
pageSize: 0,
|
||||||
|
reduceTo: 'avg',
|
||||||
|
legend: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFormulas: [],
|
||||||
|
},
|
||||||
|
clickhouse_sql: [],
|
||||||
|
promql: [],
|
||||||
|
id: 'test-query',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should update stepInterval based on time range', () => {
|
||||||
|
// 2 hours duration
|
||||||
|
const minTime = Date.now();
|
||||||
|
const maxTime = minTime + 2 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
const result = updateStepInterval(mockQuery, minTime, maxTime);
|
||||||
|
|
||||||
|
expect(result.builder.queryData[0].stepInterval).toBe(120);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve other query properties', () => {
|
||||||
|
const minTime = Date.now();
|
||||||
|
const maxTime = minTime + 1 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
const result = updateStepInterval(mockQuery, minTime, maxTime);
|
||||||
|
|
||||||
|
expect(result.builder.queryData[0].aggregateOperator).toBe('avg');
|
||||||
|
expect(result.builder.queryData[0].queryName).toBe('A');
|
||||||
|
expect(result.builder.queryData[0].dataSource).toBe('metrics');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple queryData items', () => {
|
||||||
|
const multiQueryMock: Query = {
|
||||||
|
...mockQuery,
|
||||||
|
builder: {
|
||||||
|
queryData: [
|
||||||
|
...mockQuery.builder.queryData,
|
||||||
|
{
|
||||||
|
...mockQuery.builder.queryData[0],
|
||||||
|
queryName: 'B',
|
||||||
|
stepInterval: 45,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFormulas: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const minTime = Date.now();
|
||||||
|
const maxTime = minTime + 4 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
const result = updateStepInterval(multiQueryMock, minTime, maxTime);
|
||||||
|
|
||||||
|
expect(result.builder.queryData).toHaveLength(2);
|
||||||
|
expect(result.builder.queryData[0].stepInterval).toBe(180);
|
||||||
|
expect(result.builder.queryData[1].stepInterval).toBe(180);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use calculated stepInterval when original is undefined', () => {
|
||||||
|
const queryWithUndefinedStep: Query = {
|
||||||
|
...mockQuery,
|
||||||
|
builder: {
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...mockQuery.builder.queryData[0],
|
||||||
|
stepInterval: undefined as any,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFormulas: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const minTime = Date.now();
|
||||||
|
const maxTime = minTime + 1 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
const result = updateStepInterval(queryWithUndefinedStep, minTime, maxTime);
|
||||||
|
|
||||||
|
expect(result.builder.queryData[0].stepInterval).toBe(60);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fallback to 60 when calculated stepInterval is 0', () => {
|
||||||
|
const minTime = Date.now();
|
||||||
|
const maxTime = minTime; // Same time = 0 duration
|
||||||
|
|
||||||
|
const result = updateStepInterval(mockQuery, minTime, maxTime);
|
||||||
|
|
||||||
|
expect(result.builder.queryData[0].stepInterval).toBe(60);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle very large time ranges', () => {
|
||||||
|
const minTime = Date.now();
|
||||||
|
const maxTime = minTime + 30 * 24 * 60 * 60 * 1000; // 30 days
|
||||||
|
|
||||||
|
const result = updateStepInterval(mockQuery, minTime, maxTime);
|
||||||
|
|
||||||
|
// Should calculate appropriate interval for 30 days
|
||||||
|
expect(result.builder.queryData[0].stepInterval).toBeGreaterThan(180);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -2,6 +2,7 @@ import { FORMULA_REGEXP } from 'constants/regExp';
|
|||||||
import { isEmpty, isEqual } from 'lodash-es';
|
import { isEmpty, isEqual } from 'lodash-es';
|
||||||
import { Layout } from 'react-grid-layout';
|
import { Layout } from 'react-grid-layout';
|
||||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
export const removeUndefinedValuesFromLayout = (layout: Layout[]): Layout[] =>
|
export const removeUndefinedValuesFromLayout = (layout: Layout[]): Layout[] =>
|
||||||
layout.map((obj) =>
|
layout.map((obj) =>
|
||||||
@ -51,3 +52,63 @@ export const hasColumnWidthsChanged = (
|
|||||||
return !isEqual(newWidths, existingWidths);
|
return !isEqual(newWidths, existingWidths);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the step interval in uPlot points (1 minute = 60 points)
|
||||||
|
* based on the time duration between two timestamps in nanoseconds.
|
||||||
|
*
|
||||||
|
* Conversion logic:
|
||||||
|
* - <= 1 hr → 1 min (60 points)
|
||||||
|
* - <= 3 hr → 2 min (120 points)
|
||||||
|
* - <= 5 hr → 3 min (180 points)
|
||||||
|
* - > 5 hr → max 80 bars, ceil((end-start)/80), rounded to nearest multiple of 5 min
|
||||||
|
*
|
||||||
|
* @param startNano - start time in nanoseconds
|
||||||
|
* @param endNano - end time in nanoseconds
|
||||||
|
* @returns stepInterval in uPlot points
|
||||||
|
*/
|
||||||
|
export function getStepIntervalPoints(
|
||||||
|
startNano: number,
|
||||||
|
endNano: number,
|
||||||
|
): number {
|
||||||
|
const startMs = startNano;
|
||||||
|
const endMs = endNano;
|
||||||
|
const durationMs = endMs - startMs;
|
||||||
|
const durationMin = durationMs / (60 * 1000); // convert to minutes
|
||||||
|
|
||||||
|
if (durationMin <= 60) {
|
||||||
|
return 60; // 1 min
|
||||||
|
}
|
||||||
|
if (durationMin <= 180) {
|
||||||
|
return 120; // 2 min
|
||||||
|
}
|
||||||
|
if (durationMin <= 300) {
|
||||||
|
return 180; // 3 min
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalPoints = Math.ceil(durationMs / (1000 * 60)); // total minutes
|
||||||
|
const interval = Math.ceil(totalPoints / 80); // at most 80 bars
|
||||||
|
const roundedInterval = Math.ceil(interval / 5) * 5; // round up to nearest 5
|
||||||
|
return roundedInterval * 60; // convert min to points
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateStepInterval(
|
||||||
|
query: Query,
|
||||||
|
minTime: number,
|
||||||
|
maxTime: number,
|
||||||
|
): Query {
|
||||||
|
const stepIntervalPoints = getStepIntervalPoints(minTime, maxTime);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
builder: {
|
||||||
|
...query.builder,
|
||||||
|
queryData: [
|
||||||
|
...query.builder.queryData.map((queryData) => ({
|
||||||
|
...queryData,
|
||||||
|
stepInterval: stepIntervalPoints || queryData.stepInterval || 60,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -19,12 +19,12 @@ function DataSourceInfo({
|
|||||||
dataSentToSigNoz: boolean;
|
dataSentToSigNoz: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const { activeLicenseV3 } = useAppContext();
|
const { activeLicense } = useAppContext();
|
||||||
|
|
||||||
const notSendingData = !dataSentToSigNoz;
|
const notSendingData = !dataSentToSigNoz;
|
||||||
|
|
||||||
const isEnabled =
|
const isEnabled =
|
||||||
activeLicenseV3 && activeLicenseV3.platform === LicensePlatform.CLOUD;
|
activeLicense && activeLicense.platform === LicensePlatform.CLOUD;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: deploymentsData,
|
data: deploymentsData,
|
||||||
@ -88,8 +88,8 @@ function DataSourceInfo({
|
|||||||
logEvent('Homepage: Connect dataSource clicked', {});
|
logEvent('Homepage: Connect dataSource clicked', {});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
activeLicenseV3 &&
|
activeLicense &&
|
||||||
activeLicenseV3.platform === LicensePlatform.CLOUD
|
activeLicense.platform === LicensePlatform.CLOUD
|
||||||
) {
|
) {
|
||||||
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
|
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||||
} else {
|
} else {
|
||||||
@ -105,8 +105,8 @@ function DataSourceInfo({
|
|||||||
logEvent('Homepage: Connect dataSource clicked', {});
|
logEvent('Homepage: Connect dataSource clicked', {});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
activeLicenseV3 &&
|
activeLicense &&
|
||||||
activeLicenseV3.platform === LicensePlatform.CLOUD
|
activeLicense.platform === LicensePlatform.CLOUD
|
||||||
) {
|
) {
|
||||||
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
|
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import { useGetDeploymentsData } from 'hooks/CustomDomain/useGetDeploymentsData'
|
|||||||
import { useGetHostList } from 'hooks/infraMonitoring/useGetHostList';
|
import { useGetHostList } from 'hooks/infraMonitoring/useGetHostList';
|
||||||
import { useGetK8sPodsList } from 'hooks/infraMonitoring/useGetK8sPodsList';
|
import { useGetK8sPodsList } from 'hooks/infraMonitoring/useGetK8sPodsList';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
|
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import cloneDeep from 'lodash-es/cloneDeep';
|
import cloneDeep from 'lodash-es/cloneDeep';
|
||||||
import { CompassIcon, DotIcon, HomeIcon, Plus, Wrench, X } from 'lucide-react';
|
import { CompassIcon, DotIcon, HomeIcon, Plus, Wrench, X } from 'lucide-react';
|
||||||
@ -54,6 +55,8 @@ export default function Home(): JSX.Element {
|
|||||||
const [updatingUserPreferences, setUpdatingUserPreferences] = useState(false);
|
const [updatingUserPreferences, setUpdatingUserPreferences] = useState(false);
|
||||||
const [loadingUserPreferences, setLoadingUserPreferences] = useState(true);
|
const [loadingUserPreferences, setLoadingUserPreferences] = useState(true);
|
||||||
|
|
||||||
|
const { isCommunityUser, isCommunityEnterpriseUser } = useGetTenantLicense();
|
||||||
|
|
||||||
const [checklistItems, setChecklistItems] = useState<ChecklistItem[]>(
|
const [checklistItems, setChecklistItems] = useState<ChecklistItem[]>(
|
||||||
defaultChecklistItemsState,
|
defaultChecklistItemsState,
|
||||||
);
|
);
|
||||||
@ -300,17 +303,17 @@ export default function Home(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [hostData, k8sPodsData, handleUpdateChecklistDoneItem]);
|
}, [hostData, k8sPodsData, handleUpdateChecklistDoneItem]);
|
||||||
|
|
||||||
const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext();
|
const { activeLicense, isFetchingActiveLicense } = useAppContext();
|
||||||
|
|
||||||
const [isEnabled, setIsEnabled] = useState(false);
|
const [isEnabled, setIsEnabled] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isFetchingActiveLicenseV3) {
|
if (isFetchingActiveLicense) {
|
||||||
setIsEnabled(false);
|
setIsEnabled(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsEnabled(Boolean(activeLicenseV3?.platform === LicensePlatform.CLOUD));
|
setIsEnabled(Boolean(activeLicense?.platform === LicensePlatform.CLOUD));
|
||||||
}, [activeLicenseV3, isFetchingActiveLicenseV3]);
|
}, [activeLicense, isFetchingActiveLicense]);
|
||||||
|
|
||||||
const { data: deploymentsData } = useGetDeploymentsData(isEnabled);
|
const { data: deploymentsData } = useGetDeploymentsData(isEnabled);
|
||||||
|
|
||||||
@ -323,22 +326,27 @@ export default function Home(): JSX.Element {
|
|||||||
setIsBannerDismissed(true);
|
setIsBannerDismissed(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showBanner = useMemo(
|
||||||
|
() => !isBannerDismissed && (isCommunityUser || isCommunityEnterpriseUser),
|
||||||
|
[isBannerDismissed, isCommunityUser, isCommunityEnterpriseUser],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="home-container">
|
<div className="home-container">
|
||||||
<div className="sticky-header">
|
<div className="sticky-header">
|
||||||
{!isBannerDismissed && (
|
{showBanner && (
|
||||||
<div className="home-container-banner">
|
<div className="home-container-banner">
|
||||||
<div className="home-container-banner-content">
|
<div className="home-container-banner-content">
|
||||||
Big news: SigNoz Cloud Teams plan now starting at just $49/Month -
|
Big News: SigNoz Community Edition now available with SSO (Google OAuth)
|
||||||
|
and API keys -
|
||||||
<a
|
<a
|
||||||
href="https://signoz.io/blog/cloud-teams-plan-now-at-49usd/"
|
href="https://signoz.io/blog/open-source-signoz-now-available-with-sso-and-api-keys/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="home-container-banner-link"
|
className="home-container-banner-link"
|
||||||
>
|
>
|
||||||
<i>read more</i>
|
<i>read more</i>
|
||||||
</a>
|
</a>
|
||||||
🥳🎉
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="home-container-banner-close">
|
<div className="home-container-banner-close">
|
||||||
|
|||||||
@ -32,7 +32,7 @@ function HomeChecklist({
|
|||||||
onSkip: (item: ChecklistItem) => void;
|
onSkip: (item: ChecklistItem) => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const { user, activeLicenseV3 } = useAppContext();
|
const { user, activeLicense } = useAppContext();
|
||||||
|
|
||||||
const [completedChecklistItems, setCompletedChecklistItems] = useState<
|
const [completedChecklistItems, setCompletedChecklistItems] = useState<
|
||||||
ChecklistItem[]
|
ChecklistItem[]
|
||||||
@ -94,8 +94,8 @@ function HomeChecklist({
|
|||||||
if (item.toRoute !== ROUTES.GET_STARTED_WITH_CLOUD) {
|
if (item.toRoute !== ROUTES.GET_STARTED_WITH_CLOUD) {
|
||||||
history.push(item.toRoute || '');
|
history.push(item.toRoute || '');
|
||||||
} else if (
|
} else if (
|
||||||
activeLicenseV3 &&
|
activeLicense &&
|
||||||
activeLicenseV3.platform === LicensePlatform.CLOUD
|
activeLicense.platform === LicensePlatform.CLOUD
|
||||||
) {
|
) {
|
||||||
history.push(item.toRoute || '');
|
history.push(item.toRoute || '');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import {
|
import {
|
||||||
LicensePlatform,
|
LicensePlatform,
|
||||||
LicenseV3ResModel,
|
LicenseResModel,
|
||||||
} from 'types/api/licensesV3/getActive';
|
} from 'types/api/licensesV3/getActive';
|
||||||
import { ServicesList } from 'types/api/metrics/getService';
|
import { ServicesList } from 'types/api/metrics/getService';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
@ -42,7 +42,7 @@ const EmptyState = memo(
|
|||||||
activeLicenseV3,
|
activeLicenseV3,
|
||||||
}: {
|
}: {
|
||||||
user: IUser;
|
user: IUser;
|
||||||
activeLicenseV3: LicenseV3ResModel | null;
|
activeLicenseV3: LicenseResModel | null;
|
||||||
}): JSX.Element => (
|
}): JSX.Element => (
|
||||||
<div className="empty-state-container">
|
<div className="empty-state-container">
|
||||||
<div className="empty-state-content-container">
|
<div className="empty-state-content-container">
|
||||||
@ -146,7 +146,7 @@ function ServiceMetrics({
|
|||||||
GlobalReducer
|
GlobalReducer
|
||||||
>((state) => state.globalTime);
|
>((state) => state.globalTime);
|
||||||
|
|
||||||
const { user, activeLicenseV3 } = useAppContext();
|
const { user, activeLicense } = useAppContext();
|
||||||
|
|
||||||
const [timeRange, setTimeRange] = useState(() => {
|
const [timeRange, setTimeRange] = useState(() => {
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
@ -335,7 +335,7 @@ function ServiceMetrics({
|
|||||||
{servicesExist ? (
|
{servicesExist ? (
|
||||||
<ServicesListTable services={top5Services} onRowClick={handleRowClick} />
|
<ServicesListTable services={top5Services} onRowClick={handleRowClick} />
|
||||||
) : (
|
) : (
|
||||||
<EmptyState user={user} activeLicenseV3={activeLicenseV3} />
|
<EmptyState user={user} activeLicenseV3={activeLicense} />
|
||||||
)}
|
)}
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export default function ServiceTraces({
|
|||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { user, activeLicenseV3 } = useAppContext();
|
const { user, activeLicense } = useAppContext();
|
||||||
|
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
const [timeRange, setTimeRange] = useState({
|
const [timeRange, setTimeRange] = useState({
|
||||||
@ -124,8 +124,8 @@ export default function ServiceTraces({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
activeLicenseV3 &&
|
activeLicense &&
|
||||||
activeLicenseV3.platform === LicensePlatform.CLOUD
|
activeLicense.platform === LicensePlatform.CLOUD
|
||||||
) {
|
) {
|
||||||
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
|
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
|
||||||
} else {
|
} else {
|
||||||
@ -160,7 +160,7 @@ export default function ServiceTraces({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
[user?.role, activeLicenseV3],
|
[user?.role, activeLicense],
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderDashboardsList = useCallback(
|
const renderDashboardsList = useCallback(
|
||||||
|
|||||||
@ -8,6 +8,11 @@ import HostMetricDetail from 'components/HostMetricsDetail';
|
|||||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||||
import { InfraMonitoringEvents } from 'constants/events';
|
import { InfraMonitoringEvents } from 'constants/events';
|
||||||
|
import {
|
||||||
|
getFiltersFromParams,
|
||||||
|
getOrderByFromParams,
|
||||||
|
} from 'container/InfraMonitoringK8s/commonUtils';
|
||||||
|
import { INFRA_MONITORING_K8S_PARAMS_KEYS } from 'container/InfraMonitoringK8s/constants';
|
||||||
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
|
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
|
||||||
import { useGetHostList } from 'hooks/infraMonitoring/useGetHostList';
|
import { useGetHostList } from 'hooks/infraMonitoring/useGetHostList';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
@ -15,6 +20,7 @@ import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations
|
|||||||
import { Filter } from 'lucide-react';
|
import { Filter } from 'lucide-react';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
@ -27,20 +33,51 @@ function HostsList(): JSX.Element {
|
|||||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [filters, setFilters] = useState<IBuilderQuery['filters']>({
|
const [filters, setFilters] = useState<IBuilderQuery['filters']>(() => {
|
||||||
|
const filters = getFiltersFromParams(
|
||||||
|
searchParams,
|
||||||
|
INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS,
|
||||||
|
);
|
||||||
|
if (!filters) {
|
||||||
|
return {
|
||||||
items: [],
|
items: [],
|
||||||
op: 'and',
|
op: 'and',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return filters;
|
||||||
});
|
});
|
||||||
const [showFilters, setShowFilters] = useState<boolean>(true);
|
const [showFilters, setShowFilters] = useState<boolean>(true);
|
||||||
|
|
||||||
const [orderBy, setOrderBy] = useState<{
|
const [orderBy, setOrderBy] = useState<{
|
||||||
columnName: string;
|
columnName: string;
|
||||||
order: 'asc' | 'desc';
|
order: 'asc' | 'desc';
|
||||||
} | null>(null);
|
} | null>(() => getOrderByFromParams(searchParams));
|
||||||
|
|
||||||
const [selectedHostName, setSelectedHostName] = useState<string | null>(null);
|
const handleOrderByChange = (
|
||||||
|
orderBy: {
|
||||||
|
columnName: string;
|
||||||
|
order: 'asc' | 'desc';
|
||||||
|
} | null,
|
||||||
|
): void => {
|
||||||
|
setOrderBy(orderBy);
|
||||||
|
setSearchParams({
|
||||||
|
...Object.fromEntries(searchParams.entries()),
|
||||||
|
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(orderBy),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const [selectedHostName, setSelectedHostName] = useState<string | null>(() => {
|
||||||
|
const hostName = searchParams.get('hostName');
|
||||||
|
return hostName || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleHostClick = (hostName: string): void => {
|
||||||
|
setSelectedHostName(hostName);
|
||||||
|
setSearchParams({ ...searchParams, hostName });
|
||||||
|
};
|
||||||
|
|
||||||
const { pageSize, setPageSize } = usePageSize('hosts');
|
const { pageSize, setPageSize } = usePageSize('hosts');
|
||||||
|
|
||||||
@ -82,6 +119,10 @@ function HostsList(): JSX.Element {
|
|||||||
const isNewFilterAdded = value.items.length !== filters.items.length;
|
const isNewFilterAdded = value.items.length !== filters.items.length;
|
||||||
setFilters(value);
|
setFilters(value);
|
||||||
handleChangeQueryData('filters', value);
|
handleChangeQueryData('filters', value);
|
||||||
|
setSearchParams({
|
||||||
|
...Object.fromEntries(searchParams.entries()),
|
||||||
|
[INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS]: JSON.stringify(value),
|
||||||
|
});
|
||||||
if (isNewFilterAdded) {
|
if (isNewFilterAdded) {
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
|
|
||||||
@ -161,7 +202,10 @@ function HostsList(): JSX.Element {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<HostsListControls handleFiltersChange={handleFiltersChange} />
|
<HostsListControls
|
||||||
|
filters={filters}
|
||||||
|
handleFiltersChange={handleFiltersChange}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<HostsListTable
|
<HostsListTable
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
@ -172,10 +216,10 @@ function HostsList(): JSX.Element {
|
|||||||
filters={filters}
|
filters={filters}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
setCurrentPage={setCurrentPage}
|
setCurrentPage={setCurrentPage}
|
||||||
setSelectedHostName={setSelectedHostName}
|
onHostClick={handleHostClick}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
setPageSize={setPageSize}
|
setPageSize={setPageSize}
|
||||||
setOrderBy={setOrderBy}
|
setOrderBy={handleOrderByChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -10,8 +10,10 @@ import { DataSource } from 'types/common/queryBuilder';
|
|||||||
|
|
||||||
function HostsListControls({
|
function HostsListControls({
|
||||||
handleFiltersChange,
|
handleFiltersChange,
|
||||||
|
filters,
|
||||||
}: {
|
}: {
|
||||||
handleFiltersChange: (value: IBuilderQuery['filters']) => void;
|
handleFiltersChange: (value: IBuilderQuery['filters']) => void;
|
||||||
|
filters: IBuilderQuery['filters'];
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const currentQuery = initialQueriesMap[DataSource.METRICS];
|
const currentQuery = initialQueriesMap[DataSource.METRICS];
|
||||||
const updatedCurrentQuery = useMemo(
|
const updatedCurrentQuery = useMemo(
|
||||||
@ -26,11 +28,12 @@ function HostsListControls({
|
|||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
...currentQuery.builder.queryData[0].aggregateAttribute,
|
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||||
},
|
},
|
||||||
|
filters,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[currentQuery],
|
[currentQuery, filters],
|
||||||
);
|
);
|
||||||
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export default function HostsListTable({
|
|||||||
tableData: data,
|
tableData: data,
|
||||||
hostMetricsData,
|
hostMetricsData,
|
||||||
filters,
|
filters,
|
||||||
setSelectedHostName,
|
onHostClick,
|
||||||
currentPage,
|
currentPage,
|
||||||
setCurrentPage,
|
setCurrentPage,
|
||||||
pageSize,
|
pageSize,
|
||||||
@ -77,7 +77,7 @@ export default function HostsListTable({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleRowClick = (record: HostRowData): void => {
|
const handleRowClick = (record: HostRowData): void => {
|
||||||
setSelectedHostName(record.hostName);
|
onHostClick(record.hostName);
|
||||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||||
entity: InfraMonitoringEvents.HostEntity,
|
entity: InfraMonitoringEvents.HostEntity,
|
||||||
page: InfraMonitoringEvents.ListPage,
|
page: InfraMonitoringEvents.ListPage,
|
||||||
|
|||||||
@ -41,16 +41,13 @@ export interface HostsListTableProps {
|
|||||||
| undefined;
|
| undefined;
|
||||||
hostMetricsData: HostData[];
|
hostMetricsData: HostData[];
|
||||||
filters: TagFilter;
|
filters: TagFilter;
|
||||||
setSelectedHostName: Dispatch<SetStateAction<string | null>>;
|
onHostClick: (hostName: string) => void;
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
setCurrentPage: Dispatch<SetStateAction<number>>;
|
setCurrentPage: Dispatch<SetStateAction<number>>;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
setOrderBy: Dispatch<
|
setOrderBy: (
|
||||||
SetStateAction<{
|
orderBy: { columnName: string; order: 'asc' | 'desc' } | null,
|
||||||
columnName: string;
|
) => void;
|
||||||
order: 'asc' | 'desc';
|
|
||||||
} | null>
|
|
||||||
>;
|
|
||||||
setPageSize: (pageSize: number) => void;
|
setPageSize: (pageSize: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user